import { fireEvent, render, screen } from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { MediaFile } from "@/features/media/types/media"
import { TimelineProvider } from "@/features/timeline/services/timeline-provider"
import { PlayerControls } from "../../components/player-controls"
// Вспомогательная функция для рендеринга с провайдерами
const renderWithProviders = (ui: React.ReactElement) => {
return render({ui})
}
// Импортируем централизованный мок иконок
import "@/test/mocks/libraries/lucide-react"
// Мокаем компоненты UI
vi.mock("@/components/ui/button", () => ({
Button: ({ children, onClick, variant, size, className, ...props }: any) => (
),
}))
vi.mock("@/components/ui/slider", () => ({
Slider: ({ value, onValueChange, max, step, className, disabled }: any) => (
onValueChange?.([Number.parseFloat(e.target.value)])}
max={max}
step={step}
className={className}
disabled={disabled}
/>
),
}))
// Мокаем дочерние компоненты
vi.mock("../../components/prerender-controls", () => ({
PrerenderControls: () =>
Prerender Controls
,
}))
vi.mock("../../components/volume-slider", () => ({
VolumeSlider: ({ volume, onValueChange, onValueCommit }: any) => (
{
if (onValueChange) onValueChange([0.5])
if (onValueCommit) onValueCommit([0.5])
}}
>
Volume Slider
),
}))
// Мокаем TimelineProvider
vi.mock("@/features/timeline/services/timeline-provider", () => ({
TimelineProvider: ({ children }: any) => children,
useTimeline: () => ({
isPlaying: false,
timeline: {
tracks: [],
duration: 120,
fps: 30,
},
clips: [],
}),
}))
// Мокаем useLinkedClips
vi.mock("@/features/timeline/hooks/use-linked-clips", () => ({
useLinkedClips: () => ({
linkedClips: [],
isLinked: () => false,
linkClips: vi.fn(),
unlinkClips: vi.fn(),
getLinkedClips: () => [],
}),
}))
// Мокаем useMulticam
vi.mock("@/features/multicam/hooks/use-multicam", () => ({
useMulticam: () => ({
isMulticam: false,
angles: [],
currentAngle: 0,
switchAngle: vi.fn(),
isChangingCamera: false,
}),
}))
// Мокаем хуки
const mockPlayerContext = {
isPlaying: false,
play: vi.fn().mockResolvedValue(undefined),
pause: vi.fn().mockResolvedValue(undefined),
seek: vi.fn().mockResolvedValue(undefined),
volume: 0.75,
setVolume: vi.fn(),
isRecording: false,
setIsRecording: vi.fn(),
setIsSeeking: vi.fn(),
isChangingCamera: false,
isResizableMode: false,
setIsResizableMode: vi.fn(),
videoSource: "timeline" as const,
setVideoSource: vi.fn(),
}
vi.mock("../../services/player-provider", () => ({
usePlayer: () => mockPlayerContext,
}))
const mockFullscreen = {
isFullscreen: false,
toggleFullscreen: vi.fn(),
}
vi.mock("../../hooks/use-fullscreen", () => ({
useFullscreen: () => mockFullscreen,
}))
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
vi.mock("@/features/media/utils/video", () => ({
getFrameTime: (_file: MediaFile) => 1 / 30, // 30 fps
}))
describe("PlayerControls", () => {
const mockFile: MediaFile = {
id: "test-video",
path: "/path/to/video.mp4",
name: "Test Video.mp4",
size: 1024000,
isVideo: true,
duration: 120, // 2 минуты в секундах
probeData: {
format: {},
streams: [{ r_frame_rate: "30/1" }],
} as any,
}
beforeEach(() => {
vi.clearAllMocks()
mockPlayerContext.isPlaying = false
mockPlayerContext.volume = 0.75
mockPlayerContext.isRecording = false
mockPlayerContext.isResizableMode = false
mockPlayerContext.videoSource = "timeline"
mockFullscreen.isFullscreen = false
})
describe("Отображение элементов управления", () => {
it("должен отображать все основные элементы управления", () => {
renderWithProviders()
expect(screen.getByTestId("stepback-icon")).toBeInTheDocument()
// Ищем основную play иконку через кнопку
expect(screen.getByTitle("timeline.controls.play")).toBeInTheDocument()
expect(screen.getByTestId("stepforward-icon")).toBeInTheDocument()
expect(screen.getByTestId("slider")).toBeInTheDocument()
expect(screen.getByTestId("volume-slider")).toBeInTheDocument()
expect(screen.getByTestId("maximize2-icon")).toBeInTheDocument()
})
it("должен отображать иконку паузы при воспроизведении", () => {
mockPlayerContext.isPlaying = true
renderWithProviders()
expect(screen.getByTestId("pause-icon")).toBeInTheDocument()
// Проверяем что основная play кнопка не отображается (но маленькая play иконка в AI Analysis может остаться)
expect(screen.queryByTitle("timeline.controls.play")).not.toBeInTheDocument()
})
it("должен отображать иконку записи при isRecording", () => {
mockPlayerContext.isRecording = true
renderWithProviders()
expect(screen.getByTestId("circledot-icon")).toBeInTheDocument()
})
it("должен отображать иконку minimize в полноэкранном режиме", () => {
mockFullscreen.isFullscreen = true
renderWithProviders()
expect(screen.getByTestId("minimize2-icon")).toBeInTheDocument()
expect(screen.queryByTestId("maximize2-icon")).not.toBeInTheDocument()
})
})
describe("Управление воспроизведением", () => {
it("должен переключать воспроизведение при клике на play/pause", () => {
renderWithProviders()
const playButton = screen.getByTitle("timeline.controls.play")
fireEvent.click(playButton)
expect(mockPlayerContext.play).toHaveBeenCalled()
})
it("должен переключать паузу при воспроизведении", () => {
mockPlayerContext.isPlaying = true
renderWithProviders()
const pauseButton = screen.getByTitle("timeline.controls.pause")
fireEvent.click(pauseButton)
expect(mockPlayerContext.pause).toHaveBeenCalled()
})
it("должен переключать запись", () => {
renderWithProviders()
const recordButton = screen.getByTitle("timeline.controls.record")
expect(recordButton).not.toBeDisabled() // Проверяем что кнопка активна
fireEvent.click(recordButton)
expect(mockPlayerContext.setIsRecording).toHaveBeenCalledWith(true)
})
it("должен останавливать запись", () => {
mockPlayerContext.isRecording = true
renderWithProviders()
const stopRecordButton = screen.getByTitle("timeline.controls.stopRecord")
fireEvent.click(stopRecordButton)
expect(mockPlayerContext.setIsRecording).toHaveBeenCalledWith(false)
})
})
describe("Навигация по времени", () => {
it("должен обновлять время при изменении слайдера", () => {
renderWithProviders()
const slider = screen.getByTestId("slider")
fireEvent.change(slider, { target: { value: "60" } })
expect(mockPlayerContext.seek).toHaveBeenCalledWith(60)
})
it("должен устанавливать isSeeking при начале перемещения", () => {
renderWithProviders()
const slider = screen.getByTestId("slider")
// Мокаем onValueChange для триггера isSeeking
fireEvent.change(slider, { target: { value: "30" } })
expect(mockPlayerContext.setIsSeeking).toHaveBeenCalledWith(true)
expect(mockPlayerContext.seek).toHaveBeenCalledWith(30)
})
it("должен правильно отображать время файла", () => {
const fileWithTime = {
...mockFile,
startTime: "00:01:05:15",
// Число, а не строка
duration: 120,
}
renderWithProviders()
// Компонент отображает startTime и duration из файла
expect(screen.getByText("00:01:05:15")).toBeInTheDocument()
expect(screen.getByText("120")).toBeInTheDocument()
})
it("должен отображать значения по умолчанию если время не задано", () => {
const fileWithoutTime = {
...mockFile,
startTime: undefined,
duration: undefined,
}
renderWithProviders()
// Показывает значения по умолчанию - должно быть два элемента с текстом 00:00:00:00
const defaultTimeElements = screen.getAllByText("00:00:00:00")
expect(defaultTimeElements).toHaveLength(2) // startTime и duration
})
})
describe("Управление громкостью", () => {
it("должен отображать текущую громкость", () => {
renderWithProviders()
const volumeSlider = screen.getByTestId("volume-slider")
expect(volumeSlider).toHaveAttribute("data-volume", "0.75")
})
it("должен обновлять громкость при клике на слайдер", () => {
renderWithProviders()
const volumeSlider = screen.getByTestId("volume-slider")
fireEvent.click(volumeSlider)
expect(mockPlayerContext.setVolume).toHaveBeenCalledWith(0.5)
})
})
describe("Полноэкранный режим", () => {
it("должен вызывать toggleFullscreen при клике", () => {
// Создаем mock элемент
const mockContainer = document.createElement("div")
mockContainer.className = "media-player-container"
document.body.appendChild(mockContainer)
renderWithProviders()
const fullscreenButton = screen.getByTitle("timeline.controls.fullscreen")
fireEvent.click(fullscreenButton)
expect(mockFullscreen.toggleFullscreen).toHaveBeenCalledWith(mockContainer)
// Очищаем
document.body.removeChild(mockContainer)
})
it("должен показывать ошибку если контейнер не найден", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
renderWithProviders()
const fullscreenButton = screen.getByTitle("timeline.controls.fullscreen")
fireEvent.click(fullscreenButton)
expect(consoleSpy).toHaveBeenCalledWith("[handleFullscreen] Не найден контейнер медиаплеера")
expect(mockFullscreen.toggleFullscreen).not.toHaveBeenCalled()
consoleSpy.mockRestore()
})
})
describe("Режим изменения размера", () => {
it("должен переключать режим изменения размера", () => {
renderWithProviders()
const resizeButton = screen.getByTitle("timeline.controlsMain.resizableMode")
fireEvent.click(resizeButton)
expect(mockPlayerContext.setIsResizableMode).toHaveBeenCalledWith(true)
})
it("должен показывать правильную иконку в режиме изменения размера", () => {
mockPlayerContext.isResizableMode = true
renderWithProviders()
// В режиме изменения размера title меняется на fixedSizeMode
expect(screen.getByTitle("timeline.controlsMain.fixedSizeMode")).toBeInTheDocument()
})
})
describe("Переключение источника видео", () => {
it("должен переключать источник видео при клике", () => {
renderWithProviders()
// В режиме timeline показывается иконка TvMinimalPlay
const sourceButton = screen.getByTestId("tvminimalplay-icon").closest("button")
fireEvent.click(sourceButton!)
expect(mockPlayerContext.setVideoSource).toHaveBeenCalledWith("browser")
})
it("должен переключать обратно на timeline", () => {
mockPlayerContext.videoSource = "browser" as any
renderWithProviders()
// В режиме browser показывается иконка ImagePlay
const sourceButton = screen.getByTestId("imageplay-icon").closest("button")
fireEvent.click(sourceButton!)
expect(mockPlayerContext.setVideoSource).toHaveBeenCalledWith("timeline")
})
it("должен показывать правильную иконку для текущего источника", () => {
const { rerender } = render()
// В режиме timeline
expect(screen.getByTestId("tvminimalplay-icon")).toBeInTheDocument()
expect(screen.queryByTestId("imageplay-icon")).not.toBeInTheDocument()
// Переключаем на browser
mockPlayerContext.videoSource = "browser" as any
rerender()
// В режиме browser
expect(screen.getByTestId("imageplay-icon")).toBeInTheDocument()
expect(screen.queryByTestId("tvminimalplay-icon")).not.toBeInTheDocument()
})
})
describe("Обработка времени", () => {
it("должен корректно обрабатывать Unix timestamp", () => {
// Unix timestamp (больше года в секундах)
const unixTime = Date.now() / 1000
renderWithProviders()
// Проверяем что слайдер использует правильное значение
const slider = screen.getByTestId("slider")
expect(slider).toHaveValue("0") // localDisplayTime по умолчанию 0
})
it("должен правильно рассчитывать frameTime", () => {
const fileWith60fps = {
...mockFile,
probeData: {
format: {},
streams: [{ r_frame_rate: "60/1" }],
} as any,
}
renderWithProviders()
// getFrameTime мокнут для возврата 1/30, проверяем что слайдер использует step
const slider = screen.getByTestId("slider")
expect(slider).toHaveAttribute("step", "0.001") // Компонент использует фиксированный step
})
})
describe("Отключенные состояния", () => {
it("должен отключать элементы управления при смене камеры", () => {
mockPlayerContext.isChangingCamera = true
renderWithProviders()
const playButton = screen.getByTitle("timeline.controls.play")
expect(playButton).toBeDisabled()
const slider = screen.getByTestId("slider")
expect(slider).toBeDisabled()
})
it("должен показывать prerender controls", () => {
renderWithProviders()
expect(screen.getByTestId("prerender-controls")).toBeInTheDocument()
})
})
})