/** * Comprehensive tests for use-jl-cut-hotkeys hook */ import { renderHook } from "@testing-library/react" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" // Mock timeline-machine vi.mock("../../services/timeline-machine", () => ({ timelineMachine: {}, })) // Mock shortcuts registry vi.mock("@/features/keyboard-shortcuts", () => ({ shortcutsRegistry: { updateAction: vi.fn(), }, })) import { shortcutsRegistry } from "@/features/keyboard-shortcuts" import { useJLCutHotkeys } from "../../hooks/use-jl-cut-hotkeys" import type { TimelineClip, TimelineProject, TimelineTrack } from "../../types/timeline" import { MockTimelineProvider } from "../test-providers" // Mock keyboard event const mockKeyboardEvent = new KeyboardEvent("keydown", { key: "Enter" }) const mockHotkeyEvent = { keys: ["enter"], scope: "all", element: null } // Mock functions const mockCreateJCut = vi.fn() const mockCreateLCut = vi.fn() const mockResetCut = vi.fn() const mockLinkClips = vi.fn() const mockUnlinkClips = vi.fn() const mockGetLinkedPair = vi.fn() // Mock use-jl-cuts vi.mock("../../hooks/use-jl-cuts", () => ({ useJLCuts: () => ({ createJCut: mockCreateJCut, createLCut: mockCreateLCut, resetCut: mockResetCut, linkClips: mockLinkClips, unlinkClips: mockUnlinkClips, getLinkedPair: mockGetLinkedPair, }), })) // Mock project data const mockProject: TimelineProject = { id: "project-1", name: "Test Project", sections: [ { id: "section-1", name: "Section 1", tracks: [ { id: "video-track-1", type: "video", clips: [ { id: "video-clip-1", name: "Video Clip 1", trackId: "video-track-1", startTime: 0, duration: 10, } as TimelineClip, ], } as TimelineTrack, { id: "audio-track-1", type: "audio", clips: [ { id: "audio-clip-1", name: "Audio Clip 1", trackId: "audio-track-1", startTime: 0, duration: 10, } as TimelineClip, ], } as TimelineTrack, ], }, ], globalTracks: [ { id: "music-track-1", type: "music", clips: [ { id: "music-clip-1", name: "Music Clip 1", trackId: "music-track-1", startTime: 0, duration: 20, } as TimelineClip, ], } as TimelineTrack, ], } as any const mockUiState = { selectedClipIds: ["video-clip-1"], } const mockSend = vi.fn() vi.mock("../../hooks/use-timeline", () => ({ useTimeline: () => ({ project: mockProject, uiState: mockUiState, send: mockSend, }), })) describe("useJLCutHotkeys", () => { beforeEach(() => { vi.clearAllMocks() // Сбрасываем выбранные клипы mockUiState.selectedClipIds = ["video-clip-1"] // Настройка мока getLinkedPair по умолчанию mockGetLinkedPair.mockReturnValue({ videoClipId: "video-clip-1", audioClipId: "audio-clip-1", }) }) afterEach(() => { vi.clearAllTimers() }) it("должен регистрировать все горячие клавиши", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) renderHook(() => useJLCutHotkeys(), { wrapper }) // Проверяем регистрацию всех shortcuts expect(vi.mocked(shortcutsRegistry).updateAction).toHaveBeenCalledWith("j-cut", expect.any(Function)) expect(vi.mocked(shortcutsRegistry).updateAction).toHaveBeenCalledWith("l-cut", expect.any(Function)) expect(vi.mocked(shortcutsRegistry).updateAction).toHaveBeenCalledWith("j-cut-large", expect.any(Function)) expect(vi.mocked(shortcutsRegistry).updateAction).toHaveBeenCalledWith("l-cut-large", expect.any(Function)) expect(vi.mocked(shortcutsRegistry).updateAction).toHaveBeenCalledWith("reset-cut", expect.any(Function)) expect(vi.mocked(shortcutsRegistry).updateAction).toHaveBeenCalledWith("link-clips", expect.any(Function)) expect(vi.mocked(shortcutsRegistry).updateAction).toHaveBeenCalledWith("unlink-clips", expect.any(Function)) }) it("должен создавать J-Cut при нажатии J", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) renderHook(() => useJLCutHotkeys(), { wrapper }) const jCutAction = vi.mocked(shortcutsRegistry).updateAction.mock.calls.find((call) => call[0] === "j-cut")?.[1] expect(jCutAction).toBeDefined() jCutAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockGetLinkedPair).toHaveBeenCalledWith("video-clip-1") expect(mockCreateJCut).toHaveBeenCalledWith("video-clip-1", 0.5) }) it("должен создавать L-Cut при нажатии L", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) renderHook(() => useJLCutHotkeys(), { wrapper }) const lCutAction = vi.mocked(shortcutsRegistry).updateAction.mock.calls.find((call) => call[0] === "l-cut")?.[1] expect(lCutAction).toBeDefined() lCutAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockGetLinkedPair).toHaveBeenCalledWith("video-clip-1") expect(mockCreateLCut).toHaveBeenCalledWith("video-clip-1", 0.5) }) it("должен создавать J-Cut с большим смещением при нажатии Shift+J", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) renderHook(() => useJLCutHotkeys(), { wrapper }) const jCutLargeAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "j-cut-large")?.[1] expect(jCutLargeAction).toBeDefined() jCutLargeAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockGetLinkedPair).toHaveBeenCalledWith("video-clip-1") expect(mockCreateJCut).toHaveBeenCalledWith("video-clip-1", 1.5) }) it("должен создавать L-Cut с большим смещением при нажатии Shift+L", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) renderHook(() => useJLCutHotkeys(), { wrapper }) const lCutLargeAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "l-cut-large")?.[1] expect(lCutLargeAction).toBeDefined() lCutLargeAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockGetLinkedPair).toHaveBeenCalledWith("video-clip-1") expect(mockCreateLCut).toHaveBeenCalledWith("video-clip-1", 1.5) }) it("не должен создавать J/L-Cut если нет связанной пары", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // Клип без связанной пары mockGetLinkedPair.mockReturnValue(null) renderHook(() => useJLCutHotkeys(), { wrapper }) const jCutAction = vi.mocked(shortcutsRegistry).updateAction.mock.calls.find((call) => call[0] === "j-cut")?.[1] expect(jCutAction).toBeDefined() jCutAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockGetLinkedPair).toHaveBeenCalledWith("video-clip-1") expect(mockCreateJCut).not.toHaveBeenCalled() const lCutAction = vi.mocked(shortcutsRegistry).updateAction.mock.calls.find((call) => call[0] === "l-cut")?.[1] expect(lCutAction).toBeDefined() lCutAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockCreateLCut).not.toHaveBeenCalled() }) it("не должен создавать J/L-Cut если не выбран клип", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // Нет выбранных клипов mockUiState.selectedClipIds = [] renderHook(() => useJLCutHotkeys(), { wrapper }) const jCutAction = vi.mocked(shortcutsRegistry).updateAction.mock.calls.find((call) => call[0] === "j-cut")?.[1] expect(jCutAction).toBeDefined() jCutAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockGetLinkedPair).not.toHaveBeenCalled() expect(mockCreateJCut).not.toHaveBeenCalled() }) it("не должен создавать J/L-Cut если выбрано больше одного клипа", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // Выбрано несколько клипов mockUiState.selectedClipIds = ["video-clip-1", "audio-clip-1"] renderHook(() => useJLCutHotkeys(), { wrapper }) const jCutAction = vi.mocked(shortcutsRegistry).updateAction.mock.calls.find((call) => call[0] === "j-cut")?.[1] expect(jCutAction).toBeDefined() jCutAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockGetLinkedPair).not.toHaveBeenCalled() expect(mockCreateJCut).not.toHaveBeenCalled() }) it("должен сбрасывать срез при нажатии R", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) renderHook(() => useJLCutHotkeys(), { wrapper }) const resetCutAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "reset-cut")?.[1] expect(resetCutAction).toBeDefined() resetCutAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockResetCut).toHaveBeenCalledWith("video-clip-1") }) it("не должен сбрасывать срез если не выбран клип", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) mockUiState.selectedClipIds = [] renderHook(() => useJLCutHotkeys(), { wrapper }) const resetCutAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "reset-cut")?.[1] expect(resetCutAction).toBeDefined() resetCutAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockResetCut).not.toHaveBeenCalled() }) it("должен связывать видео и аудио клипы при нажатии Cmd+Alt+L", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // Выбираем видео и аудио клипы mockUiState.selectedClipIds = ["video-clip-1", "audio-clip-1"] renderHook(() => useJLCutHotkeys(), { wrapper }) const linkClipsAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "link-clips")?.[1] expect(linkClipsAction).toBeDefined() linkClipsAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockLinkClips).toHaveBeenCalledWith("video-clip-1", "audio-clip-1") }) it("должен связывать клипы в правильном порядке (видео, аудио)", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // Выбираем в обратном порядке - сначала аудио, потом видео mockUiState.selectedClipIds = ["audio-clip-1", "video-clip-1"] renderHook(() => useJLCutHotkeys(), { wrapper }) const linkClipsAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "link-clips")?.[1] expect(linkClipsAction).toBeDefined() linkClipsAction?.(mockKeyboardEvent, mockHotkeyEvent as any) // Должен автоматически определить правильный порядок expect(mockLinkClips).toHaveBeenCalledWith("video-clip-1", "audio-clip-1") }) it("должен связывать музыкальный трек с видео", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // Выбираем видео и музыкальный клип mockUiState.selectedClipIds = ["video-clip-1", "music-clip-1"] renderHook(() => useJLCutHotkeys(), { wrapper }) const linkClipsAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "link-clips")?.[1] expect(linkClipsAction).toBeDefined() linkClipsAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockLinkClips).toHaveBeenCalledWith("video-clip-1", "music-clip-1") }) it("не должен связывать клипы если выбрано не 2 клипа", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // Выбран только один клип mockUiState.selectedClipIds = ["video-clip-1"] renderHook(() => useJLCutHotkeys(), { wrapper }) const linkClipsAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "link-clips")?.[1] expect(linkClipsAction).toBeDefined() linkClipsAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockLinkClips).not.toHaveBeenCalled() // Выбрано три клипа mockUiState.selectedClipIds = ["video-clip-1", "audio-clip-1", "music-clip-1"] linkClipsAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockLinkClips).not.toHaveBeenCalled() }) it("не должен связывать клипы одного типа", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // Добавляем еще один видео клип mockProject.sections[0].tracks[0].clips.push({ id: "video-clip-2", name: "Video Clip 2", trackId: "video-track-1", startTime: 10, duration: 10, } as TimelineClip) // Выбираем два видео клипа mockUiState.selectedClipIds = ["video-clip-1", "video-clip-2"] renderHook(() => useJLCutHotkeys(), { wrapper }) const linkClipsAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "link-clips")?.[1] expect(linkClipsAction).toBeDefined() linkClipsAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockLinkClips).not.toHaveBeenCalled() // Очистка mockProject.sections[0].tracks[0].clips.pop() }) it("должен отвязывать клипы при нажатии Cmd+Alt+U", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) renderHook(() => useJLCutHotkeys(), { wrapper }) const unlinkClipsAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "unlink-clips")?.[1] expect(unlinkClipsAction).toBeDefined() unlinkClipsAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockUnlinkClips).toHaveBeenCalledWith("video-clip-1") }) it("должен отвязывать первый выбранный клип если выбрано несколько", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) mockUiState.selectedClipIds = ["video-clip-1", "audio-clip-1"] renderHook(() => useJLCutHotkeys(), { wrapper }) const unlinkClipsAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "unlink-clips")?.[1] expect(unlinkClipsAction).toBeDefined() unlinkClipsAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockUnlinkClips).toHaveBeenCalledWith("video-clip-1") }) it("не должен отвязывать если нет выбранных клипов", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) mockUiState.selectedClipIds = [] renderHook(() => useJLCutHotkeys(), { wrapper }) const unlinkClipsAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "unlink-clips")?.[1] expect(unlinkClipsAction).toBeDefined() unlinkClipsAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockUnlinkClips).not.toHaveBeenCalled() }) it("должен корректно обрабатывать отсутствие проекта", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // Временно удаляем проект const originalSections = mockProject.sections const originalGlobalTracks = mockProject.globalTracks mockProject.sections = [] mockProject.globalTracks = [] // Тест проверяет логику хука, но хук все равно вызовет createJCut // если есть linked pair. Проверим что функция вызывается, но с правильными параметрами renderHook(() => useJLCutHotkeys(), { wrapper }) const jCutAction = vi.mocked(shortcutsRegistry).updateAction.mock.calls.find((call) => call[0] === "j-cut")?.[1] expect(jCutAction).toBeDefined() jCutAction?.(mockKeyboardEvent, mockHotkeyEvent as any) // Функция может быть вызвана, но это нормально - // хук не проверяет существование клипов в проекте expect(mockGetLinkedPair).toHaveBeenCalledWith("video-clip-1") // Восстанавливаем проект mockProject.sections = originalSections mockProject.globalTracks = originalGlobalTracks }) it("должен корректно находить клипы в globalTracks", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // Выбираем клип из globalTracks mockUiState.selectedClipIds = ["music-clip-1"] mockGetLinkedPair.mockReturnValue({ videoClipId: "video-clip-1", audioClipId: "music-clip-1", }) renderHook(() => useJLCutHotkeys(), { wrapper }) const jCutAction = vi.mocked(shortcutsRegistry).updateAction.mock.calls.find((call) => call[0] === "j-cut")?.[1] expect(jCutAction).toBeDefined() jCutAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockGetLinkedPair).toHaveBeenCalledWith("music-clip-1") expect(mockCreateJCut).toHaveBeenCalledWith("music-clip-1", 0.5) }) it("должен поддерживать различные типы аудио треков", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // Сохраняем оригинальные globalTracks const originalGlobalTracks = [...mockProject.globalTracks] // Тестируем каждый тип аудио трека const audioTypes = ["audio", "music", "voiceover", "sfx", "ambient"] audioTypes.forEach((type, index) => { // Добавляем трек const track = { id: `${type}-track`, type, clips: [ { id: `${type}-clip`, name: `${type} Clip`, trackId: `${type}-track`, startTime: 0, duration: 10, } as TimelineClip, ], } as TimelineTrack mockProject.globalTracks.push(track) // Выбираем видео и новый аудио клип mockUiState.selectedClipIds = ["video-clip-1", `${type}-clip`] // Очищаем вызовы updateAction перед рендером vi.mocked(shortcutsRegistry).updateAction.mockClear() // Рендерим хук для каждого теста отдельно const { unmount } = renderHook(() => useJLCutHotkeys(), { wrapper }) const linkClipsAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "link-clips")?.[1] expect(linkClipsAction).toBeDefined() linkClipsAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockLinkClips).toHaveBeenNthCalledWith(index + 1, "video-clip-1", `${type}-clip`) // Очистка и размонтирование unmount() mockProject.globalTracks.pop() }) // Восстанавливаем оригинальные globalTracks mockProject.globalTracks = originalGlobalTracks }) it("должен работать с изображениями как с видео", () => { const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ) // Проверяем что sections и tracks существуют if (!mockProject.sections || mockProject.sections.length === 0) { mockProject.sections = [ { id: "section-1", name: "Section 1", tracks: [], }, ] as any } // Сохраняем оригинальные треки const originalTracks = [...mockProject.sections[0].tracks] // Добавляем трек с изображением const imageTrack = { id: "image-track-1", type: "image", clips: [ { id: "image-clip-1", name: "Image Clip 1", trackId: "image-track-1", startTime: 0, duration: 5, } as TimelineClip, ], } as TimelineTrack mockProject.sections[0].tracks.push(imageTrack) // Выбираем изображение и аудио mockUiState.selectedClipIds = ["image-clip-1", "audio-clip-1"] // Очищаем предыдущие вызовы vi.mocked(shortcutsRegistry).updateAction.mockClear() renderHook(() => useJLCutHotkeys(), { wrapper }) const linkClipsAction = vi .mocked(shortcutsRegistry) .updateAction.mock.calls.find((call) => call[0] === "link-clips")?.[1] expect(linkClipsAction).toBeDefined() linkClipsAction?.(mockKeyboardEvent, mockHotkeyEvent as any) expect(mockLinkClips).toHaveBeenCalledWith("image-clip-1", "audio-clip-1") // Очистка - восстанавливаем оригинальные треки mockProject.sections[0].tracks = originalTracks }) })