import { fireEvent, screen, within } from "@testing-library/react" import { beforeEach, describe, expect, it, vi } from "vitest" import { render } from "@/test/test-utils" import { RollEditHandle } from "../../../components/edit-tools/roll-edit-handle" import { TimelineClip } from "../../../types" import { EDIT_MODES } from "../../../types/edit-modes" // Мокаем useEditModeContext const mockEditModeContext = { editMode: EDIT_MODES.ROLL 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, })) describe("RollEditHandle", () => { // Создаем тестовые клипы const leftClip: TimelineClip = { id: "clip-1", trackId: "track-1", sourceId: "source-1", startTime: 10, duration: 20, trimStart: 0, trimEnd: 0, effects: [], speed: 1, volume: 1, opacity: 1, filters: [], transitions: { in: null, out: null }, metadata: {}, } const rightClip: TimelineClip = { id: "clip-2", trackId: "track-1", sourceId: "source-2", startTime: 30, // Adjacent to leftClip (10 + 20 = 30) duration: 15, trimStart: 0, trimEnd: 0, effects: [], speed: 1, volume: 1, opacity: 1, filters: [], transitions: { in: null, out: null }, metadata: {}, } const defaultProps = { leftClip, rightClip, isHovered: true, isActive: false, timeScale: 10, onRollStart: vi.fn(), } beforeEach(() => { vi.clearAllMocks() mockEditModeContext.editMode = EDIT_MODES.ROLL }) describe("Видимость компонента", () => { it("должен не отображаться когда editMode !== ROLL", () => { mockEditModeContext.editMode = EDIT_MODES.SELECT const { container } = render() const handle = container.querySelector(".cursor-ew-resize") expect(handle).not.toBeInTheDocument() }) it("должен не отображаться когда isHovered = false", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize") expect(handle).not.toBeInTheDocument() }) it("должен не отображаться когда клипы не смежные", () => { const nonAdjacentRightClip = { ...rightClip, startTime: 35, // Gap of 5 seconds } const { container } = render() const handle = container.querySelector(".cursor-ew-resize") expect(handle).not.toBeInTheDocument() }) it("должен отображаться когда все условия выполнены", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize") expect(handle).toBeInTheDocument() }) it("должен отображаться при минимальном зазоре (0.001)", () => { const almostAdjacentRightClip = { ...rightClip, startTime: 30.0005, // Gap of 0.0005 < 0.001 } const { container } = render() const handle = container.querySelector(".cursor-ew-resize") expect(handle).toBeInTheDocument() }) it("не должен отображаться при зазоре больше допустимого", () => { const gappedRightClip = { ...rightClip, startTime: 30.002, // Gap of 0.002 > 0.001 } const { container } = render() const handle = container.querySelector(".cursor-ew-resize") expect(handle).not.toBeInTheDocument() }) }) describe("Позиционирование", () => { it("должен позиционироваться на границе клипов", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() // leftEnd = 10 + 20 = 30, timeScale = 10, position = 30 * 10 = 300px expect(handle).toHaveStyle({ left: "300px" }) }) it("должен корректно масштабироваться с timeScale", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() // leftEnd = 30, timeScale = 5, position = 30 * 5 = 150px expect(handle).toHaveStyle({ left: "150px" }) }) it("должен правильно позиционироваться для клипов в начале таймлайна", () => { const earlyLeftClip = { ...leftClip, startTime: 0, duration: 5 } const earlyRightClip = { ...rightClip, startTime: 5, duration: 10 } const { container } = render( , ) const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() // leftEnd = 0 + 5 = 5, timeScale = 10, position = 5 * 10 = 50px expect(handle).toHaveStyle({ left: "50px" }) }) }) describe("Интерактивность", () => { it("должен вызывать onRollStart при mouseDown", () => { const onRollStart = vi.fn() const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() fireEvent.mouseDown(handle, { clientX: 100 }) expect(onRollStart).toHaveBeenCalledTimes(1) expect(onRollStart).toHaveBeenCalledWith(100) }) it("должен останавливать propagation при mouseDown", () => { const onRollStart = vi.fn() const stopPropagation = vi.fn() const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() // Используем fireEvent с настройкой stopPropagation const event = fireEvent.mouseDown(handle, { clientX: 100 }) // Проверяем что onRollStart был вызван (это подтверждает что обработчик работает) expect(onRollStart).toHaveBeenCalledTimes(1) }) it("не должен вызывать onRollStart если не передан", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() // Не должно быть ошибок при клике expect(() => { fireEvent.mouseDown(handle, { clientX: 100 }) }).not.toThrow() }) }) describe("Визуальное состояние", () => { it("должен иметь базовые стили когда не активен", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() expect(handle).toHaveClass("absolute", "top-0", "bottom-0", "w-4", "-translate-x-1/2") expect(handle).toHaveClass("flex", "items-center", "justify-center", "cursor-ew-resize", "z-20") expect(handle).toHaveClass("transition-all", "duration-150") const background = handle.querySelector(".bg-purple-500\\/20") expect(background).toBeInTheDocument() expect(background).toHaveClass("absolute", "inset-0", "rounded") expect(background).toHaveClass("border-x-2", "border-purple-500") expect(background).not.toHaveClass("bg-purple-500/40") }) it("должен иметь активные стили когда isActive = true", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() const background = handle.querySelector(".bg-purple-500\\/20") expect(background).toBeInTheDocument() expect(background).toHaveClass("bg-purple-500/40") }) it("должен отображать индикаторы когда активен", () => { const { container } = render() // Проверяем левый индикатор const leftIndicator = container.querySelector(".right-full") expect(leftIndicator).toBeInTheDocument() expect(leftIndicator).toHaveClass("absolute", "top-1/2", "-translate-y-1/2", "mr-1") const leftLine = leftIndicator?.querySelector(".bg-purple-500\\/50") expect(leftLine).toBeInTheDocument() expect(leftLine).toHaveClass("w-8", "h-0.5") // Проверяем правый индикатор const rightIndicator = container.querySelector(".left-full") expect(rightIndicator).toBeInTheDocument() expect(rightIndicator).toHaveClass("absolute", "top-1/2", "-translate-y-1/2", "ml-1") const rightLine = rightIndicator?.querySelector(".bg-purple-500\\/50") expect(rightLine).toBeInTheDocument() expect(rightLine).toHaveClass("w-8", "h-0.5") }) it("не должен отображать индикаторы когда не активен", () => { const { container } = render() const leftIndicator = container.querySelector(".right-full") expect(leftIndicator).not.toBeInTheDocument() const rightIndicator = container.querySelector(".left-full") expect(rightIndicator).not.toBeInTheDocument() }) }) describe("Иконка", () => { it("должен отображать иконку GripVertical", () => { const { container } = render() const icon = container.querySelector("svg") expect(icon).toBeInTheDocument() expect(icon).toHaveClass("w-4", "h-4", "text-purple-500") const iconWrapper = icon?.parentElement expect(iconWrapper).toHaveClass("relative", "z-10") }) }) describe("Tooltip", () => { it("должен отображать tooltip с правильным содержимым", () => { render() const tooltipContent = screen.getByTestId("tooltip-content") expect(tooltipContent).toBeInTheDocument() const title = within(tooltipContent).getByText("Roll Edit (W)") expect(title).toBeInTheDocument() expect(title).toHaveClass("text-sm") const description = within(tooltipContent).getByText("Drag to adjust edit point") expect(description).toBeInTheDocument() expect(description).toHaveClass("text-xs", "opacity-80") }) it("должен иметь правильные стили для tooltip", () => { render() const tooltipContent = screen.getByTestId("tooltip-content") expect(tooltipContent).toHaveClass("bg-gray-900", "text-white") }) }) describe("Граничные случаи", () => { it("должен работать с очень маленькими клипами", () => { const tinyLeftClip = { ...leftClip, startTime: 0, duration: 0.001 } const tinyRightClip = { ...rightClip, startTime: 0.001, duration: 0.001 } const { container } = render( , ) const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() expect(handle).toHaveStyle({ left: "0.01px" }) // 0.001 * 10 }) it("должен работать с очень большими значениями времени", () => { const lateLeftClip = { ...leftClip, startTime: 10000, duration: 5000 } const lateRightClip = { ...rightClip, startTime: 15000, duration: 3000 } const { container } = render( , ) const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() expect(handle).toHaveStyle({ left: "150000px" }) // 15000 * 10 }) it("должен работать с отрицательным timeScale", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() expect(handle).toHaveStyle({ left: "-300px" }) // 30 * -10 }) it("должен работать с нулевым timeScale", () => { const { container } = render() const handle = container.querySelector(".cursor-ew-resize")! expect(handle).toBeInTheDocument() expect(handle).toHaveStyle({ left: "0px" }) // 30 * 0 }) it("должен корректно обрабатывать клипы с одинаковым trackId", () => { const sameTrackRightClip = { ...rightClip, trackId: leftClip.trackId, } const { container } = render() const handle = container.querySelector(".cursor-ew-resize") expect(handle).toBeInTheDocument() }) it("должен работать с клипами на разных треках", () => { const differentTrackRightClip = { ...rightClip, trackId: "track-2", } const { container } = render() const handle = container.querySelector(".cursor-ew-resize") expect(handle).toBeInTheDocument() }) }) describe("Производительность", () => { it("не должен перерендериваться при изменении неиспользуемых свойств клипа", () => { const { rerender, container } = render() const handle1 = container.querySelector(".cursor-ew-resize") // Изменяем свойства, которые не влияют на отображение const updatedLeftClip = { ...leftClip, volume: 0.5, opacity: 0.8, effects: ["effect1"], } rerender() const handle2 = container.querySelector(".cursor-ew-resize") // Компонент должен остаться на том же месте expect(handle1).toEqual(handle2) }) it("должен перерендериваться при изменении позиции клипов", () => { const { rerender, container } = render() const handle1 = container.querySelector(".cursor-ew-resize")! expect(handle1).toHaveStyle({ left: "300px" }) const movedLeftClip = { ...leftClip, duration: 25, // Changed duration affects position } const movedRightClip = { ...rightClip, startTime: 35, // Also need to move right clip to keep them adjacent } rerender() const handle2 = container.querySelector(".cursor-ew-resize")! expect(handle2).toBeInTheDocument() expect(handle2).toHaveStyle({ left: "350px" }) // (10 + 25) * 10 }) }) })