import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { useProjectSettings } from "@/features/project-settings/hooks/use-project-settings" import { TimelineContent } from "../../components/timeline-content" // Mock все внешние зависимости vi.mock("@/components/ui/badge", () => ({ Badge: ({ children, variant }: any) => {children}, })) vi.mock("@/components/ui/button", () => ({ Button: ({ children, onClick, ...props }: any) => ( ), })) vi.mock("@/components/ui/card", () => ({ Card: ({ children, className }: any) =>
{children}
, CardContent: ({ children }: any) =>
{children}
, CardHeader: ({ children }: any) =>
{children}
, CardTitle: ({ children }: any) =>

{children}

, })) vi.mock("@/components/ui/resizable", () => ({ ResizableHandle: () =>
, ResizablePanel: ({ children, defaultSize }: any) =>
{children}
, ResizablePanelGroup: ({ children }: any) =>
{children}
, })) // Mock hooks const mockCurrentProject = { id: "test-project", name: "Test Project", } const mockProjectSettings = { aspectRatio: { value: { width: 1920, height: 1080 }, }, frameRate: "30", } const mockTimelineState = { project: null as any, uiState: { timeScale: 10, selectedTrackIds: [], }, currentTime: 0, createProject: vi.fn(), addSection: vi.fn(), addTrack: vi.fn(), updateTrack: vi.fn(), selectTracks: vi.fn(), seek: vi.fn(), error: null as string | null, clearError: vi.fn(), send: vi.fn(), } const mockTracks = { tracks: [] as any[], setTrackHeight: vi.fn(), } const mockClips = { clips: [] as any[], } const mockDragState = { dragState: { isDragging: false }, } vi.mock("@/features/app-state/hooks/use-current-project", () => ({ useCurrentProject: () => ({ currentProject: mockCurrentProject }), })) vi.mock("@/features/project-settings/hooks/use-project-settings", () => ({ useProjectSettings: vi.fn(() => ({ settings: mockProjectSettings })), })) vi.mock("../../hooks/use-timeline", () => ({ useTimeline: () => mockTimelineState, })) vi.mock("../../hooks/use-tracks", () => ({ useTracks: () => mockTracks, })) vi.mock("../../hooks/use-clips", () => ({ useClips: () => mockClips, })) vi.mock("../../hooks/use-drag-drop-timeline", () => ({ useDragDropTimeline: () => mockDragState, })) // Мокаем AI интеграцию vi.mock("@/features/ai-chat/hooks/use-timeline-ai-integration", () => ({ useTimelineAIIntegration: () => ({ isReady: true, hasProject: false, clipsCount: 0, tracksCount: 0, projectDuration: 0, }), })) vi.mock("../../hooks/use-timeline-player-sync", () => ({ useTimelinePlayerSync: vi.fn(), })) vi.mock("../../hooks/use-edit-mode", () => ({ EditModeProvider: ({ children }: any) =>
{children}
, })) // Mock child components vi.mock("../../components/timeline-hotkeys", () => ({ TimelineHotkeys: () =>
, })) vi.mock("../../components/timeline-speed-ramping-integration", () => ({ TimelineSpeedRampingIntegration: () =>
, SpeedRampingIndicator: () =>
, TimelineSpeedRampingStatus: () =>
, })) vi.mock("../../components/edit-mode-selector", () => ({ EditModeSelector: () =>
, })) vi.mock("../../components/edit-tools/edit-mode-overlay", () => ({ EditModeOverlay: () =>
, })) vi.mock("../../components/ai-markers/ai-marker-controls", () => ({ AIMarkerControls: () =>
, })) vi.mock("../../components/track-controls-panel", () => ({ TrackControlsPanel: () =>
, })) vi.mock("../../components/drag-drop-provider", () => ({ DragDropProvider: ({ children }: any) =>
{children}
, })) vi.mock("../../components/markers", () => ({ TimelineMarkersLayer: () =>
, })) vi.mock("../../components/ai-analysis/timeline-ai-overlay", () => ({ TimelineAIOverlay: () =>
, })) vi.mock("../../components/edit-tools/split-indicator", () => ({ SplitIndicator: () =>
, })) vi.mock("../../components/track-insertion-zone", () => ({ TrackInsertionZones: () =>
, })) vi.mock("../../components/timeline-preview-strip", () => ({ TimelinePreviewStrip: () =>
, })) vi.mock("../../components/timeline-scale", () => ({ TimelineScale: () =>
, })) vi.mock("../../components/track/track", () => ({ Track: ({ track }: any) =>
{track.name}
, })) describe("TimelineContent", () => { beforeEach(() => { vi.clearAllMocks() // Reset mock states mockTimelineState.project = null mockTimelineState.error = null mockTracks.tracks = [] mockClips.clips = [] // Mock ResizeObserver global.ResizeObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), })) }) afterEach(() => { vi.restoreAllMocks() }) describe("Loading and error states", () => { it("should show loading state when project is not initialized", () => { render() expect(screen.getByText("Загрузка Timeline...")).toBeInTheDocument() expect(screen.getByText("Инициализация проекта...")).toBeInTheDocument() }) it("should show error state when there is an error", () => { mockTimelineState.error = "Failed to load timeline" render() expect(screen.getByText("Ошибка Timeline")).toBeInTheDocument() expect(screen.getByText("Failed to load timeline")).toBeInTheDocument() expect(screen.getByText("Закрыть")).toBeInTheDocument() }) it("should clear error when clicking close button", () => { mockTimelineState.error = "Some error" render() const closeButton = screen.getByText("Закрыть") fireEvent.click(closeButton) expect(mockTimelineState.clearError).toHaveBeenCalled() }) }) describe("Project initialization", () => { it("should create project on mount when no project exists", async () => { render() await waitFor(() => { expect(mockTimelineState.createProject).toHaveBeenCalledWith("Test Project", { width: 1920, height: 1080, frameRate: 30, }) }) }) it("should add initial section when project is created", async () => { mockTimelineState.project = { id: "test", name: "Test", sections: [], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } render() await waitFor(() => { expect(mockTimelineState.addSection).toHaveBeenCalledWith("Main Section", 0, 300) }) }) it("should not add section if sections already exist", () => { mockTimelineState.project = { id: "test", name: "Test", sections: [{ id: "section-1", name: "Existing" }], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } render() expect(mockTimelineState.addSection).not.toHaveBeenCalled() }) }) describe("Main content rendering", () => { beforeEach(() => { mockTimelineState.project = { id: "test", name: "Test", sections: [{ id: "section-1", name: "Main" }], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } }) it("should render all main components", () => { render() expect(screen.getByTestId("timeline-hotkeys")).toBeInTheDocument() expect(screen.getByTestId("timeline-speed-ramping-integration")).toBeInTheDocument() expect(screen.getByTestId("speed-ramping-indicator")).toBeInTheDocument() expect(screen.getByTestId("edit-mode-overlay")).toBeInTheDocument() expect(screen.getByTestId("edit-mode-selector")).toBeInTheDocument() expect(screen.getByTestId("ai-marker-controls")).toBeInTheDocument() }) it("should display project information", () => { render() expect(screen.getByText("Test Project")).toBeInTheDocument() expect(screen.getByText("1920x1080 @ 30fps")).toBeInTheDocument() }) it("should display statistics badges", () => { mockTracks.tracks = [{ id: "t1" }, { id: "t2" }] mockClips.clips = [{ id: "c1" }, { id: "c2" }, { id: "c3" }] render() expect(screen.getByText("1 секций")).toBeInTheDocument() expect(screen.getByText("2 треков")).toBeInTheDocument() expect(screen.getByText("3 клипов")).toBeInTheDocument() }) }) describe("Track management", () => { beforeEach(() => { mockTimelineState.project = { id: "test", name: "Test", sections: [{ id: "section-1", name: "Main" }], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } }) it("should show empty state when no tracks", () => { mockTracks.tracks = [] render() expect(screen.getByText("Треки не найдены")).toBeInTheDocument() expect(screen.getByText("Добавить видео трек")).toBeInTheDocument() }) it("should add track when clicking add button", () => { mockTracks.tracks = [] render() const addButton = screen.getByText("Добавить видео трек") fireEvent.click(addButton) expect(mockTimelineState.addTrack).toHaveBeenCalledWith("video", "Видео трек") }) it("should render tracks when they exist", () => { mockTracks.tracks = [ { id: "track-1", name: "Video Track 1", type: "video", clips: [] }, { id: "track-2", name: "Audio Track 1", type: "audio", clips: [] }, ] render() expect(screen.getByTestId("track-track-1")).toBeInTheDocument() expect(screen.getByTestId("track-track-2")).toBeInTheDocument() expect(screen.getByText("Video Track 1")).toBeInTheDocument() expect(screen.getByText("Audio Track 1")).toBeInTheDocument() }) }) describe("Timeline components", () => { beforeEach(() => { mockTimelineState.project = { id: "test", name: "Test", sections: [{ id: "section-1", name: "Main" }], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } mockTracks.tracks = [{ id: "track-1", name: "Track 1", type: "video", clips: [] }] }) it("should render timeline scale", () => { render() expect(screen.getByTestId("timeline-scale")).toBeInTheDocument() }) it("should render markers layer", () => { render() expect(screen.getByTestId("timeline-markers-layer")).toBeInTheDocument() }) it("should render AI overlay", () => { render() expect(screen.getByTestId("timeline-ai-overlay")).toBeInTheDocument() }) it("should render split indicator", () => { render() expect(screen.getByTestId("split-indicator")).toBeInTheDocument() }) it("should render track insertion zones when dragging", () => { mockDragState.dragState.isDragging = true render() expect(screen.getByTestId("track-insertion-zones")).toBeInTheDocument() }) }) describe("Preview strip", () => { beforeEach(() => { mockTimelineState.project = { id: "test", name: "Test", sections: [{ id: "section-1", name: "Main" }], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } }) it("should not render preview strip when no video clips", () => { mockTracks.tracks = [{ id: "track-1", name: "Audio Track", type: "audio", clips: [] }] mockClips.clips = [] render() expect(screen.queryByTestId("timeline-preview-strip")).not.toBeInTheDocument() }) it("should render preview strip for video clips with media", () => { mockTracks.tracks = [{ id: "track-1", name: "Video Track", type: "video", clips: ["clip-1"] }] mockClips.clips = [ { id: "clip-1", trackId: "track-1", mediaFile: { path: "/path/to/video.mp4" }, startTime: 0, duration: 10, }, ] render() expect(screen.getByTestId("timeline-preview-strip")).toBeInTheDocument() }) }) describe("Scroll and resize handling", () => { beforeEach(() => { mockTimelineState.project = { id: "test", name: "Test", sections: [{ id: "section-1", name: "Main" }], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } mockTracks.tracks = [{ id: "track-1", name: "Track 1", type: "video", clips: [] }] }) it("should handle scroll events", () => { const { container } = render() const scrollContainer = container.querySelector(".overflow-auto") expect(scrollContainer).toBeTruthy() // Simulate scroll fireEvent.scroll(scrollContainer!, { target: { scrollLeft: 100 } }) }) it("should setup resize observer", () => { const mockObserve = vi.fn() const mockDisconnect = vi.fn() global.ResizeObserver = vi.fn().mockImplementation(() => ({ observe: mockObserve, disconnect: mockDisconnect, })) const { unmount } = render() expect(mockObserve).toHaveBeenCalled() unmount() expect(mockDisconnect).toHaveBeenCalled() }) }) describe("Integration with timeline state", () => { beforeEach(() => { mockTimelineState.project = { id: "test", name: "Test", sections: [{ id: "section-1", name: "Main" }], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } mockTracks.tracks = [ { id: "track-1", name: "Track 1", type: "video", clips: [], isSelected: false, }, ] }) it("should pass correct props to Track component", () => { mockTimelineState.uiState.selectedTrackIds = ["track-1"] mockTimelineState.currentTime = 5 const { container } = render() const track = container.querySelector('[data-testid="track-track-1"]') expect(track).toBeTruthy() }) it("should handle track selection", () => { render() // Since Track is mocked, we can't test the actual selection, // but we can verify the component receives the correct props expect(mockTimelineState.selectTracks).toBeDefined() }) }) describe("Timeline header rendering", () => { beforeEach(() => { mockTimelineState.project = { id: "test", name: "Test", sections: [{ id: "section-1", name: "Main" }], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } mockTracks.tracks = [{ id: "track-1", name: "Track 1", type: "video", clips: [] }] }) it("should render timeline header with label", () => { render() expect(screen.getByText("Временная шкала")).toBeInTheDocument() }) it("should render resizable panels", () => { const { container } = render() const panels = container.querySelectorAll("[data-default-size]") expect(panels).toHaveLength(2) expect(panels[0]).toHaveAttribute("data-default-size", "25") expect(panels[1]).toHaveAttribute("data-default-size", "75") }) it("should render resizable handle", () => { render() expect(screen.getByTestId("resizable-handle")).toBeInTheDocument() }) }) describe("Split functionality", () => { beforeEach(() => { mockTimelineState.project = { id: "test", name: "Test", sections: [{ id: "section-1", name: "Main" }], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } mockTracks.tracks = [ { id: "track-1", name: "Track 1", type: "video", clips: [{ id: "clip-1", startTime: 0, duration: 10 }], }, ] }) it("should setup split indicator with correct props", () => { const { container } = render() expect(screen.getByTestId("split-indicator")).toBeInTheDocument() // Verify scroll container is passed const scrollContainer = container.querySelector(".overflow-auto") expect(scrollContainer).toBeTruthy() }) }) describe("Window event listeners", () => { beforeEach(() => { mockTimelineState.project = { id: "test", name: "Test", sections: [{ id: "section-1", name: "Main" }], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } mockTracks.tracks = [{ id: "track-1", name: "Track 1", type: "video", clips: [] }] }) it("should setup window resize listener", () => { const addEventListenerSpy = vi.spyOn(window, "addEventListener") render() expect(addEventListenerSpy).toHaveBeenCalledWith("resize", expect.any(Function)) }) it("should cleanup listeners on unmount", () => { const removeEventListenerSpy = vi.spyOn(window, "removeEventListener") const { unmount } = render() unmount() expect(removeEventListenerSpy).toHaveBeenCalledWith("resize", expect.any(Function)) }) }) describe("Track updates", () => { beforeEach(() => { mockTimelineState.project = { id: "test", name: "Test", sections: [{ id: "section-1", name: "Main" }], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } mockTracks.tracks = [ { id: "track-1", name: "Track 1", type: "video", clips: [], }, ] }) it("should pass update handler to tracks", () => { render() // Verify updateTrack function is available expect(mockTimelineState.updateTrack).toBeDefined() }) }) describe("Project settings fallback", () => { it("should use project settings when projectSettings not available", () => { mockTimelineState.project = { id: "test", name: "Fallback Project", sections: [{ id: "section-1", name: "Main" }], settings: { resolution: { width: 1280, height: 720 }, fps: 25 }, duration: 300, } // Temporarily set projectSettings to null vi.mocked(useProjectSettings).mockReturnValueOnce({ settings: null } as any) render() expect(screen.getByText("1280x720 @ 25fps")).toBeInTheDocument() }) }) describe("Drag state integration", () => { beforeEach(() => { mockTimelineState.project = { id: "test", name: "Test", sections: [{ id: "section-1", name: "Main" }], settings: { resolution: { width: 1920, height: 1080 }, fps: 30 }, duration: 300, } mockTracks.tracks = [{ id: "track-1", name: "Track 1", type: "video", clips: [] }] }) it("should not show track insertion zones when not dragging", () => { mockDragState.dragState.isDragging = false render() // TrackInsertionZones should still be rendered but with isVisible=false expect(screen.getByTestId("track-insertion-zones")).toBeInTheDocument() }) it("should show track insertion zones when dragging", () => { mockDragState.dragState.isDragging = true render() expect(screen.getByTestId("track-insertion-zones")).toBeInTheDocument() }) }) })