/**
* 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
})
})