import { fireEvent, render, screen } from "@testing-library/react" import { beforeEach, describe, expect, it, vi } from "vitest" import { ClipTrimHandles, RippleHandles, RollHandles } from "../../../components/clip/clip-trim-handles" import { EditModeProvider } from "../../../hooks/use-edit-mode" import { EDIT_MODES } from "../../../types/edit-modes" // Mock useEditModeContext возвращаемого контекста const mockSetEditMode = vi.fn() const mockIsEditMode = vi.fn() const mockEditModeContext = { editMode: EDIT_MODES.SELECT, setEditMode: mockSetEditMode, isEditMode: mockIsEditMode, cursor: "default", } // Мокаем хук editMode vi.mock("../../../hooks/use-edit-mode", async (importOriginal) => { const actual = await importOriginal() return { ...actual, useEditModeContext: () => mockEditModeContext, } }) // Мокаем cn utility vi.mock("@/lib/utils", () => ({ cn: (...classes: any[]) => classes.filter(Boolean).join(" "), })) // Helper для создания дефолтных пропсов const createDefaultProps = () => ({ onTrimStart: vi.fn(), onTrimMove: vi.fn(), onTrimEnd: vi.fn(), isSelected: true, disabled: false, }) // Провайдер тестов с EditModeProvider const TestWrapper = ({ children }: { children: React.ReactNode }) => { return {children} } describe("ClipTrimHandles", () => { beforeEach(() => { vi.clearAllMocks() // Устанавливаем дефолтное поведение mockIsEditMode.mockImplementation((mode: string) => { return mode === EDIT_MODES.TRIM }) }) describe("Рендеринг и видимость", () => { it("должен не показывать handles когда не в trim/ripple/roll режиме", () => { mockIsEditMode.mockReturnValue(false) const props = createDefaultProps() const { container } = render() expect(container.firstChild).toBeNull() }) it("должен показывать handles в trim режиме когда клип выбран", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = createDefaultProps() render() expect(screen.getByTestId("trim-handle-start")).toBeInTheDocument() expect(screen.getByTestId("trim-handle-end")).toBeInTheDocument() }) it("должен показывать handles в ripple режиме", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.RIPPLE) const props = createDefaultProps() render() expect(screen.getByTestId("trim-handle-start")).toBeInTheDocument() expect(screen.getByTestId("trim-handle-end")).toBeInTheDocument() }) it("должен показывать handles в roll режиме", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.ROLL) const props = createDefaultProps() render() expect(screen.getByTestId("trim-handle-start")).toBeInTheDocument() expect(screen.getByTestId("trim-handle-end")).toBeInTheDocument() }) it("должен не показывать handles когда клип не выбран", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = { ...createDefaultProps(), isSelected: false } const { container } = render() expect(container.firstChild).toBeNull() }) it("должен применять кастомный className", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = { ...createDefaultProps(), className: "custom-class" } render() const startHandle = screen.getByTestId("trim-handle-start") expect(startHandle).toHaveClass("custom-class") }) }) describe("Взаимодействие с handles", () => { it("должен вызывать onTrimStart при mousedown на start handle", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = createDefaultProps() render() const startHandle = screen.getByTestId("trim-handle-start") fireEvent.mouseDown(startHandle, { clientX: 100 }) expect(props.onTrimStart).toHaveBeenCalledWith("start", 100) }) it("должен вызывать onTrimStart при mousedown на end handle", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = createDefaultProps() render() const endHandle = screen.getByTestId("trim-handle-end") fireEvent.mouseDown(endHandle, { clientX: 200 }) expect(props.onTrimStart).toHaveBeenCalledWith("end", 200) }) it("должен предотвращать default и stop propagation при mousedown", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = createDefaultProps() render() const startHandle = screen.getByTestId("trim-handle-start") // Используем реальный Event объект const preventDefault = vi.fn() const stopPropagation = vi.fn() const mouseDownEvent = new MouseEvent("mousedown", { clientX: 100, bubbles: true, cancelable: true, }) // Мокаем методы mouseDownEvent.preventDefault = preventDefault mouseDownEvent.stopPropagation = stopPropagation fireEvent(startHandle, mouseDownEvent) expect(preventDefault).toHaveBeenCalled() expect(stopPropagation).toHaveBeenCalled() }) it("должен не реагировать на mousedown когда disabled", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = { ...createDefaultProps(), disabled: true } render() const startHandle = screen.getByTestId("trim-handle-start") fireEvent.mouseDown(startHandle, { clientX: 100 }) expect(props.onTrimStart).not.toHaveBeenCalled() }) }) describe("Drag операции", () => { it("должен обрабатывать drag операцию start handle", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = createDefaultProps() render() const startHandle = screen.getByTestId("trim-handle-start") // Начинаем drag fireEvent.mouseDown(startHandle, { clientX: 100 }) // Двигаем мышь fireEvent.mouseMove(document, { clientX: 150 }) expect(props.onTrimMove).toHaveBeenCalledWith(50) // Отпускаем мышь fireEvent.mouseUp(document, { shiftKey: false }) expect(props.onTrimEnd).toHaveBeenCalledWith(true) }) it("должен обрабатывать drag операцию end handle", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = createDefaultProps() render() const endHandle = screen.getByTestId("trim-handle-end") // Начинаем drag fireEvent.mouseDown(endHandle, { clientX: 200 }) // Двигаем мышь fireEvent.mouseMove(document, { clientX: 180 }) expect(props.onTrimMove).toHaveBeenCalledWith(-20) // Отпускаем мышь fireEvent.mouseUp(document, { shiftKey: false }) expect(props.onTrimEnd).toHaveBeenCalledWith(true) }) it("должен отменять операцию при Shift+mouseup", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = createDefaultProps() render() const startHandle = screen.getByTestId("trim-handle-start") // Начинаем drag fireEvent.mouseDown(startHandle, { clientX: 100 }) // Двигаем мышь fireEvent.mouseMove(document, { clientX: 150 }) // Отпускаем с Shift (отмена) fireEvent.mouseUp(document, { shiftKey: true }) expect(props.onTrimEnd).toHaveBeenCalledWith(false) }) it("должен показывать visual feedback во время drag", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = createDefaultProps() const { container } = render() const startHandle = screen.getByTestId("trim-handle-start") // Начинаем drag fireEvent.mouseDown(startHandle, { clientX: 100 }) // Проверяем что показывается feedback const feedback = container.querySelector(".animate-pulse") expect(feedback).toBeInTheDocument() // Заканчиваем drag fireEvent.mouseUp(document, { shiftKey: false }) // Feedback должен исчезнуть expect(container.querySelector(".animate-pulse")).not.toBeInTheDocument() }) }) describe("Disabled состояние", () => { it("должен применять disabled стили", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = { ...createDefaultProps(), disabled: true } render() const startHandle = screen.getByTestId("trim-handle-start") const endHandle = screen.getByTestId("trim-handle-end") expect(startHandle).toHaveClass("cursor-not-allowed", "opacity-50") expect(endHandle).toHaveClass("cursor-not-allowed", "opacity-50") }) }) describe("Cleanup операции", () => { it("должен удалять event listeners при unmount", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = createDefaultProps() const addEventListenerSpy = vi.spyOn(document, "addEventListener") const removeEventListenerSpy = vi.spyOn(document, "removeEventListener") const { unmount } = render() const startHandle = screen.getByTestId("trim-handle-start") // Начинаем drag (добавляем listeners) fireEvent.mouseDown(startHandle, { clientX: 100 }) expect(addEventListenerSpy).toHaveBeenCalledWith("mousemove", expect.any(Function)) expect(addEventListenerSpy).toHaveBeenCalledWith("mouseup", expect.any(Function)) // Unmount компонента unmount() expect(removeEventListenerSpy).toHaveBeenCalledWith("mousemove", expect.any(Function)) expect(removeEventListenerSpy).toHaveBeenCalledWith("mouseup", expect.any(Function)) addEventListenerSpy.mockRestore() removeEventListenerSpy.mockRestore() }) }) }) describe("RippleHandles", () => { beforeEach(() => { vi.clearAllMocks() }) it("должен рендерить ClipTrimHandles когда не в ripple режиме", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = createDefaultProps() render() expect(screen.getByTestId("trim-handle-start")).toBeInTheDocument() expect(screen.getByTestId("trim-handle-end")).toBeInTheDocument() }) it("должен рендерить с orange стилями в ripple режиме", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.RIPPLE) const props = createDefaultProps() const { container } = render() const handles = container.querySelector(".border-orange-500") expect(handles).toBeInTheDocument() }) it("должен показывать ripple indicator когда showRippleIndicator=true и клип выбран", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.RIPPLE) const props = { ...createDefaultProps(), isSelected: true } const { container } = render() const indicator = container.querySelector("svg") expect(indicator).toBeInTheDocument() }) it("должен не показывать ripple indicator когда showRippleIndicator=false", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.RIPPLE) const props = { ...createDefaultProps(), isSelected: true } const { container } = render() const indicator = container.querySelector("svg") expect(indicator).not.toBeInTheDocument() }) it("должен не показывать ripple indicator когда клип не выбран", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.RIPPLE) const props = { ...createDefaultProps(), isSelected: false } const { container } = render() const indicator = container.querySelector("svg") expect(indicator).not.toBeInTheDocument() }) }) describe("RollHandles", () => { const createRollProps = () => ({ leftClipId: "clip-1", rightClipId: "clip-2", position: 150, onRollStart: vi.fn(), onRollMove: vi.fn(), onRollEnd: vi.fn(), isActive: true, disabled: false, }) beforeEach(() => { vi.clearAllMocks() }) it("должен не рендериться когда не в roll режиме", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.TRIM) const props = createRollProps() const { container } = render() expect(container.firstChild).toBeNull() }) it("должен не рендериться когда не active", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.ROLL) const props = { ...createRollProps(), isActive: false } const { container } = render() expect(container.firstChild).toBeNull() }) it("должен рендериться в roll режиме когда active", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.ROLL) const props = createRollProps() const { container } = render() const handle = container.firstChild as HTMLElement expect(handle).toBeInTheDocument() expect(handle).toHaveStyle({ left: "150px" }) }) it("должен вызывать onRollStart при mousedown", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.ROLL) const props = createRollProps() const { container } = render() const handle = container.firstChild as HTMLElement fireEvent.mouseDown(handle, { clientX: 200 }) expect(props.onRollStart).toHaveBeenCalledWith(200) }) it("должен обрабатывать roll drag операцию", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.ROLL) const props = createRollProps() const { container } = render() const handle = container.firstChild as HTMLElement // Начинаем drag fireEvent.mouseDown(handle, { clientX: 200 }) // Двигаем мышь fireEvent.mouseMove(document, { clientX: 250 }) expect(props.onRollMove).toHaveBeenCalledWith(50) // Отпускаем мышь fireEvent.mouseUp(document, { shiftKey: false }) expect(props.onRollEnd).toHaveBeenCalledWith(true) }) it("должен отменять roll операцию при Shift+mouseup", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.ROLL) const props = createRollProps() const { container } = render() const handle = container.firstChild as HTMLElement // Начинаем drag fireEvent.mouseDown(handle, { clientX: 200 }) // Отпускаем с Shift (отмена) fireEvent.mouseUp(document, { shiftKey: true }) expect(props.onRollEnd).toHaveBeenCalledWith(false) }) it("должен не реагировать на mousedown когда disabled", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.ROLL) const props = { ...createRollProps(), disabled: true } const { container } = render() const handle = container.firstChild as HTMLElement fireEvent.mouseDown(handle, { clientX: 200 }) expect(props.onRollStart).not.toHaveBeenCalled() }) it("должен применять disabled стили", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.ROLL) const props = { ...createRollProps(), disabled: true } const { container } = render() const handle = container.firstChild as HTMLElement expect(handle).toHaveClass("cursor-not-allowed", "opacity-50") }) it("должен показывать purple стили для roll handles", () => { mockIsEditMode.mockImplementation((mode: string) => mode === EDIT_MODES.ROLL) const props = createRollProps() const { container } = render() const handle = container.firstChild as HTMLElement expect(handle).toHaveClass("bg-purple-500/30") const indicator = container.querySelector(".bg-purple-500") expect(indicator).toBeInTheDocument() }) })