import { fireEvent, render, screen, waitFor } from "@testing-library/react"
import { beforeEach, describe, expect, it, vi } from "vitest"
import { EffectComparison } from "../../components/effect-comparison"
import { VideoEffect } from "../../types"
import { generateCSSFilterForEffect, getPlaybackRate } from "../../utils/css-effects"
// Мокаем внешние зависимости
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, fallback?: string) => fallback || key,
}),
}))
vi.mock("@/components/ui/button", () => ({
Button: ({ children, onClick, ...props }: any) => (
),
}))
vi.mock("@/components/ui/slider", () => ({
Slider: ({ value, onValueChange, min, max, step, ...props }: any) => (
onValueChange([Number.parseInt(e.target.value)])}
min={min}
max={max}
step={step}
{...props}
/>
),
}))
vi.mock("lucide-react", () => ({
Pause: () => Pause,
Play: () => Play,
RotateCcw: () => Reset,
}))
vi.mock("../../utils/css-effects", () => ({
generateCSSFilterForEffect: vi.fn(),
getPlaybackRate: vi.fn(),
}))
describe("EffectComparison", () => {
const mockEffect: VideoEffect = {
id: "test-effect",
name: {
ru: "Тестовый эффект",
en: "Test Effect",
},
description: {
ru: "Описание тестового эффекта",
en: "Test effect description",
},
category: "blur_sharpen",
scope: ["video"],
processingType: "css",
version: "1.0.0",
tags: ["test"],
complexity: "low",
gpuAccelerated: false,
parameters: [
{
id: "intensity",
name: { ru: "Интенсивность", en: "Intensity" },
type: "number",
defaultValue: 50,
range: { min: 0, max: 100 },
},
{
id: "brightness",
name: { ru: "Яркость", en: "Brightness" },
type: "number",
defaultValue: 100,
range: { min: 0, max: 200 },
},
],
presets: [],
processors: {
css: {
shader: "brightness({{brightness}}%) blur({{intensity}}px)",
},
},
}
// Создаем мок для HTMLVideoElement
const createVideoMock = () => {
const videoMock = {
currentTime: 0,
playbackRate: 1,
play: vi.fn().mockResolvedValue(undefined),
pause: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
style: { filter: "" },
}
return videoMock as any
}
beforeEach(() => {
vi.clearAllMocks()
// Сброс моков утилит
vi.mocked(generateCSSFilterForEffect).mockReturnValue("brightness(1.2) contrast(1.1)")
vi.mocked(getPlaybackRate).mockReturnValue(1)
// Мокаем HTMLVideoElement
Object.defineProperty(HTMLVideoElement.prototype, "play", {
writable: true,
value: vi.fn().mockResolvedValue(undefined),
})
Object.defineProperty(HTMLVideoElement.prototype, "pause", {
writable: true,
value: vi.fn(),
})
Object.defineProperty(HTMLVideoElement.prototype, "currentTime", {
writable: true,
value: 0,
})
Object.defineProperty(HTMLVideoElement.prototype, "playbackRate", {
writable: true,
value: 1,
})
// Мокаем getBoundingClientRect
Element.prototype.getBoundingClientRect = vi.fn(() => ({
width: 600,
height: 400,
top: 0,
left: 0,
bottom: 400,
right: 600,
x: 0,
y: 0,
toJSON: vi.fn(),
}))
})
describe("Рендеринг", () => {
it("должен отображать основные элементы", () => {
render()
// Проверяем наличие видео элементов
const videos = document.querySelectorAll("video")
expect(videos).toHaveLength(2)
// Проверяем контролы
expect(screen.getByRole("button", { name: /play|воспроизвести/i })).toBeInTheDocument()
expect(screen.getByRole("button", { name: /reset|сброс/i })).toBeInTheDocument()
expect(screen.getByTestId("slider")).toBeInTheDocument()
})
it("должен устанавливать правильные размеры контейнера", () => {
render()
const container = document.querySelector(".relative.overflow-hidden")
expect(container).toHaveStyle({
width: "800px",
height: "600px",
})
})
it("должен отображать лейблы оригинала и эффекта", () => {
render()
expect(screen.getByText("Оригинал")).toBeInTheDocument()
expect(screen.getByText("С эффектом")).toBeInTheDocument()
})
it("должен использовать переданный путь к видео", () => {
render()
const videos = document.querySelectorAll("video")
videos.forEach((video) => {
expect(video).toHaveAttribute("src", "/custom-video.mp4")
})
})
it("должен использовать дефолтный путь к видео", () => {
render()
const videos = document.querySelectorAll("video")
videos.forEach((video) => {
expect(video).toHaveAttribute("src", "/t1.mp4")
})
})
})
describe("Применение эффектов", () => {
it("должен применять CSS фильтры к видео с эффектом", () => {
render()
expect(vi.mocked(generateCSSFilterForEffect)).toHaveBeenCalledWith(mockEffect)
const effectVideo = document.querySelectorAll("video")[1]
expect(effectVideo.style.filter).toBe("brightness(1.2) contrast(1.1)")
})
it("должен применять кастомные параметры к эффекту", () => {
const customParams = { intensity: 75, brightness: 120 }
render()
const expectedEffect = {
...mockEffect,
parameters: mockEffect.parameters.map((param) => ({
...param,
defaultValue: customParams[param.id] ?? param.defaultValue,
})),
}
expect(vi.mocked(generateCSSFilterForEffect)).toHaveBeenCalledWith(expectedEffect)
})
it("должен применять скорость воспроизведения", () => {
vi.mocked(getPlaybackRate).mockReturnValue(0.8)
render()
expect(vi.mocked(getPlaybackRate)).toHaveBeenCalledWith(mockEffect)
const effectVideo = document.querySelectorAll("video")[1]
expect(effectVideo.playbackRate).toBe(0.8)
})
it("должен обновлять эффект при изменении параметров", () => {
const { rerender } = render()
expect(vi.mocked(generateCSSFilterForEffect)).toHaveBeenCalledTimes(1)
const newCustomParams = { brightness: 150 }
rerender()
expect(vi.mocked(generateCSSFilterForEffect)).toHaveBeenCalledTimes(2)
})
})
describe("Воспроизведение видео", () => {
it("должен начинать воспроизведение при клике на Play", async () => {
render()
const playButton = screen.getByRole("button", { name: /play|воспроизвести/i })
fireEvent.click(playButton)
// Кнопка должна изменится на паузу
await waitFor(() => {
expect(screen.getByTestId("pause-icon")).toBeInTheDocument()
})
})
it("должен ставить на паузу при клике на Pause", async () => {
render()
const playButton = screen.getByRole("button", { name: /play|воспроизвести/i })
// Сначала запускаем воспроизведение
fireEvent.click(playButton)
await waitFor(() => {
expect(screen.getByTestId("pause-icon")).toBeInTheDocument()
})
// Затем ставим на паузу
const pauseButton = screen.getByRole("button", { name: /pause|пауза/i })
fireEvent.click(pauseButton)
// Кнопка должна вернуться к play
await waitFor(() => {
expect(screen.getByTestId("play-icon")).toBeInTheDocument()
})
})
it("должен сбрасывать видео при клике на Reset", () => {
render()
// Сначала воспроизводим
const playButton = screen.getByRole("button", { name: /play|воспроизвести/i })
fireEvent.click(playButton)
const resetButton = screen.getByRole("button", { name: /reset|сброс/i })
fireEvent.click(resetButton)
// Проверяем что кнопка вернулась к play (видео остановлено)
expect(screen.getByTestId("play-icon")).toBeInTheDocument()
})
it("должен синхронизировать время воспроизведения между видео", () => {
render()
// Проверяем что компонент рендерится с двумя видео элементами
const videos = document.querySelectorAll("video")
expect(videos).toHaveLength(2)
})
})
describe("Управление разделителем", () => {
it("должен устанавливать начальную позицию разделителя на 50%", () => {
render()
const slider = screen.getByTestId("slider")
expect(slider).toHaveValue("50")
const separator = document.querySelector('[style*="left: 50%"]')
expect(separator).toBeInTheDocument()
})
it("должен изменять позицию разделителя через слайдер", () => {
render()
const slider = screen.getByTestId("slider")
fireEvent.change(slider, { target: { value: "75" } })
expect(slider).toHaveValue("75")
const separator = document.querySelector('[style*="left: 75%"]')
expect(separator).toBeInTheDocument()
// Проверяем что отображается правильный процент
expect(screen.getByText("75%")).toBeInTheDocument()
})
it("должен начинать перетаскивание при mousedown на контейнере", () => {
render()
const container = document.querySelector(".relative.overflow-hidden")
expect(container).toBeInTheDocument()
fireEvent.mouseDown(container!)
// После mousedown компонент должен войти в режим перетаскивания
// Проверяем что курсор изменился
expect(container).toHaveClass("cursor-ew-resize")
})
it("должен обновлять позицию при перетаскивании мышью", () => {
const addEventListenerSpy = vi.spyOn(window, "addEventListener")
render()
const container = document.querySelector(".relative.overflow-hidden")
fireEvent.mouseDown(container!)
// Проверяем что добавлены слушатели для mousemove и mouseup
expect(addEventListenerSpy).toHaveBeenCalledWith("mousemove", expect.any(Function))
expect(addEventListenerSpy).toHaveBeenCalledWith("mouseup", expect.any(Function))
})
it("должен применять clipPath для видео с эффектом", () => {
render()
const effectVideoContainer = document.querySelector('[style*="clip-path"]')
expect(effectVideoContainer).toBeInTheDocument()
expect(effectVideoContainer).toHaveStyle({
clipPath: "polygon(50% 0, 100% 0, 100% 100%, 50% 100%)",
})
})
it("должен обновлять clipPath при изменении позиции", () => {
render()
const slider = screen.getByTestId("slider")
fireEvent.change(slider, { target: { value: "30" } })
const effectVideoContainer = document.querySelector('[style*="clip-path"]')
expect(effectVideoContainer).toHaveStyle({
clipPath: "polygon(30% 0, 100% 0, 100% 100%, 30% 100%)",
})
})
})
describe("Обработка событий", () => {
it("должен очищать слушатели событий при размонтировании", () => {
const removeEventListenerSpy = vi.spyOn(window, "removeEventListener")
const { unmount } = render()
unmount()
expect(removeEventListenerSpy).toHaveBeenCalledWith("mousemove", expect.any(Function))
expect(removeEventListenerSpy).toHaveBeenCalledWith("mouseup", expect.any(Function))
})
it("должен корректно обрабатывать отсутствие видео элементов", () => {
// Просто проверяем что компонент рендерится без ошибок
expect(() => {
render()
}).not.toThrow()
})
it("должен ограничивать позицию разделителя между 0 и 100", () => {
render()
const slider = screen.getByTestId("slider")
// Тестируем максимальное значение
fireEvent.change(slider, { target: { value: "150" } })
expect(slider).toHaveValue("100")
// Тестируем минимальное значение
fireEvent.change(slider, { target: { value: "-10" } })
expect(slider).toHaveValue("0")
})
})
describe("Состояние компонента", () => {
it("должен сохранять состояние воспроизведения", async () => {
render()
const playButton = screen.getByRole("button", { name: /play|воспроизвести/i })
fireEvent.click(playButton)
await waitFor(() => {
expect(screen.getByTestId("pause-icon")).toBeInTheDocument()
})
})
it("должен сбрасывать состояние при reset", () => {
render()
// Изменяем позицию слайдера
const slider = screen.getByTestId("slider")
fireEvent.change(slider, { target: { value: "80" } })
// Сбрасываем
const resetButton = screen.getByRole("button", { name: /reset|сброс/i })
fireEvent.click(resetButton)
// Позиция должна вернуться к 50%
expect(slider).toHaveValue("50")
expect(screen.getByText("50%")).toBeInTheDocument()
})
it("должен корректно обрабатывать изменения эффекта", () => {
const { rerender } = render()
const newEffect: VideoEffect = {
...mockEffect,
id: "new-effect",
name: "New Effect",
}
rerender()
expect(vi.mocked(generateCSSFilterForEffect)).toHaveBeenLastCalledWith(newEffect)
})
})
describe("Accessibility", () => {
it("должен иметь правильную структуру для скринридеров", () => {
render()
// Проверяем наличие кнопок с правильными ролями
expect(screen.getByRole("button", { name: /play|воспроизвести/i })).toBeInTheDocument()
expect(screen.getByRole("button", { name: /reset|сброс/i })).toBeInTheDocument()
// Проверяем наличие слайдера
expect(screen.getByTestId("slider")).toBeInTheDocument()
})
it("должен отображать текущее значение позиции", () => {
render()
expect(screen.getByText("50%")).toBeInTheDocument()
expect(screen.getByText(/позиция/i)).toBeInTheDocument()
})
})
describe("Синхронизация видео", () => {
it("должен синхронизировать видео когда разница времени > 0.1", () => {
render()
const videos = document.querySelectorAll("video")
const originalVideo = videos[0]
const effectVideo = videos[1]
// Устанавливаем разное время
Object.defineProperty(originalVideo, "currentTime", { value: 5.0, writable: true })
Object.defineProperty(effectVideo, "currentTime", { value: 4.8, writable: true })
// Симулируем событие timeupdate
fireEvent(originalVideo, new Event("timeupdate"))
// Время должно синхронизироваться
expect(effectVideo.currentTime).toBe(5.0)
})
it("не должен синхронизировать видео когда разница времени < 0.1", () => {
render()
const videos = document.querySelectorAll("video")
const originalVideo = videos[0]
const effectVideo = videos[1]
// Устанавливаем близкое время
Object.defineProperty(originalVideo, "currentTime", { value: 5.0, writable: true })
Object.defineProperty(effectVideo, "currentTime", { value: 4.95, writable: true })
// Симулируем событие timeupdate
fireEvent(originalVideo, new Event("timeupdate"))
// Время не должно измениться
expect(effectVideo.currentTime).toBe(4.95)
})
it("должен корректно обрабатывать отсутствие видео элементов при синхронизации", () => {
// Мокаем refs чтобы они возвращали null
const { container } = render()
// Удаляем video элементы из DOM
const videos = container.querySelectorAll("video")
videos.forEach((video) => video.remove())
// Проверяем что компонент не падает при отсутствии видео
expect(() => {
const mockEvent = new Event("timeupdate")
fireEvent(window, mockEvent)
}).not.toThrow()
})
})
describe("Обработка перетаскивания мыши", () => {
it("должен игнорировать mousemove когда не в режиме перетаскивания", () => {
render()
const slider = screen.getByTestId("slider")
const initialValue = slider.getAttribute("value")
// Симулируем mousemove без предварительного mousedown
fireEvent(
window,
new MouseEvent("mousemove", {
clientX: 300,
clientY: 200,
}),
)
// Значение слайдера не должно измениться
expect(slider).toHaveValue(initialValue)
})
it("должен игнорировать mousemove когда containerRef недоступен", () => {
render()
const container = document.querySelector(".relative.overflow-hidden")
const slider = screen.getByTestId("slider")
// Начинаем перетаскивание
fireEvent.mouseDown(container!)
// Удаляем container из DOM для имитации отсутствия ref
container?.remove()
const initialValue = slider.getAttribute("value")
// Симулируем mousemove
fireEvent(
window,
new MouseEvent("mousemove", {
clientX: 300,
clientY: 200,
}),
)
// Значение не должно измениться
expect(slider).toHaveValue(initialValue)
})
it("должен правильно рассчитывать позицию при mousemove на краях", () => {
render()
const container = document.querySelector(".relative.overflow-hidden")
const slider = screen.getByTestId("slider")
// Начинаем перетаскивание
fireEvent.mouseDown(container!)
// Тест левого края (x = 0)
fireEvent(
window,
new MouseEvent("mousemove", {
clientX: 0,
clientY: 200,
}),
)
expect(slider).toHaveValue("0")
// Тест правого края (x больше ширины)
fireEvent(
window,
new MouseEvent("mousemove", {
clientX: 700, // больше чем ширина 600px
clientY: 200,
}),
)
expect(slider).toHaveValue("100")
})
it("должен корректно завершать перетаскивание при mouseup", () => {
const removeEventListenerSpy = vi.spyOn(window, "removeEventListener")
render()
const container = document.querySelector(".relative.overflow-hidden")
// Начинаем перетаскивание
fireEvent.mouseDown(container!)
// Завершаем перетаскивание
fireEvent(window, new MouseEvent("mouseup"))
// Проверяем что слушатели удалены
expect(removeEventListenerSpy).toHaveBeenCalledWith("mousemove", expect.any(Function))
expect(removeEventListenerSpy).toHaveBeenCalledWith("mouseup", expect.any(Function))
})
})
describe("Производительность", () => {
it("должен не вызывать лишних перерендеров", () => {
let renderCount = 0
const TestComponent = () => {
renderCount++
return
}
const { rerender } = render()
expect(renderCount).toBe(1)
// Повторный рендер с теми же пропсами
rerender()
// Рендер должен произойти, но это нормально для React
expect(renderCount).toBe(2)
})
it("должен эффективно обрабатывать частые изменения позиции", () => {
render()
const slider = screen.getByTestId("slider")
// Множественные быстрые изменения
for (let i = 0; i < 10; i++) {
fireEvent.change(slider, { target: { value: `${i * 10}` } })
}
// Финальное значение должно быть корректным
expect(slider).toHaveValue("90")
})
})
})