import { fireEvent, render, screen, waitFor } from "@testing-library/react" import { beforeEach, describe, expect, it, vi } from "vitest" import { EffectPresets } from "../../components/effect-presets" import type { VideoEffect } from "../../types" // Мокаем react-i18next vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string, fallback?: string) => fallback || key, i18n: { language: "ru" }, }), })) // Мокаем UI компоненты vi.mock("@/components/ui/button", () => ({ Button: ({ children, onClick, variant, className, title, ...props }: any) => ( ), })) vi.mock("@/components/ui/tooltip", () => ({ TooltipProvider: ({ children }: any) =>
{children}
, Tooltip: ({ children }: any) =>
{children}
, TooltipTrigger: ({ children, asChild }: any) => (
{children}
), TooltipContent: ({ children, side, className }: any) => (
{children}
), })) // Мокаем иконки vi.mock("lucide-react", () => ({ ChevronDown: () => ChevronDown, ChevronRight: () => ChevronRight, Settings: () => Settings, Trash2: () => Trash2, })) describe("EffectPresets", () => { const mockPresets = [ { id: "light", name: { ru: "Легкий", en: "Light" }, description: { ru: "Легкий эффект", en: "Light effect" }, parameters: { intensity: 25, brightness: 110 }, tags: ["basic"], }, { id: "strong", name: { ru: "Сильный", en: "Strong" }, description: { ru: "Сильный эффект", en: "Strong effect" }, parameters: { intensity: 75, brightness: 130 }, tags: ["intense"], }, ] const baseEffect: VideoEffect = { id: "test-effect", name: { ru: "Тестовый эффект", en: "Test Effect" }, description: { ru: "Тестовый эффект", en: "Test Effect" }, category: "blur_sharpen", scope: ["video"], processingType: "css", version: "1.0.0", tags: ["popular"], complexity: "low", gpuAccelerated: false, parameters: [], presets: mockPresets, processors: { css: { shader: "blur(1px)", }, }, } const mockProps = { effect: baseEffect, onApplyPreset: vi.fn(), selectedPreset: undefined, } const mockLocalStorage = (() => { let store: Record = {} return { getItem: vi.fn((key: string) => store[key] || null), setItem: vi.fn((key: string, value: string) => { store[key] = value }), removeItem: vi.fn((key: string) => { delete store[key] }), clear: vi.fn(() => { store = {} }), } })() beforeEach(() => { vi.clearAllMocks() // Мокаем localStorage Object.defineProperty(window, "localStorage", { value: mockLocalStorage, writable: true, }) // Мокаем console vi.spyOn(console, "error").mockImplementation(() => {}) // Сброс состояния localStorage mockLocalStorage.clear() }) describe("Рендеринг компонента", () => { it("должен отображать заголовок с количеством пресетов", () => { render() expect(screen.getByText("Пресеты")).toBeInTheDocument() expect(screen.getByText("(2)")).toBeInTheDocument() }) it("должен отображать иконку настроек", () => { render() expect(screen.getByTestId("settings-icon")).toBeInTheDocument() }) it("должен быть свернут по умолчанию", () => { render() expect(screen.getByTestId("chevron-right")).toBeInTheDocument() expect(screen.queryByText("Легкий")).not.toBeInTheDocument() }) it("не должен отображаться если нет пресетов", () => { const effectWithoutPresets = { ...baseEffect, presets: undefined } const { container } = render() expect(container.firstChild).toBeNull() }) }) describe("Разворачивание/сворачивание", () => { it("должен разворачиваться при клике на заголовок", () => { render() const header = screen.getByRole("button") fireEvent.click(header) expect(screen.getByTestId("chevron-down")).toBeInTheDocument() expect(screen.getAllByText("Легкий")).toHaveLength(2) // button + tooltip expect(screen.getAllByText("Сильный")).toHaveLength(2) // button + tooltip }) it("должен сворачиваться при повторном клике", () => { render() const header = screen.getByRole("button") // Разворачиваем fireEvent.click(header) expect(screen.getAllByText("Легкий")).toHaveLength(2) // button + tooltip // Сворачиваем fireEvent.click(header) expect(screen.queryByText("Легкий")).not.toBeInTheDocument() }) }) describe("Отображение пресетов", () => { beforeEach(() => { mockLocalStorage.clear() mockLocalStorage.getItem.mockReturnValue(null) render() const header = screen.getByRole("button") fireEvent.click(header) // Разворачиваем }) it("должен отображать названия пресетов на русском языке", () => { expect(screen.getAllByText("Легкий")).toHaveLength(2) // button + tooltip expect(screen.getAllByText("Сильный")).toHaveLength(2) // button + tooltip }) it("должен отображать описания пресетов", () => { expect(screen.getAllByText("Легкий эффект")).toHaveLength(2) // button + tooltip expect(screen.getAllByText("Сильный эффект")).toHaveLength(2) // button + tooltip }) it("должен выделять выбранный пресет", () => { // Этот тест нужно выполнить отдельно с selectedPreset // поэтому не используем beforeEach этого describe блока }) it("должен выделять выбранный пресет - отдельный тест", () => { mockLocalStorage.clear() mockLocalStorage.getItem.mockReturnValue(null) const { container } = render() const header = container.querySelector("button")! fireEvent.click(header) // Ищем кнопки пресетов по их data-variant атрибуту const allButtons = container.querySelectorAll("button[data-variant]") const lightButton = Array.from(allButtons).find( (btn) => btn.textContent?.includes("Легкий") && !btn.textContent?.includes("ChevronDown") && !btn.textContent?.includes("Settings"), ) const strongButton = Array.from(allButtons).find( (btn) => btn.textContent?.includes("Сильный") && !btn.textContent?.includes("ChevronDown") && !btn.textContent?.includes("Settings"), ) expect(lightButton).toHaveAttribute("data-variant", "default") expect(strongButton).toHaveAttribute("data-variant", "outline") }) it("должен вызывать onApplyPreset при клике на пресет", () => { const lightPresetButtons = screen.getAllByText("Легкий") const lightPresetButton = lightPresetButtons[0].closest("button") fireEvent.click(lightPresetButton!) expect(mockProps.onApplyPreset).toHaveBeenCalledWith("light", { intensity: 25, brightness: 110, }) }) it("должен отображать tooltip с информацией о пресете", () => { expect(screen.getAllByTestId("tooltip-content")).toHaveLength(2) }) }) describe("Пользовательские пресеты", () => { const customPresets = { custom_1234567890: { name: { ru: "Мой пресет", en: "My Preset" }, description: { ru: "Пользовательский пресет", en: "Custom preset" }, params: { intensity: 50, brightness: 120 }, createdAt: "2024-01-01T00:00:00.000Z", }, } beforeEach(() => { mockLocalStorage.getItem.mockReturnValue(JSON.stringify(customPresets)) }) it("должен загружать пользовательские пресеты из localStorage", async () => { render() expect(mockLocalStorage.getItem).toHaveBeenCalledWith("effect_presets_test-effect") const header = screen.getByRole("button") fireEvent.click(header) await waitFor(() => { expect(screen.getAllByText("Мой пресет")).toHaveLength(2) // button + tooltip }) }) it("должен отображать индикатор пользовательского пресета", async () => { render() const header = screen.getByRole("button") fireEvent.click(header) await waitFor(() => { expect(screen.getAllByText("Пользовательский")).toHaveLength(1) // только в кнопке, не в tooltip }) }) it("должен отображать кнопку удаления для пользовательских пресетов", async () => { render() const header = screen.getByRole("button") fireEvent.click(header) await waitFor(() => { expect(screen.getByTestId("trash-icon")).toBeInTheDocument() }) }) it("должен удалять пользовательский пресет", async () => { render() const header = screen.getByRole("button") fireEvent.click(header) await waitFor(() => { const deleteButton = screen.getByTestId("trash-icon").closest("button") fireEvent.click(deleteButton!) }) // Когда удаляем последний пресет, компонент вызывает removeItem expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("effect_presets_test-effect") }) it("должен удалять ключ из localStorage когда нет пресетов", async () => { render() const header = screen.getByRole("button") fireEvent.click(header) await waitFor(() => { const deleteButton = screen.getByTestId("trash-icon").closest("button") fireEvent.click(deleteButton!) }) expect(mockLocalStorage.removeItem).toHaveBeenCalledWith("effect_presets_test-effect") }) it("должен отображать дату создания пользовательского пресета", async () => { render() const header = screen.getByRole("button") fireEvent.click(header) await waitFor(() => { // Ищем текст в tooltipе по data-testid const tooltips = screen.getAllByTestId("tooltip-content") const createdAtTooltip = tooltips.find((tooltip) => tooltip.textContent?.includes("Создано")) expect(createdAtTooltip).toBeInTheDocument() }) }) it("должен обновлять количество пресетов с учетом пользовательских", async () => { render() await waitFor(() => { expect(screen.getByText("(3)")).toBeInTheDocument() // 2 встроенных + 1 пользовательский }) }) }) describe("Локализация", () => { it("должен использовать fallback на английский для названий", () => { const presetWithoutRu = { ...baseEffect, presets: [ { id: "test", name: { en: "English Only" }, description: { ru: "Описание", en: "Description" }, parameters: { intensity: 50 }, tags: [], }, ], } render() const header = screen.getByRole("button") fireEvent.click(header) expect(screen.getAllByText("English Only")).toHaveLength(2) // button + tooltip }) it("должен использовать fallback на английский для описаний", () => { const presetWithoutRu = { ...baseEffect, presets: [ { id: "test", name: { ru: "Тест", en: "Test" }, description: { en: "English Description" }, parameters: { intensity: 50 }, tags: [], }, ], } render() const header = screen.getByRole("button") fireEvent.click(header) expect(screen.getAllByText("English Description")).toHaveLength(2) // button + tooltip }) }) describe("Обработка ошибок", () => { it("должен обрабатывать ошибки при загрузке из localStorage", () => { mockLocalStorage.getItem.mockReturnValue("invalid json") expect(() => { render() }).not.toThrow() expect(console.error).toHaveBeenCalledWith("Error parsing custom presets:", expect.any(SyntaxError)) }) it("должен работать когда localStorage недоступен", () => { mockLocalStorage.getItem.mockImplementation(() => { throw new Error("localStorage not available") }) expect(() => { render() }).not.toThrow() expect(console.error).toHaveBeenCalledWith("Error accessing localStorage:", expect.any(Error)) }) it("должен обрабатывать пустые пресеты", () => { const effectWithEmptyPresets = { ...baseEffect, presets: [] } const { container } = render() expect(container.firstChild).toBeNull() }) }) describe("Tooltip отображение", () => { beforeEach(() => { mockLocalStorage.clear() mockLocalStorage.getItem.mockReturnValue(null) render() const header = screen.getByRole("button") fireEvent.click(header) }) it("должен отображать параметры в tooltip", () => { const tooltipContent = screen.getAllByTestId("tooltip-content") expect(tooltipContent.length).toBeGreaterThan(0) }) it("должен отображать правильные CSS классы для tooltip", () => { const tooltipContent = screen.getAllByTestId("tooltip-content")[0] expect(tooltipContent).toHaveAttribute("data-side", "right") expect(tooltipContent).toHaveClass("max-w-xs") }) }) describe("CSS и стили", () => { it("должен применять правильные классы к контейнеру", () => { const { container } = render() const mainContainer = container.firstChild expect(mainContainer).toHaveClass("border", "rounded-lg") }) it("должен применять hover эффекты к заголовку", () => { render() const header = screen.getByRole("button") expect(header).toHaveClass("hover:bg-gray-50", "dark:hover:bg-gray-800") }) it("должен применять правильную высоту к кнопкам пресетов", () => { render() const header = screen.getByRole("button") fireEvent.click(header) const presetButtons = screen.getAllByText("Легкий") const presetButton = presetButtons[0].closest("button") expect(presetButton).toHaveClass("h-auto", "py-2") }) }) describe("Интеграция", () => { it("должен корректно работать с эффектом без встроенных пресетов но с пользовательскими", () => { const customPresets = { custom_123: { name: { ru: "Единственный", en: "Only One" }, description: { ru: "Единственный пресет", en: "Only preset" }, params: { intensity: 60 }, }, } mockLocalStorage.getItem.mockReturnValue(JSON.stringify(customPresets)) const effectWithoutBuiltinPresets = { ...baseEffect, presets: undefined } render() expect(screen.getByText("(1)")).toBeInTheDocument() }) it("должен правильно объединять встроенные и пользовательские пресеты", async () => { const customPresets = { custom_456: { name: { ru: "Дополнительный", en: "Additional" }, description: { ru: "Дополнительный пресет", en: "Additional preset" }, params: { intensity: 80 }, }, } mockLocalStorage.getItem.mockReturnValue(JSON.stringify(customPresets)) render() await waitFor(() => { expect(screen.getByText("(3)")).toBeInTheDocument() // 2 встроенных + 1 пользовательский }) const header = screen.getByRole("button") fireEvent.click(header) expect(screen.getAllByText("Легкий")).toHaveLength(2) // встроенный (button + tooltip) expect(screen.getAllByText("Сильный")).toHaveLength(2) // встроенный (button + tooltip) await waitFor(() => { expect(screen.getAllByText("Дополнительный")).toHaveLength(2) // пользовательский (button + tooltip) }) }) }) describe("Производительность", () => { it("должен эффективно рендерить большое количество пресетов", () => { const manyPresets = Array.from({ length: 20 }, (_, i) => ({ id: `preset-${i}`, name: { ru: `Пресет ${i}`, en: `Preset ${i}` }, description: { ru: `Описание ${i}`, en: `Description ${i}` }, parameters: { intensity: i * 5 }, tags: [], })) const effectWithManyPresets = { ...baseEffect, presets: manyPresets, } const startTime = performance.now() render() const endTime = performance.now() expect(endTime - startTime).toBeLessThan(100) }) it("должен правильно обрабатывать частые изменения selectedPreset", () => { const { rerender } = render() for (let i = 0; i < 10; i++) { const preset = i % 2 === 0 ? "light" : "strong" rerender() } expect(screen.getByText("Пресеты")).toBeInTheDocument() }) }) })