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()
})
})
})