/**
* Comprehensive tests for use-clip-groups hook
*/
import { act, renderHook } from "@testing-library/react"
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { useClipGroups } from "../../hooks/use-clip-groups"
import { TimelineGroupManager } from "../../services/group-manager"
import type { ClipGroup } from "../../types/clip-groups"
import type { TimelineClip, TimelineProject } from "../../types/timeline"
import { MockTimelineProvider } from "../test-providers"
// Mock timeline-machine
vi.mock("../../services/timeline-machine", () => ({
timelineMachine: {},
}))
// Mock group manager
vi.mock("../../services/group-manager", () => {
const mockGroupManager = {
groups: new Map(),
getGroupHierarchy: vi.fn().mockReturnValue([]),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
createGroup: vi.fn(),
ungroupClips: vi.fn(),
addToGroup: vi.fn(),
removeFromGroup: vi.fn(),
toggleCollapse: vi.fn(),
lockGroup: vi.fn(),
renameGroup: vi.fn(),
setGroupColor: vi.fn(),
createNestedSequence: vi.fn(),
updateNestedSequence: vi.fn(),
breakApartSequence: vi.fn(),
getGroup: vi.fn(),
getGroupByClip: vi.fn(),
isClipInGroup: vi.fn(),
}
return {
TimelineGroupManager: vi.fn(() => mockGroupManager),
}
})
// Mock useTimeline
const mockSend = vi.fn()
const mockProject: TimelineProject = {
id: "project-1",
name: "Test Project",
sections: [
{
id: "section-1",
name: "Section 1",
tracks: [
{
id: "track-1",
clips: [
{
id: "clip-1",
name: "Clip 1",
trackId: "track-1",
startTime: 0,
duration: 5,
} as TimelineClip,
{
id: "clip-2",
name: "Clip 2",
trackId: "track-1",
startTime: 5,
duration: 5,
} as TimelineClip,
],
},
],
},
],
globalTracks: [
{
id: "global-track-1",
clips: [
{
id: "clip-3",
name: "Clip 3",
trackId: "global-track-1",
startTime: 0,
duration: 10,
} as TimelineClip,
],
},
],
} as any
vi.mock("../../hooks/use-timeline", () => ({
useTimeline: vi.fn(() => ({
project: mockProject,
send: mockSend,
})),
}))
describe("useClipGroups", () => {
let mockGroupManager: any
beforeEach(() => {
vi.clearAllMocks()
// Получаем экземпляр мока
const TimelineGroupManagerMock = vi.mocked(TimelineGroupManager)
mockGroupManager = new TimelineGroupManagerMock()
// Настройка моков по умолчанию
mockGroupManager.createGroup.mockReturnValue({
success: true,
groupId: "group-1",
})
mockGroupManager.ungroupClips.mockReturnValue({
success: true,
affectedClips: [{ clipId: "clip-1", trackId: "track-1" }],
})
mockGroupManager.getGroup.mockImplementation((groupId: string) => {
if (groupId === "group-1") {
return {
id: "group-1",
name: "Test Group",
clips: [
{ clipId: "clip-1", trackId: "track-1" },
{ clipId: "clip-2", trackId: "track-1" },
],
locked: false,
color: "#3b82f6",
syncMode: "none",
collapsed: false,
createdAt: Date.now(),
modifiedAt: Date.now(),
} as ClipGroup
}
return undefined
})
})
afterEach(() => {
vi.clearAllTimers()
})
it("должен инициализироваться без ошибок", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
expect(result.current).toBeDefined()
expect(result.current.groups).toEqual([])
expect(result.current.groupManager).toBeDefined()
})
it("должен иметь все необходимые методы", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
expect(result.current.createGroup).toBeDefined()
expect(result.current.ungroupClips).toBeDefined()
expect(result.current.addToGroup).toBeDefined()
expect(result.current.removeFromGroup).toBeDefined()
expect(result.current.toggleCollapse).toBeDefined()
expect(result.current.lockGroup).toBeDefined()
expect(result.current.renameGroup).toBeDefined()
expect(result.current.setGroupColor).toBeDefined()
expect(result.current.createNestedSequence).toBeDefined()
expect(result.current.updateNestedSequence).toBeDefined()
expect(result.current.breakApartSequence).toBeDefined()
expect(result.current.getGroupByClip).toBeDefined()
expect(result.current.isClipInGroup).toBeDefined()
expect(result.current.getClipsInGroup).toBeDefined()
})
it("должен создавать группу из клипов", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
const clips = [
mockProject.sections[0].tracks[0].clips[0],
mockProject.sections[0].tracks[0].clips[1],
] as TimelineClip[]
act(() => {
const operationResult = result.current.createGroup(clips, "My Group")
expect(operationResult.success).toBe(true)
expect(operationResult.groupId).toBe("group-1")
})
// Проверяем вызов менеджера
expect(mockGroupManager.createGroup).toHaveBeenCalledWith(
[
{ clipId: "clip-1", trackId: "track-1" },
{ clipId: "clip-2", trackId: "track-1" },
],
"My Group",
undefined,
)
// Проверяем отправку события
expect(mockSend).toHaveBeenCalledWith({
type: "CLIPS_GROUPED",
groupId: "group-1",
clipIds: ["clip-1", "clip-2"],
})
})
it("должен создавать группу с опциями", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
const clips = [mockProject.sections[0].tracks[0].clips[0]] as TimelineClip[]
const options = {
autoColor: true,
collapseOnCreate: true,
}
act(() => {
result.current.createGroup(clips, undefined, options)
})
expect(mockGroupManager.createGroup).toHaveBeenCalledWith(
[{ clipId: "clip-1", trackId: "track-1" }],
undefined,
options,
)
})
it("должен разгруппировывать клипы", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
act(() => {
const operationResult = result.current.ungroupClips("group-1")
expect(operationResult.success).toBe(true)
})
expect(mockGroupManager.ungroupClips).toHaveBeenCalledWith("group-1")
expect(mockSend).toHaveBeenCalledWith({
type: "CLIPS_UNGROUPED",
groupId: "group-1",
clipIds: ["clip-1"],
})
})
it("должен добавлять клипы в группу", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
mockGroupManager.addToGroup.mockReturnValue({
success: true,
})
const newClips = [mockProject.globalTracks[0].clips[0]] as TimelineClip[]
act(() => {
const operationResult = result.current.addToGroup("group-1", newClips)
expect(operationResult.success).toBe(true)
})
expect(mockGroupManager.addToGroup).toHaveBeenCalledWith("group-1", [
{ clipId: "clip-3", trackId: "global-track-1" },
])
expect(mockSend).toHaveBeenCalledWith({
type: "CLIPS_ADDED_TO_GROUP",
groupId: "group-1",
clipIds: ["clip-3"],
})
})
it("должен удалять клипы из группы", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
mockGroupManager.removeFromGroup.mockReturnValue({
success: true,
})
const clipsToRemove = [mockProject.sections[0].tracks[0].clips[0]] as TimelineClip[]
act(() => {
const operationResult = result.current.removeFromGroup("group-1", clipsToRemove)
expect(operationResult.success).toBe(true)
})
expect(mockGroupManager.removeFromGroup).toHaveBeenCalledWith("group-1", [{ clipId: "clip-1", trackId: "track-1" }])
expect(mockSend).toHaveBeenCalledWith({
type: "CLIPS_REMOVED_FROM_GROUP",
groupId: "group-1",
clipIds: ["clip-1"],
})
})
it("должен переключать состояние свернутости группы", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
act(() => {
result.current.toggleCollapse("group-1")
})
expect(mockGroupManager.toggleCollapse).toHaveBeenCalledWith("group-1")
expect(mockSend).toHaveBeenCalledWith({
type: "GROUP_TOGGLED",
groupId: "group-1",
})
})
it("должен блокировать и разблокировать группу", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
act(() => {
result.current.lockGroup("group-1", true)
})
expect(mockGroupManager.lockGroup).toHaveBeenCalledWith("group-1", true)
expect(mockSend).toHaveBeenCalledWith({
type: "GROUP_LOCKED",
groupId: "group-1",
locked: true,
})
act(() => {
result.current.lockGroup("group-1", false)
})
expect(mockGroupManager.lockGroup).toHaveBeenCalledWith("group-1", false)
})
it("должен переименовывать группу", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
act(() => {
result.current.renameGroup("group-1", "New Name")
})
expect(mockGroupManager.renameGroup).toHaveBeenCalledWith("group-1", "New Name")
})
it("должен изменять цвет группы", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
act(() => {
result.current.setGroupColor("group-1", "#ef4444")
})
expect(mockGroupManager.setGroupColor).toHaveBeenCalledWith("group-1", "#ef4444")
})
it("должен создавать вложенную последовательность", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
mockGroupManager.createNestedSequence.mockReturnValue({
success: true,
groupId: "sequence-1",
})
const clips = mockProject.sections[0].tracks[0].clips
act(() => {
const operationResult = result.current.createNestedSequence(clips, "My Sequence")
expect(operationResult.success).toBe(true)
expect(operationResult.groupId).toBe("sequence-1")
})
expect(mockGroupManager.createNestedSequence).toHaveBeenCalledWith(
[
{ clipId: "clip-1", trackId: "track-1" },
{ clipId: "clip-2", trackId: "track-1" },
],
"My Sequence",
)
expect(mockSend).toHaveBeenCalledWith({
type: "NESTED_SEQUENCE_CREATED",
sequenceId: "sequence-1",
clipIds: ["clip-1", "clip-2"],
})
})
it("должен обновлять вложенную последовательность", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
const updates = {
scale: 0.5,
position: { x: 100, y: 50 },
opacity: 0.8,
}
act(() => {
result.current.updateNestedSequence("sequence-1", updates)
})
expect(mockGroupManager.updateNestedSequence).toHaveBeenCalledWith("sequence-1", updates)
})
it("должен разбирать вложенную последовательность", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
mockGroupManager.breakApartSequence.mockReturnValue({
success: true,
affectedClips: [
{ clipId: "clip-1", trackId: "track-1" },
{ clipId: "clip-2", trackId: "track-1" },
],
})
act(() => {
const operationResult = result.current.breakApartSequence("sequence-1")
expect(operationResult.success).toBe(true)
})
expect(mockGroupManager.breakApartSequence).toHaveBeenCalledWith("sequence-1")
expect(mockSend).toHaveBeenCalledWith({
type: "NESTED_SEQUENCE_BROKEN",
sequenceId: "sequence-1",
clipIds: ["clip-1", "clip-2"],
})
})
it("должен получать группу по клипу", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
mockGroupManager.getGroupByClip.mockReturnValue({
id: "group-1",
name: "Test Group",
})
const group = result.current.getGroupByClip("clip-1")
expect(mockGroupManager.getGroupByClip).toHaveBeenCalledWith("clip-1")
expect(group).toEqual({
id: "group-1",
name: "Test Group",
})
})
it("должен проверять, находится ли клип в группе", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
mockGroupManager.isClipInGroup.mockReturnValue(true)
const isInGroup = result.current.isClipInGroup("clip-1")
expect(mockGroupManager.isClipInGroup).toHaveBeenCalledWith("clip-1")
expect(isInGroup).toBe(true)
})
it("должен получать клипы в группе", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
const clips = result.current.getClipsInGroup("group-1")
expect(mockGroupManager.getGroup).toHaveBeenCalledWith("group-1")
expect(clips).toHaveLength(2)
expect(clips[0].id).toBe("clip-1")
expect(clips[1].id).toBe("clip-2")
})
it("должен возвращать пустой массив для несуществующей группы", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
mockGroupManager.getGroup.mockReturnValue(undefined)
const clips = result.current.getClipsInGroup("non-existent-group")
expect(clips).toEqual([])
})
it("должен обрабатывать ошибки при создании группы", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
mockGroupManager.createGroup.mockReturnValue({
success: false,
error: "Клипы уже в группе",
})
const clips = [mockProject.sections[0].tracks[0].clips[0]] as TimelineClip[]
act(() => {
const operationResult = result.current.createGroup(clips)
expect(operationResult.success).toBe(false)
expect(operationResult.error).toBe("Клипы уже в группе")
})
// При ошибке событие не должно отправляться
expect(mockSend).not.toHaveBeenCalled()
})
it("должен подписываться на события менеджера групп", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result, unmount } = renderHook(() => useClipGroups(), { wrapper })
// Проверяем подписку
expect(mockGroupManager.addEventListener).toHaveBeenCalled()
// Размонтируем компонент
unmount()
// Проверяем отписку
expect(mockGroupManager.removeEventListener).toHaveBeenCalled()
})
it("должен обновлять состояние групп при изменениях", async () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
// Симулируем изменение групп
const newGroups = [
{
id: "group-1",
name: "Updated Group",
clips: [],
},
]
mockGroupManager.getGroupHierarchy.mockReturnValue(newGroups)
// Вызываем callback, переданный в addEventListener
const updateCallback = mockGroupManager.addEventListener.mock.calls[0][0]
act(() => {
updateCallback()
})
// Ждем обновления состояния
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0))
})
expect(result.current.groups).toEqual(newGroups)
})
it("должен находить клипы из глобальных треков", () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
{children}
)
const { result } = renderHook(() => useClipGroups(), { wrapper })
mockGroupManager.getGroup.mockImplementation((groupId: string) => {
if (groupId === "group-2") {
return {
id: "group-2",
name: "Global Group",
clips: [{ clipId: "clip-3", trackId: "global-track-1" }],
locked: false,
color: "#22c55e",
syncMode: "none",
collapsed: false,
createdAt: Date.now(),
modifiedAt: Date.now(),
} as ClipGroup
}
return undefined
})
const clips = result.current.getClipsInGroup("group-2")
expect(clips).toHaveLength(1)
expect(clips[0].id).toBe("clip-3")
expect(clips[0].trackId).toBe("global-track-1")
})
})