import { fireEvent, screen, within } from "@testing-library/react" import { beforeEach, describe, expect, it, vi } from "vitest" import { render } from "@/test/test-utils" import { SlipSlideHandles } from "../../../components/edit-tools/slip-slide-handles" import { TimelineClip } from "../../../types" import { EDIT_MODES } from "../../../types/edit-modes" // Мокаем useEditModeContext const mockEditModeContext = { editMode: EDIT_MODES.SLIP as any, setEditMode: vi.fn(), isEditMode: vi.fn(), cursor: "ew-resize", } vi.mock("../../../hooks/use-edit-mode", () => ({ useEditModeContext: () => mockEditModeContext, })) // Мокаем cn utility vi.mock("@/lib/utils", () => ({ cn: (...classes: any[]) => classes.filter(Boolean).join(" "), })) // Мокаем Tooltip компоненты vi.mock("@/components/ui/tooltip", () => ({ Tooltip: ({ children }: any) => children, TooltipContent: ({ children, className }: any) => (
{children}
), TooltipProvider: ({ children }: any) => children, TooltipTrigger: ({ children }: any) => children, })) // Мокаем иконки vi.mock("lucide-react", () => ({ MoveHorizontal: ({ className }: any) => , Maximize2: ({ className }: any) => , AlertTriangle: ({ className }: any) => , })) describe("SlipSlideHandles", () => { // Создаем тестовый клип const clip: TimelineClip = { id: "clip-1", name: "Test Clip", trackId: "track-1", mediaId: "media-1", startTime: 10, duration: 20, mediaStartTime: 5, mediaEndTime: 25, offset: 5, // Для slip тестов mediaDuration: 60, // Для slip тестов effects: [], speed: 1, volume: 1, opacity: 1, filters: [], transitions: [], isReversed: false, isSelected: false, isLocked: false, createdAt: new Date(), updatedAt: new Date(), } const defaultProps = { clip, isHovered: true, isActive: false, timeScale: 10, onSlipStart: vi.fn(), onSlideStart: vi.fn(), } beforeEach(() => { vi.clearAllMocks() mockEditModeContext.editMode = EDIT_MODES.SLIP }) describe("Видимость компонента", () => { it("должен не отображаться когда isHovered = false", () => { const { container } = render() expect(container.querySelector(".cursor-ew-resize")).not.toBeInTheDocument() }) it("должен не отображаться когда editMode не SLIP или SLIDE", () => { mockEditModeContext.editMode = EDIT_MODES.SELECT const { container } = render() expect(container.querySelector(".cursor-ew-resize")).not.toBeInTheDocument() }) it("должен отображаться в SLIP режиме", () => { mockEditModeContext.editMode = EDIT_MODES.SLIP const { container } = render() const handle = container.querySelector(".cursor-ew-resize") expect(handle).toBeInTheDocument() }) it("должен отображаться в SLIDE режиме", () => { mockEditModeContext.editMode = EDIT_MODES.SLIDE const { container } = render() const handle = container.querySelector(".cursor-ew-resize") expect(handle).toBeInTheDocument() }) }) describe("Slip Mode", () => { beforeEach(() => { mockEditModeContext.editMode = EDIT_MODES.SLIP }) it("должен отображать handle в центре клипа", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() // Центр клипа: (startTime + duration/2) * timeScale = (10 + 10) * 10 = 200px expect(handle).toHaveStyle({ left: "200px" }) }) it("должен отображать media boundaries когда есть offset", () => { const { container } = render() // Проверяем контейнер медиа границ const mediaExtent = container.querySelector(".pointer-events-none")! expect(mediaExtent).toBeInTheDocument() // Left position: clipLeft - offsetPixels = 100 - 50 = 50px expect(mediaExtent).toHaveStyle({ left: "50px", width: "600px", // mediaDuration * timeScale = 60 * 10 }) // Проверяем левую границу медиа const leftBoundary = mediaExtent.querySelector(".left-0.bg-blue-500\\/50") expect(leftBoundary).toBeInTheDocument() }) it("должен отображать правую границу медиа когда есть доступное медиа справа", () => { const { container } = render() const mediaExtent = container.querySelector(".pointer-events-none")! const rightBoundary = mediaExtent.querySelector(".right-0.bg-blue-500\\/50") // mediaDuration - offset - duration = 60 - 5 - 20 = 35 > 0 expect(rightBoundary).toBeInTheDocument() }) it("не должен отображать левую границу когда offset = 0", () => { const clipNoOffset = { ...clip, offset: 0 } const { container } = render() const mediaExtent = container.querySelector(".pointer-events-none")! const leftBoundary = mediaExtent.querySelector(".left-0.bg-blue-500\\/50") expect(leftBoundary).not.toBeInTheDocument() }) it("должен отображать индикатор доступного медиа", () => { const { container } = render() const availableMediaIndicator = container.querySelector(".h-1.bg-blue-500\\/20.rounded-full") expect(availableMediaIndicator).toBeInTheDocument() expect(availableMediaIndicator).toHaveClass("absolute") expect(availableMediaIndicator).toHaveClass("top-1/2") expect(availableMediaIndicator).toHaveClass("-translate-y-1/2") expect(availableMediaIndicator).toHaveClass("left-0") expect(availableMediaIndicator).toHaveClass("right-0") }) it("должен использовать blue цвета для slip режима", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toHaveClass("bg-blue-500/20") expect(handle).toHaveClass("border-blue-500") const icon = screen.getByTestId("move-horizontal-icon") expect(icon).toHaveClass("text-blue-500") }) it("должен применять активные стили когда isActive = true", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toHaveClass("bg-blue-500/40") expect(handle).toHaveClass("scale-110") }) it("должен вызывать onSlipStart при mouseDown", () => { const onSlipStart = vi.fn() const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! fireEvent.mouseDown(handle, { clientX: 150 }) expect(onSlipStart).toHaveBeenCalledTimes(1) expect(onSlipStart).toHaveBeenCalledWith(150) }) it("должен отображать tooltip с правильным содержимым для slip", () => { render() const tooltipContent = screen.getByTestId("tooltip-content") expect(tooltipContent).toBeInTheDocument() const title = within(tooltipContent).getByText("Slip Edit (Y)") expect(title).toBeInTheDocument() const description = within(tooltipContent).getByText("Drag to slip media content") expect(description).toBeInTheDocument() }) it("должен правильно рассчитывать позиции для клипа без mediaDuration", () => { const clipNoMediaDuration = { ...clip, mediaDuration: undefined } const { container } = render() const mediaExtent = container.querySelector(".pointer-events-none")! expect(mediaExtent).toBeInTheDocument() // Должен использовать duration как fallback expect(mediaExtent).toHaveStyle({ width: "200px" }) // duration * timeScale = 20 * 10 }) it("должен работать с отрицательным timeScale", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() expect(handle).toHaveStyle({ left: "-200px" }) // (10 + 10) * -10 }) }) describe("Slide Mode", () => { beforeEach(() => { mockEditModeContext.editMode = EDIT_MODES.SLIDE }) it("должен отображать handle в центре клипа", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() // Центр клипа: (startTime + duration/2) * timeScale = (10 + 10) * 10 = 200px expect(handle).toHaveStyle({ left: "200px" }) }) it("должен отображать направляющие стрелки", () => { const { container } = render() // Проверяем контейнер стрелок const arrowsContainer = container.querySelector(".absolute.top-0.bottom-0")! expect(arrowsContainer).toBeInTheDocument() expect(arrowsContainer).toHaveStyle({ left: "100px", // startTime * timeScale width: "200px", // duration * timeScale }) // Проверяем левую стрелку const leftArrow = arrowsContainer.querySelector(".left-0.top-1\\/2") expect(leftArrow).toBeInTheDocument() const leftArrowLine = leftArrow?.querySelector(".w-8.h-0\\.5.bg-green-500\\/50") expect(leftArrowLine).toBeInTheDocument() // Проверяем правую стрелку const rightArrow = arrowsContainer.querySelector(".right-0.top-1\\/2") expect(rightArrow).toBeInTheDocument() const rightArrowLine = rightArrow?.querySelector(".w-8.h-0\\.5.bg-green-500\\/50") expect(rightArrowLine).toBeInTheDocument() }) it("должен использовать green цвета для slide режима", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toHaveClass("bg-green-500/20") expect(handle).toHaveClass("border-green-500") const icon = screen.getByTestId("maximize2-icon") expect(icon).toHaveClass("text-green-500") expect(icon).toHaveClass("rotate-90") }) it("должен применять активные стили когда isActive = true", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toHaveClass("bg-green-500/40") expect(handle).toHaveClass("scale-110") }) it("должен вызывать onSlideStart при mouseDown", () => { const onSlideStart = vi.fn() const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! fireEvent.mouseDown(handle, { clientX: 250 }) expect(onSlideStart).toHaveBeenCalledTimes(1) expect(onSlideStart).toHaveBeenCalledWith(250) }) it("должен отображать tooltip с правильным содержимым для slide", () => { render() const tooltipContent = screen.getByTestId("tooltip-content") expect(tooltipContent).toBeInTheDocument() const title = within(tooltipContent).getByText("Slide Edit (U)") expect(title).toBeInTheDocument() const description = within(tooltipContent).getByText("Drag to slide clip") expect(description).toBeInTheDocument() }) it("должен отображать стрелки с правильными стилями", () => { const { container } = render() // Проверяем стили стрелок const leftArrowHead = container.querySelector(".border-r-green-500\\/50") expect(leftArrowHead).toBeInTheDocument() expect(leftArrowHead).toHaveClass("border-t-4") expect(leftArrowHead).toHaveClass("border-t-transparent") expect(leftArrowHead).toHaveClass("border-r-4") expect(leftArrowHead).toHaveClass("border-b-4") expect(leftArrowHead).toHaveClass("border-b-transparent") const rightArrowHead = container.querySelector(".border-l-green-500\\/50") expect(rightArrowHead).toBeInTheDocument() expect(rightArrowHead).toHaveClass("border-t-4") expect(rightArrowHead).toHaveClass("border-t-transparent") expect(rightArrowHead).toHaveClass("border-l-4") expect(rightArrowHead).toHaveClass("border-b-4") expect(rightArrowHead).toHaveClass("border-b-transparent") }) }) describe("Общие функции", () => { it("должен останавливать propagation при mouseDown в любом режиме", () => { mockEditModeContext.editMode = EDIT_MODES.SLIP const onSlipStart = vi.fn() const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! fireEvent.mouseDown(handle, { clientX: 100 }) // Если onSlipStart вызван, значит обработчик сработал и stopPropagation тоже expect(onSlipStart).toHaveBeenCalledTimes(1) }) it("не должен вызывать обработчики если они не переданы", () => { mockEditModeContext.editMode = EDIT_MODES.SLIP const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! // Не должно быть ошибок при клике expect(() => { fireEvent.mouseDown(handle, { clientX: 100 }) }).not.toThrow() }) it("должен иметь правильные базовые стили для handle", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toHaveClass("absolute") expect(handle).toHaveClass("top-1/2") expect(handle).toHaveClass("left-1/2") expect(handle).toHaveClass("-translate-x-1/2") expect(handle).toHaveClass("-translate-y-1/2") expect(handle).toHaveClass("w-12") expect(handle).toHaveClass("h-12") expect(handle).toHaveClass("rounded-full") expect(handle).toHaveClass("flex") expect(handle).toHaveClass("items-center") expect(handle).toHaveClass("justify-center") expect(handle).toHaveClass("transition-all") expect(handle).toHaveClass("duration-150") }) it("должен корректно масштабироваться с разными timeScale", () => { const { rerender, container } = render() let handle = container.querySelector(".cursor-ew-resize")! expect(handle).toHaveStyle({ left: "100px" }) // (10 + 10) * 5 rerender() handle = container.querySelector(".cursor-ew-resize")! expect(handle).toHaveStyle({ left: "400px" }) // (10 + 10) * 20 }) }) describe("Граничные случаи", () => { it("должен работать с очень маленькими клипами", () => { const tinyClip = { ...clip, startTime: 0, duration: 0.01 } const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() expect(handle).toHaveStyle({ left: "0.05px" }) // (0 + 0.005) * 10 }) it("должен работать с очень большими значениями", () => { const largeClip = { ...clip, startTime: 10000, duration: 5000 } const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() expect(handle).toHaveStyle({ left: "125000px" }) // (10000 + 2500) * 10 }) it("должен работать с нулевым timeScale", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() expect(handle).toHaveStyle({ left: "0px" }) }) it("должен корректно обрабатывать клип без offset в slip режиме", () => { mockEditModeContext.editMode = EDIT_MODES.SLIP const clipNoOffset = { ...clip, offset: 0 } const { container } = render() const mediaExtent = container.querySelector(".pointer-events-none")! expect(mediaExtent).toBeInTheDocument() expect(mediaExtent).toHaveStyle({ left: "100px" }) // clipLeft - 0 }) it("должен возвращать null для неподдерживаемых режимов", () => { mockEditModeContext.editMode = EDIT_MODES.TRIM const { container } = render() expect(container.querySelector(".cursor-ew-resize")).not.toBeInTheDocument() }) }) describe("Tooltip стили", () => { it("должен иметь правильные стили для tooltip", () => { render() const tooltipContent = screen.getByTestId("tooltip-content") expect(tooltipContent).toHaveClass("bg-gray-900") expect(tooltipContent).toHaveClass("text-white") }) it("должен отображать правильный hotkey для каждого режима", () => { // Slip mode mockEditModeContext.editMode = EDIT_MODES.SLIP const { rerender } = render() let tooltipContent = screen.getByTestId("tooltip-content") expect(within(tooltipContent).getByText(/\(Y\)/)).toBeInTheDocument() // Slide mode mockEditModeContext.editMode = EDIT_MODES.SLIDE rerender() tooltipContent = screen.getByTestId("tooltip-content") expect(within(tooltipContent).getByText(/\(U\)/)).toBeInTheDocument() }) }) })