import { fireEvent, render, screen, waitFor } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { beforeEach, describe, expect, it, vi } from "vitest" import { ProjectSettingsModal } from "../../components/project-settings-modal" // Создаем моки функций const mockUpdateSettings = vi.fn() const mockResetSettings = vi.fn() const mockCloseModal = vi.fn() const mockSettings = { aspectRatio: { label: "16:9", textLabel: "Widescreen", value: { width: 1920, height: 1080 }, }, resolution: "1920x1080", frameRate: "30", colorSpace: "rec709", } // Мокируем все зависимости vi.mock("../../hooks/use-project-settings", () => ({ useProjectSettings: vi.fn(() => ({ settings: mockSettings, updateSettings: mockUpdateSettings, resetSettings: mockResetSettings, })), })) vi.mock("@/features/modals/services/modal-provider", () => ({ useModal: vi.fn(() => ({ closeModal: mockCloseModal, openModal: vi.fn(), isModalOpen: vi.fn(), })), })) vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string, params?: any) => { if (params) { return `${key} ${JSON.stringify(params)}` } return key }, }), })) // Мокируем lucide-react иконки vi.mock("lucide-react", async (importOriginal) => { const actual = await importOriginal() return { ...actual, Lock: () =>
Lock
, Unlock: () =>
Unlock
, ChevronDownIcon: () =>
ChevronDown
, } }) // Мокируем типы и константы проекта vi.mock("../../types/project", () => ({ ASPECT_RATIOS: [ { label: "16:9", textLabel: "Widescreen", value: { width: 1920, height: 1080 } }, { label: "4:3", textLabel: "Standard", value: { width: 1440, height: 1080 } }, { label: "1:1", textLabel: "Square", value: { width: 1080, height: 1080 } }, { label: "custom", textLabel: "Custom", value: { width: 1920, height: 1080 } }, ], FRAME_RATES: [ { label: "24 fps", value: "24" }, { label: "30 fps", value: "30" }, { label: "60 fps", value: "60" }, ], COLOR_SPACES: [ { label: "Rec. 709", value: "rec709" }, { label: "Rec. 2020", value: "rec2020" }, { label: "sRGB", value: "srgb" }, ], getDefaultResolutionForAspectRatio: vi.fn((aspectRatio) => { const resolutions: Record = { "16:9": { value: "1920x1080", width: 1920, height: 1080, label: "1920x1080" }, "4:3": { value: "1440x1080", width: 1440, height: 1080, label: "1440x1080" }, "1:1": { value: "1080x1080", width: 1080, height: 1080, label: "1080x1080" }, custom: { value: "custom", width: 1920, height: 1080, label: "Custom" }, } return resolutions[aspectRatio] || resolutions["16:9"] }), getResolutionsForAspectRatio: vi.fn(() => [ { value: "1920x1080", width: 1920, height: 1080, label: "1920x1080" }, { value: "1280x720", width: 1280, height: 720, label: "1280x720" }, { value: "3840x2160", width: 3840, height: 2160, label: "3840x2160" }, ]), })) // Мокируем утилиты vi.mock("../../utils", () => ({ getAspectRatioLabel: vi.fn((textLabel) => textLabel), getAspectRatioString: vi.fn((width, height) => { const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b)) const divisor = gcd(width, height) return `${width / divisor}:${height / divisor}` }), })) // Мокируем window.dispatchEvent Object.defineProperty(window, "dispatchEvent", { writable: true, value: vi.fn(), }) describe("ProjectSettingsModal", () => { beforeEach(() => { vi.clearAllMocks() // Сбрасываем настройки к дефолтным mockSettings.aspectRatio = { label: "16:9", textLabel: "Widescreen", value: { width: 1920, height: 1080 }, } mockSettings.resolution = "1920x1080" mockSettings.frameRate = "30" mockSettings.colorSpace = "rec709" }) describe("Базовый рендеринг", () => { it("должен рендериться без ошибок", () => { expect(() => { render() }).not.toThrow() }) it("должен рендерить все основные элементы", () => { render() expect(screen.getByText("dialogs.projectSettings.aspectRatio")).toBeInTheDocument() expect(screen.getByText("dialogs.projectSettings.resolution")).toBeInTheDocument() expect(screen.getByText("dialogs.projectSettings.customSize")).toBeInTheDocument() expect(screen.getByText("dialogs.projectSettings.frameRate")).toBeInTheDocument() expect(screen.getByText("dialogs.projectSettings.colorSpace")).toBeInTheDocument() expect(screen.getByText("dialogs.projectSettings.cancel")).toBeInTheDocument() expect(screen.getByText("dialogs.projectSettings.save")).toBeInTheDocument() }) it("должен показывать текущие настройки", () => { render() expect(screen.getByDisplayValue("1920")).toBeInTheDocument() expect(screen.getByDisplayValue("1080")).toBeInTheDocument() }) it("должен показывать иконку блокировки по умолчанию", () => { render() expect(screen.getAllByTestId("lock-icon")).toHaveLength(2) // Кнопка + информация }) it("должен отображать правильное количество combobox элементов", () => { render() const comboboxes = screen.getAllByRole("combobox") expect(comboboxes).toHaveLength(4) // аспект, разрешение, fps, цвет }) it("должен отображать правильное количество input элементов", () => { render() const numberInputs = screen.getAllByRole("spinbutton") expect(numberInputs).toHaveLength(2) // ширина и высота }) }) describe("Изменение соотношения сторон", () => { it("должен рендерить select для соотношения сторон", () => { render() const comboboxes = screen.getAllByRole("combobox") expect(comboboxes).toHaveLength(4) // аспект, разрешение, fps, цвет expect(comboboxes[0]).toBeInTheDocument() }) it("должен показывать текущее соотношение сторон", () => { render() const aspectRatioCombobox = screen.getAllByRole("combobox")[0] expect(aspectRatioCombobox).toHaveTextContent("16:9") }) it.skip("должен обрабатывать смену соотношения сторон на custom", () => { mockSettings.aspectRatio.label = "custom" render() expect(screen.getByText("dialogs.projectSettings.aspectRatioLabels.custom")).toBeInTheDocument() // Восстанавливаем mockSettings.aspectRatio.label = "16:9" }) }) describe("Изменение разрешения", () => { it("должен рендерить select для разрешения", () => { render() const resolutionSelect = screen.getAllByRole("combobox")[1] expect(resolutionSelect).toBeInTheDocument() }) it("должен показывать текущее разрешение", () => { render() const resolutionSelect = screen.getAllByRole("combobox")[1] expect(resolutionSelect).toHaveTextContent("1920x1080") }) it.skip("должен показывать custom для пользовательского соотношения", () => { mockSettings.aspectRatio.label = "custom" mockSettings.resolution = "custom" render() const resolutionSelect = screen.getAllByRole("combobox")[1] expect(resolutionSelect).toHaveTextContent("dialogs.projectSettings.aspectRatioLabels.custom") // Восстанавливаем mockSettings.aspectRatio.label = "16:9" mockSettings.resolution = "1920x1080" }) }) describe("Пользовательские размеры", () => { it("должен изменять ширину", () => { render() const widthInput = screen.getByDisplayValue("1920") fireEvent.change(widthInput, { target: { value: "1280" } }) expect(mockUpdateSettings).toHaveBeenCalled() }) it("должен изменять высоту", () => { render() const heightInput = screen.getByDisplayValue("1080") fireEvent.change(heightInput, { target: { value: "720" } }) expect(mockUpdateSettings).toHaveBeenCalled() }) it("должен игнорировать неверные значения ширины", () => { render() const widthInput = screen.getByDisplayValue("1920") fireEvent.change(widthInput, { target: { value: "abc" } }) // При неверном значении функция не должна вызываться expect(mockUpdateSettings).not.toHaveBeenCalled() }) it("должен игнорировать отрицательные значения высоты", () => { render() const heightInput = screen.getByDisplayValue("1080") fireEvent.change(heightInput, { target: { value: "-100" } }) expect(mockUpdateSettings).not.toHaveBeenCalled() }) it("должен игнорировать нулевые значения", () => { render() const widthInput = screen.getByDisplayValue("1920") fireEvent.change(widthInput, { target: { value: "0" } }) expect(mockUpdateSettings).not.toHaveBeenCalled() }) it("должен пропорционально изменять высоту при заблокированном соотношении", () => { render() const widthInput = screen.getByDisplayValue("1920") fireEvent.change(widthInput, { target: { value: "1280" } }) expect(mockUpdateSettings).toHaveBeenCalledWith( expect.objectContaining({ aspectRatio: expect.objectContaining({ value: expect.objectContaining({ width: 1280, height: expect.any(Number), }), }), }), ) }) it("должен пропорционально изменять ширину при заблокированном соотношении", () => { render() const heightInput = screen.getByDisplayValue("1080") fireEvent.change(heightInput, { target: { value: "720" } }) expect(mockUpdateSettings).toHaveBeenCalledWith( expect.objectContaining({ aspectRatio: expect.objectContaining({ value: expect.objectContaining({ width: expect.any(Number), height: 720, }), }), }), ) }) it("должен устанавливать правильные атрибуты min/max", () => { render() const widthInput = screen.getByDisplayValue("1920") const heightInput = screen.getByDisplayValue("1080") expect(widthInput).toHaveAttribute("min", "320") expect(widthInput).toHaveAttribute("max", "7680") expect(heightInput).toHaveAttribute("min", "240") expect(heightInput).toHaveAttribute("max", "4320") }) it("должен обрабатывать экстремальные значения", () => { render() const widthInput = screen.getByDisplayValue("1920") // Тестируем минимальное значение fireEvent.change(widthInput, { target: { value: "320" } }) expect(mockUpdateSettings).toHaveBeenCalled() vi.clearAllMocks() // Тестируем максимальное значение fireEvent.change(widthInput, { target: { value: "7680" } }) expect(mockUpdateSettings).toHaveBeenCalled() }) }) describe("Блокировка соотношения сторон", () => { it("должен переключать блокировку соотношения сторон", () => { render() // Поскольку кнопка не имеет accessible name, ищем по title const lockButton = screen.getByTitle("dialogs.projectSettings.unlockAspectRatio") fireEvent.click(lockButton) expect(screen.getAllByTestId("unlock-icon")).toHaveLength(2) // Кнопка + информация }) it("должен показывать тултип для блокировки", () => { render() const lockButton = screen.getByTitle("dialogs.projectSettings.unlockAspectRatio") expect(lockButton).toHaveAttribute("title", "dialogs.projectSettings.unlockAspectRatio") }) it("должен изменять стили кнопки при переключении", () => { render() const lockButton = screen.getByTitle("dialogs.projectSettings.unlockAspectRatio") // Проверяем что кнопка имеет класс для заблокированного состояния expect(lockButton).toHaveClass("text-[#00CCC0]") fireEvent.click(lockButton) const unlockButton = screen.getByTitle("dialogs.projectSettings.lockAspectRatio") expect(unlockButton).toHaveClass("text-gray-400") }) it("должен изменять только ширину при разблокированном соотношении", () => { render() // Разблокируем соотношение const lockButton = screen.getByTitle("dialogs.projectSettings.unlockAspectRatio") fireEvent.click(lockButton) vi.clearAllMocks() const widthInput = screen.getByDisplayValue("1920") fireEvent.change(widthInput, { target: { value: "1280" } }) expect(mockUpdateSettings).toHaveBeenCalledWith( expect.objectContaining({ aspectRatio: expect.objectContaining({ value: expect.objectContaining({ width: 1280, }), }), }), ) }) }) describe("Изменение частоты кадров", () => { it("должен рендерить select для частоты кадров", () => { render() const frameRateSelect = screen.getAllByRole("combobox")[2] expect(frameRateSelect).toBeInTheDocument() }) it("должен показывать текущую частоту кадров", () => { render() const frameRateSelect = screen.getAllByRole("combobox")[2] expect(frameRateSelect).toHaveTextContent("30") }) it("должен обрабатывать смену частоты кадров через mock", () => { render() // Поскольку мы не можем легко тестировать dropdown, проверим что select существует const frameRateSelect = screen.getAllByRole("combobox")[2] expect(frameRateSelect).toHaveAttribute("role", "combobox") }) }) describe("Изменение цветового пространства", () => { it("должен рендерить select для цветового пространства", () => { render() const colorSpaceSelect = screen.getAllByRole("combobox")[3] expect(colorSpaceSelect).toBeInTheDocument() }) it("должен показывать текущее цветовое пространство", () => { render() const colorSpaceSelect = screen.getAllByRole("combobox")[3] expect(colorSpaceSelect).toHaveTextContent("Rec. 709") }) it("должен обрабатывать смену цветового пространства через mock", () => { render() const colorSpaceSelect = screen.getAllByRole("combobox")[3] expect(colorSpaceSelect).toHaveAttribute("role", "combobox") }) }) describe("Кнопки действий", () => { it("должен закрывать модальное окно при нажатии Отмена", async () => { const user = userEvent.setup() render() const cancelButton = screen.getByText("dialogs.projectSettings.cancel") await user.click(cancelButton) expect(mockCloseModal).toHaveBeenCalled() expect(mockUpdateSettings).not.toHaveBeenCalled() }) it("должен сохранять настройки и закрывать модальное окно при нажатии Сохранить", async () => { const user = userEvent.setup() render() const saveButton = screen.getByText("dialogs.projectSettings.save") await user.click(saveButton) expect(mockUpdateSettings).toHaveBeenCalled() await waitFor( () => { expect(mockCloseModal).toHaveBeenCalled() }, { timeout: 150 }, ) }) it("должен вызывать window.dispatchEvent при сохранении", async () => { const user = userEvent.setup() render() const saveButton = screen.getByText("dialogs.projectSettings.save") await user.click(saveButton) await waitFor( () => { expect(window.dispatchEvent).toHaveBeenCalledWith( expect.objectContaining({ type: "resize", }), ) }, { timeout: 150 }, ) }) it.skip("должен обрабатывать сохранение с пользовательским разрешением", async () => { const user = userEvent.setup() // Устанавливаем custom разрешение mockSettings.resolution = "custom" render() const saveButton = screen.getByText("dialogs.projectSettings.save") await user.click(saveButton) expect(mockUpdateSettings).toHaveBeenCalledWith( expect.objectContaining({ resolution: expect.stringMatching(/\\d+x\\d+/), }), ) // Восстанавливаем mockSettings.resolution = "1920x1080" }) it("должен обрабатывать сохранение со стандартным разрешением", async () => { const user = userEvent.setup() render() const saveButton = screen.getByText("dialogs.projectSettings.save") await user.click(saveButton) expect(mockUpdateSettings).toHaveBeenCalledWith( expect.objectContaining({ aspectRatio: expect.objectContaining({ value: expect.objectContaining({ width: expect.any(Number), height: expect.any(Number), }), }), }), ) }) it.skip("должен применять таймаут при сохранении", async () => { const user = userEvent.setup() render() const saveButton = screen.getByText("dialogs.projectSettings.save") const startTime = Date.now() await user.click(saveButton) await waitFor( () => { expect(mockCloseModal).toHaveBeenCalled() }, { timeout: 150 }, ) const endTime = Date.now() expect(endTime - startTime).toBeGreaterThanOrEqual(50) // Облегченная проверка задержки }) it("должен обрабатывать множественные нажатия кнопки сохранения", async () => { const user = userEvent.setup() render() const saveButton = screen.getByText("dialogs.projectSettings.save") // Быстро нажимаем несколько раз await user.click(saveButton) await user.click(saveButton) await user.click(saveButton) // Должно сработать несколько раз expect(mockUpdateSettings).toHaveBeenCalledTimes(3) }) }) describe("Информация о соотношении сторон", () => { it("должен показывать информацию о заблокированном соотношении", () => { render() expect(screen.getByText(/dialogs.projectSettings.aspectRatioLocked/)).toBeInTheDocument() }) it("должен показывать информацию о разблокированном соотношении", () => { render() // Разблокируем соотношение const lockButton = screen.getByTitle("dialogs.projectSettings.unlockAspectRatio") fireEvent.click(lockButton) expect(screen.getByText(/dialogs.projectSettings.aspectRatioUnlocked/)).toBeInTheDocument() }) it("должен показывать пользовательское соотношение для custom", () => { // Устанавливаем custom соотношение mockSettings.aspectRatio.label = "custom" render() expect(screen.getByText(/dialogs.projectSettings.aspectRatioLocked/)).toBeInTheDocument() // Восстанавливаем mockSettings.aspectRatio.label = "16:9" }) it("должен отображать правильные иконки для состояния блокировки", () => { render() // В заблокированном состоянии должны быть иконки Lock expect(screen.getAllByTestId("lock-icon")).toHaveLength(2) // Разблокируем const lockButton = screen.getByTitle("dialogs.projectSettings.unlockAspectRatio") fireEvent.click(lockButton) // Теперь должны быть иконки Unlock expect(screen.getAllByTestId("unlock-icon")).toHaveLength(2) }) it("должен показывать правильный цвет для заблокированного состояния", () => { render() const lockButton = screen.getByTitle("dialogs.projectSettings.unlockAspectRatio") expect(lockButton).toHaveClass("text-[#00CCC0]") }) }) describe("Сложные сценарии", () => { it("должен обрабатывать последовательные изменения размеров", () => { render() const widthInput = screen.getByDisplayValue("1920") const heightInput = screen.getByDisplayValue("1080") // Изменяем ширину fireEvent.change(widthInput, { target: { value: "1280" } }) expect(mockUpdateSettings).toHaveBeenCalledTimes(1) // Изменяем высоту на непропорциональное значение fireEvent.change(heightInput, { target: { value: "800" } }) expect(mockUpdateSettings).toHaveBeenCalledTimes(2) }) it("должен обрабатывать переключение между заблокированным и разблокированным режимом", () => { render() const lockButton = screen.getByTitle("dialogs.projectSettings.unlockAspectRatio") // Разблокируем fireEvent.click(lockButton) expect(screen.getAllByTestId("unlock-icon")).toHaveLength(2) // Заблокируем обратно const unlockButton = screen.getByTitle("dialogs.projectSettings.lockAspectRatio") fireEvent.click(unlockButton) expect(screen.getAllByTestId("lock-icon")).toHaveLength(2) }) it("должен сохранять состояние блокировки при изменении размеров", () => { render() // Разблокируем соотношение const lockButton = screen.getByTitle("dialogs.projectSettings.unlockAspectRatio") fireEvent.click(lockButton) vi.clearAllMocks() // Изменяем размеры - они должны изменяться независимо const widthInput = screen.getByDisplayValue("1920") fireEvent.change(widthInput, { target: { value: "1600" } }) expect(mockUpdateSettings).toHaveBeenCalledWith( expect.objectContaining({ aspectRatio: expect.objectContaining({ value: expect.objectContaining({ width: 1600, }), }), }), ) }) it("должен правильно обрабатывать пограничные случаи в расчетах", () => { render() const widthInput = screen.getByDisplayValue("1920") // Проверяем расчет для нечетных чисел fireEvent.change(widthInput, { target: { value: "1921" } }) expect(mockUpdateSettings).toHaveBeenCalled() vi.clearAllMocks() // Проверяем расчет для очень больших чисел fireEvent.change(widthInput, { target: { value: "7680" } }) expect(mockUpdateSettings).toHaveBeenCalled() }) it("должен корректно работать с различными соотношениями сторон", () => { // Тестируем 4:3 mockSettings.aspectRatio = { label: "4:3", textLabel: "Standard", value: { width: 1440, height: 1080 }, } render() expect(screen.getByDisplayValue("1440")).toBeInTheDocument() expect(screen.getByDisplayValue("1080")).toBeInTheDocument() // Восстанавливаем mockSettings.aspectRatio = { label: "16:9", textLabel: "Widescreen", value: { width: 1920, height: 1080 }, } }) }) describe("Интеграционные тесты", () => { it.skip("должен правильно обрабатывать полный цикл редактирования", async () => { const user = userEvent.setup() render() // 1. Изменяем ширину const widthInput = screen.getByDisplayValue("1920") fireEvent.change(widthInput, { target: { value: "1280" } }) // 2. Разблокируем соотношение const lockButton = screen.getByTitle("dialogs.projectSettings.unlockAspectRatio") fireEvent.click(lockButton) // 3. Изменяем высоту независимо const heightInput = screen.getByDisplayValue("1080") fireEvent.change(heightInput, { target: { value: "800" } }) // 4. Сохраняем const saveButton = screen.getByText("dialogs.projectSettings.save") await user.click(saveButton) // Проверяем что все вызовы были сделаны expect(mockUpdateSettings).toHaveBeenCalledTimes(2) // width, height changes (без save) await waitFor( () => { expect(mockCloseModal).toHaveBeenCalled() }, { timeout: 150 }, ) }) it("должен сохранять корректные данные при различных состояниях", async () => { const user = userEvent.setup() // Устанавливаем разное разрешение mockSettings.resolution = "3840x2160" render() const saveButton = screen.getByText("dialogs.projectSettings.save") await user.click(saveButton) expect(mockUpdateSettings).toHaveBeenCalledWith( expect.objectContaining({ resolution: expect.any(String), aspectRatio: expect.any(Object), frameRate: expect.any(String), colorSpace: expect.any(String), }), ) // Восстанавливаем mockSettings.resolution = "1920x1080" }) }) describe("Обработка ошибок", () => { it("должен обрабатывать некорректные данные gracefully", () => { render() const widthInput = screen.getByDisplayValue("1920") // Пытаемся ввести некорректные данные fireEvent.change(widthInput, { target: { value: "invalid" } }) fireEvent.change(widthInput, { target: { value: "null" } }) fireEvent.change(widthInput, { target: { value: "undefined" } }) // updateSettings не должна вызываться с некорректными данными expect(mockUpdateSettings).not.toHaveBeenCalled() }) it("должен игнорировать очень большие значения", () => { render() const widthInput = screen.getByDisplayValue("1920") // Проверяем поведение с очень большим числом (больше максимального) fireEvent.change(widthInput, { target: { value: "99999" } }) // В зависимости от реализации, может вызываться или не вызываться // Проверим что компонент не падает expect(widthInput).toBeInTheDocument() }) it("должен обрабатывать пустые значения", () => { render() const widthInput = screen.getByDisplayValue("1920") fireEvent.change(widthInput, { target: { value: "" } }) // С пустыми значениями функция не должна вызываться expect(mockUpdateSettings).not.toHaveBeenCalled() }) }) })