/**
* Tests for VideoClip component
*/
import { fireEvent, render, screen } from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"
// Мокаем backend-sync ДО импорта компонентов
vi.mock("@/features/app-state/services/backend-sync", () => ({
getBackendSync: () => ({
onStateChange: vi.fn(() => () => {}),
sendCommand: vi.fn().mockResolvedValue(undefined),
executeCommand: vi.fn().mockResolvedValue({ success: true }),
onEvent: vi.fn(() => () => {}),
}),
}))
// Мокаем useUserSettings
vi.mock("@/features/user-settings", () => ({
useUserSettings: () => ({
playerVolume: 100,
handlePlayerVolumeChange: vi.fn(),
}),
}))
// Мокаем timeline-player-sync
vi.mock("../../../services/timeline-player-sync", () => ({
timelinePlayerSync: {
syncSelectedClip: vi.fn().mockResolvedValue(undefined),
},
}))
// Мокаем useTimeline
const mockTimelineActor = {
send: vi.fn(),
}
const mockUiState = {
context: {
selectedClipIds: [],
currentTime: 0,
},
}
const mockTimeline = {
timelineActor: mockTimelineActor,
uiState: mockUiState,
selectClips: vi.fn(),
copySelection: vi.fn(),
splitClip: vi.fn(),
}
vi.mock("../../../hooks/use-timeline", () => ({
useTimeline: () => mockTimeline,
}))
import { VideoClip } from "../../../components/clip/video-clip"
import { TimelineClip, TimelineTrack, TrackType } from "../../../types"
// Mock data
const mockVideoClip: TimelineClip = {
id: "clip-1",
trackId: "track-1",
mediaId: "media-1",
name: "Test Video Clip",
startTime: 0,
duration: 10,
trimStart: 0,
trimEnd: 10,
mediaStartTime: 0,
mediaEndTime: 10,
volume: 1,
speed: 1,
opacity: 1,
isReversed: false,
isSelected: false,
isLocked: false,
effects: [],
filters: [],
transitions: [],
position: {
x: 0,
y: 0,
width: 1,
height: 1,
rotation: 0,
scaleX: 1,
scaleY: 1,
},
}
const mockVideoTrack: TimelineTrack = {
id: "track-1",
name: "Video Track",
type: "video" as TrackType,
order: 0,
enabled: true,
locked: false,
height: 80,
clips: [],
}
const mockImageTrack: TimelineTrack = {
...mockVideoTrack,
type: "image" as TrackType,
name: "Image Track",
}
describe("VideoClip", () => {
const mockOnUpdate = vi.fn()
const mockOnRemove = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
// Сбрасываем моки timeline
mockTimeline.selectClips.mockClear()
mockTimeline.copySelection.mockClear()
mockTimeline.splitClip.mockClear()
mockTimelineActor.send.mockClear()
})
describe("Rendering", () => {
it("should render video clip with correct name and icon", () => {
render()
expect(screen.getByText("Test Video Clip")).toBeInTheDocument()
expect(screen.getByText("10s")).toBeInTheDocument()
})
it("should render image clip with image icon", () => {
render()
expect(screen.getByText("Test Video Clip")).toBeInTheDocument()
})
it("should apply correct colors for video track", () => {
const { container } = render(
,
)
const clipElement = container.firstChild
expect(clipElement).toHaveClass("bg-blue-500")
})
it("should apply correct colors for image track", () => {
const { container } = render(
,
)
const clipElement = container.firstChild
expect(clipElement).toHaveClass("bg-purple-500")
})
})
describe("Selection", () => {
it("should call onUpdate when clicked", () => {
render()
const clipElement = screen.getByText("Test Video Clip").closest("div")
fireEvent.click(clipElement!)
expect(mockOnUpdate).toHaveBeenCalledWith({ isSelected: true })
})
it("should toggle selection state", () => {
const selectedClip = { ...mockVideoClip, isSelected: true }
render()
const clipElement = screen.getByText("Test Video Clip").closest("div")
fireEvent.click(clipElement!)
expect(mockOnUpdate).toHaveBeenCalledWith({ isSelected: false })
})
it("should show selection ring when selected", () => {
const selectedClip = { ...mockVideoClip, isSelected: true }
const { container } = render(
,
)
const clipElement = container.firstChild
expect(clipElement).toHaveClass("ring-2")
})
})
describe("Hover Effects", () => {
it("should show action buttons on hover", () => {
render()
const clipElement = screen.getByText("Test Video Clip").closest("div")
fireEvent.mouseEnter(clipElement!)
expect(screen.getByTitle("Копировать")).toBeInTheDocument()
expect(screen.getByTitle("Разделить")).toBeInTheDocument()
expect(screen.getByTitle("Удалить")).toBeInTheDocument()
})
it("should hide action buttons on mouse leave", () => {
render()
const clipElement = screen.getByText("Test Video Clip").closest("div")
fireEvent.mouseEnter(clipElement!)
fireEvent.mouseLeave(clipElement!)
expect(screen.queryByTitle("Копировать")).not.toBeInTheDocument()
expect(screen.queryByTitle("Разделить")).not.toBeInTheDocument()
expect(screen.queryByTitle("Удалить")).not.toBeInTheDocument()
})
it("should not show buttons when clip is locked", () => {
const lockedClip = { ...mockVideoClip, isLocked: true }
render()
const clipElement = screen.getByText("Test Video Clip").closest("div")
fireEvent.mouseEnter(clipElement!)
expect(screen.queryByTitle("Копировать")).not.toBeInTheDocument()
expect(screen.queryByTitle("Разделить")).not.toBeInTheDocument()
expect(screen.queryByTitle("Удалить")).not.toBeInTheDocument()
})
it("should show resize handles on hover", () => {
const { container } = render(
,
)
const clipElement = screen.getByText("Test Video Clip").closest("div")
fireEvent.mouseEnter(clipElement!)
// В новой реализации ручки отображаются как дивы с классами cursor-*-resize
const leftHandle = container.querySelector(".cursor-w-resize")
const rightHandle = container.querySelector(".cursor-e-resize")
expect(leftHandle).toBeInTheDocument()
expect(rightHandle).toBeInTheDocument()
})
})
describe("Action Buttons", () => {
it("should handle copy button click", () => {
render()
const clipElement = screen.getByText("Test Video Clip").closest("div")
fireEvent.mouseEnter(clipElement!)
const copyButton = screen.getByTitle("Копировать")
fireEvent.click(copyButton)
// Проверяем что были вызваны правильные методы
expect(mockTimeline.selectClips).toHaveBeenCalledWith(["clip-1"], false)
expect(mockTimeline.copySelection).toHaveBeenCalled()
})
it("should handle split button click", () => {
render()
const clipElement = screen.getByText("Test Video Clip").closest("div")
fireEvent.mouseEnter(clipElement!)
const splitButton = screen.getByTitle("Разделить")
fireEvent.click(splitButton)
// Проверяем что был вызван метод разделения
expect(mockTimeline.splitClip).toHaveBeenCalledWith("clip-1", 5) // clip.startTime + clip.duration / 2 = 0 + 10 / 2 = 5
})
it("should handle remove button click", () => {
render()
const clipElement = screen.getByText("Test Video Clip").closest("div")
fireEvent.mouseEnter(clipElement!)
const removeButton = screen.getByTitle("Удалить")
fireEvent.click(removeButton)
expect(mockOnRemove).toHaveBeenCalled()
})
it("should stop propagation on button clicks", () => {
render()
const clipElement = screen.getByText("Test Video Clip").closest("div")
fireEvent.mouseEnter(clipElement!)
const removeButton = screen.getByTitle("Удалить")
fireEvent.click(removeButton)
// Should not trigger clip selection
expect(mockOnUpdate).not.toHaveBeenCalled()
})
})
describe("Visual Indicators", () => {
it("should show effects indicator", () => {
const clipWithEffects = {
...mockVideoClip,
effects: [{ id: "effect-1", name: "Blur", params: {} }],
}
render(
,
)
expect(screen.getByTitle("Эффекты применены")).toBeInTheDocument()
})
it("should show filters indicator", () => {
const clipWithFilters = {
...mockVideoClip,
filters: [{ id: "filter-1", name: "Vintage", value: "vintage" }],
}
render(
,
)
expect(screen.getByTitle("Фильтры применены")).toBeInTheDocument()
})
it("should show transitions indicator", () => {
const clipWithTransitions = {
...mockVideoClip,
transitions: [{ id: "transition-1", name: "Fade", duration: 1000 }],
}
render(
,
)
expect(screen.getByTitle("Переходы применены")).toBeInTheDocument()
})
it("should show opacity when locked", () => {
const lockedClip = { ...mockVideoClip, isLocked: true }
const { container } = render(
,
)
const clipElement = container.firstChild
expect(clipElement).toHaveClass("opacity-60")
})
})
describe("Progress Bar", () => {
it("should calculate trim progress correctly", () => {
const trimmedClip = {
...mockVideoClip,
trimStart: 2,
trimEnd: 8,
mediaStartTime: 2,
mediaEndTime: 8,
}
const { container } = render(
,
)
// В новой реализации прогресс бар - это просто div с белым фоном
const progressBars = container.getElementsByClassName("bg-white/50")
expect(progressBars).toHaveLength(1)
const progressBar = progressBars[0]
// Проверяем вычисления
// width = duration / (mediaEndTime - mediaStartTime + duration) * 100
// width = 10 / (8 - 2 + 10) * 100 = 10 / 16 * 100 = 62.5%
expect(progressBar).toHaveStyle({ width: "62.5%" })
})
})
describe("Edge Cases", () => {
it("should handle clip without name", () => {
const clipWithoutName = { ...mockVideoClip, name: "" }
const { container } = render(
,
)
// Клип должен отображаться, даже без имени
expect(screen.getByText("10s")).toBeInTheDocument()
// Проверяем, что есть элемент с классом truncate, но он может быть пустым
const nameElements = container.querySelectorAll(".truncate")
expect(nameElements.length).toBeGreaterThan(0)
// Проверяем, что хотя бы один элемент пустой
const emptyNameElement = Array.from(nameElements).find((el) => el.textContent === "")
expect(emptyNameElement).toBeTruthy()
})
it("should handle zero duration", () => {
const zeroDurationClip = { ...mockVideoClip, duration: 0 }
render(
,
)
expect(screen.getByText("0s")).toBeInTheDocument()
})
it("should handle missing callbacks", () => {
render()
const clipElement = screen.getByText("Test Video Clip").closest("div")
fireEvent.click(clipElement!)
// Should not throw error
expect(true).toBe(true)
})
})
})