import { act, fireEvent, render, screen } from "@testing-library/react" import { beforeEach, describe, expect, it, vi } from "vitest" import { useLanguage } from "@/features/language" import { useModal } from "@/features/modals/services/modal-provider" import { UserSettingsModal } from "../../components/user-settings-modal" import { useApiKeys } from "../../hooks/use-api-keys" import { useUserSettings } from "../../hooks/use-user-settings" // Мокаем Tauri API vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn().mockImplementation((cmd) => { if (cmd === "select_directory") { return Promise.resolve("selected/directory/path") } return Promise.resolve(null) }), })) // Мокаем Tauri Dialog Plugin vi.mock("@tauri-apps/plugin-dialog", () => ({ open: vi.fn().mockImplementation((options) => { if (options?.directory) { return Promise.resolve("selected/directory/path") } return Promise.resolve(null) }), })) // Мокаем хуки vi.mock("@/features/language") vi.mock("../../hooks/use-user-settings") vi.mock("../../hooks/use-api-keys") vi.mock("@/features/modals/services/modal-provider") vi.mock("react-i18next", () => ({ useTranslation: () => ({ t: (key: string) => key, }), })) // Мокаем console.log и console.error vi.spyOn(console, "log").mockImplementation(() => {}) vi.spyOn(console, "error").mockImplementation(() => {}) // Создаем функцию для получения моковых данных с переопределениями const getMockUserSettings = (overrides = {}) => ({ screenshotsPath: "public/screenshots", playerScreenshotsPath: "public/media", openAiApiKey: "", claudeApiKey: "", isBrowserVisible: true, isTimelineVisible: true, isOptionsVisible: true, activeTab: "media" as const, layoutMode: "default" as const, playerVolume: 100, handleScreenshotsPathChange: vi.fn(), handleAiApiKeyChange: vi.fn(), handleClaudeApiKeyChange: vi.fn(), handlePlayerScreenshotsPathChange: vi.fn(), handleTabChange: vi.fn(), handleLayoutChange: vi.fn(), toggleBrowserVisibility: vi.fn(), handlePlayerVolumeChange: vi.fn(), toggleTimelineVisibility: vi.fn(), toggleOptionsVisibility: vi.fn(), ...overrides, }) describe("UserSettingsModal", () => { const mockHandleScreenshotsPathChange = vi.fn() const mockHandleAiApiKeyChange = vi.fn() const mockHandleClaudeApiKeyChange = vi.fn() const mockHandlePlayerScreenshotsPathChange = vi.fn() const mockHandlePlayerVolumeChange = vi.fn() const mockToggleTimelineVisibility = vi.fn() const mockToggleOptionsVisibility = vi.fn() const mockChangeLanguage = vi.fn() const mockCloseModal = vi.fn() beforeEach(() => { // Очищаем моки перед каждым тестом vi.clearAllMocks() // Устанавливаем моки по умолчанию vi.mocked(useUserSettings).mockImplementation(() => ({ screenshotsPath: "public/screenshots", playerScreenshotsPath: "public/media", openAiApiKey: "", claudeApiKey: "", isBrowserVisible: true, isTimelineVisible: true, isOptionsVisible: true, activeTab: "media", layoutMode: "default", playerVolume: 100, handleScreenshotsPathChange: mockHandleScreenshotsPathChange, handleAiApiKeyChange: mockHandleAiApiKeyChange, handleClaudeApiKeyChange: mockHandleClaudeApiKeyChange, handlePlayerScreenshotsPathChange: mockHandlePlayerScreenshotsPathChange, handleTabChange: vi.fn(), handleLayoutChange: vi.fn(), toggleBrowserVisibility: vi.fn(), handlePlayerVolumeChange: mockHandlePlayerVolumeChange, toggleTimelineVisibility: mockToggleTimelineVisibility, toggleOptionsVisibility: mockToggleOptionsVisibility, })) vi.mocked(useLanguage).mockImplementation(() => ({ currentLanguage: "ru", changeLanguage: mockChangeLanguage, systemLanguage: "ru", isLoading: false, error: null, refreshLanguage: vi.fn(), })) vi.mocked(useModal).mockImplementation(() => ({ openModal: vi.fn(), closeModal: mockCloseModal, modalType: null, modalData: null, isOpen: false, submitModal: vi.fn(), })) vi.mocked(useApiKeys).mockImplementation(() => ({ getApiKeyStatus: vi.fn().mockReturnValue("not_set"), updateApiKeyStatus: vi.fn(), testApiKey: vi.fn(), initiateOAuth: vi.fn(), youtubeCredentials: { clientId: "", clientSecret: "" }, updateYoutubeCredentials: vi.fn(), tiktokCredentials: { clientId: "", clientSecret: "" }, updateTiktokCredentials: vi.fn(), vimeoCredentials: { clientId: "", clientSecret: "", accessToken: "" }, updateVimeoCredentials: vi.fn(), telegramCredentials: { botToken: "", chatId: "" }, updateTelegramCredentials: vi.fn(), codecovToken: "", updateCodecovToken: vi.fn(), tauriAnalyticsKey: "", updateTauriAnalyticsKey: vi.fn(), })) }) it("should render correctly", () => { render() // Проверяем, что компонент отрендерился с табами expect(screen.getByText("dialogs.userSettings.tabs.general")).toBeInTheDocument() expect(screen.getByText("dialogs.userSettings.tabs.aiServices")).toBeInTheDocument() expect(screen.getByText("dialogs.userSettings.tabs.socialNetworks")).toBeInTheDocument() // Проверяем общие настройки на активной вкладке "General" expect(screen.getByText("dialogs.userSettings.interfaceLanguage")).toBeInTheDocument() expect(screen.getByText("dialogs.userSettings.screenshotsPath")).toBeInTheDocument() expect(screen.getByText("dialogs.userSettings.cancel")).toBeInTheDocument() expect(screen.getByText("dialogs.userSettings.save")).toBeInTheDocument() }) it("should handle language change", () => { render() // Проверяем, что селект языка отображается expect(screen.getByText("dialogs.userSettings.interfaceLanguage")).toBeInTheDocument() // Симулируем выбор языка, вызывая напрямую функцию changeLanguage void vi.mocked(useLanguage)().changeLanguage("en") // Проверяем, что changeLanguage был вызван expect(mockChangeLanguage).toHaveBeenCalledWith("en") }) it("should handle screenshots path change", () => { render() // Находим инпут пути скриншотов const screenshotsPathInput = screen.getByPlaceholderText("public/screenshots") // Вводим новый путь act(() => { act(() => { fireEvent.change(screenshotsPathInput, { target: { value: "new/path" } }) }) }) // Нажимаем кнопку "Сохранить" const saveButton = screen.getByText("dialogs.userSettings.save") act(() => { act(() => { fireEvent.click(saveButton) }) }) // Проверяем, что handleScreenshotsPathChange был вызван с правильными параметрами expect(mockHandleScreenshotsPathChange).toHaveBeenCalledWith("new/path") }) it("should render tabs correctly", () => { render() // Проверяем, что табы отображаются expect(screen.getByText("dialogs.userSettings.tabs.general")).toBeInTheDocument() expect(screen.getByText("dialogs.userSettings.tabs.aiServices")).toBeInTheDocument() expect(screen.getByText("dialogs.userSettings.tabs.socialNetworks")).toBeInTheDocument() // General tab активен по умолчанию, проверяем его содержимое expect(screen.getByText("dialogs.userSettings.interfaceLanguage")).toBeInTheDocument() }) it("should handle cancel button click", () => { render() // Нажимаем кнопку "Отмена" const cancelButton = screen.getByText("dialogs.userSettings.cancel") act(() => { act(() => { fireEvent.click(cancelButton) }) }) // Проверяем, что closeModal был вызван expect(mockCloseModal).toHaveBeenCalled() }) it("should clear screenshots path when X button is clicked", () => { // Переопределяем значение screenshotsPath для этого теста vi.mocked(useUserSettings).mockImplementation(() => getMockUserSettings({ screenshotsPath: "custom/path", handleScreenshotsPathChange: mockHandleScreenshotsPathChange, handleAiApiKeyChange: mockHandleAiApiKeyChange, }), ) render() // Находим кнопку X рядом с инпутом пути скриншотов const clearButton = screen.getByTitle("dialogs.userSettings.clearPath") // Кликаем по кнопке X act(() => { act(() => { fireEvent.click(clearButton) }) }) // Проверяем, что handleScreenshotsPathChange был вызван с правильным значением expect(mockHandleScreenshotsPathChange).toHaveBeenCalledWith("public/screenshots") }) it("should update state when language changes in context", () => { // Рендерим компонент render() // Проверяем, что начальное значение языка установлено правильно const selectValue = screen.getByRole("combobox") expect(selectValue).toHaveTextContent("ru") // Изменяем язык в контексте vi.mocked(useLanguage).mockImplementation(() => ({ currentLanguage: "en", changeLanguage: mockChangeLanguage, systemLanguage: "ru", isLoading: false, error: null, refreshLanguage: vi.fn(), })) // Перерендериваем компонент render() // Проверяем, что значение языка обновилось const updatedSelectValue = screen.getAllByRole("combobox")[1] expect(updatedSelectValue).toHaveTextContent("en") }) it("should update state when screenshotsPath changes in context", () => { // Рендерим компонент render() // Проверяем, что начальное значение пути скриншотов установлено правильно const screenshotsPathInput = screen.getByPlaceholderText("public/screenshots") expect(screenshotsPathInput).toHaveValue("public/screenshots") // Изменяем путь скриншотов в контексте vi.mocked(useUserSettings).mockImplementation(() => getMockUserSettings({ screenshotsPath: "new/path", handleScreenshotsPathChange: mockHandleScreenshotsPathChange, handleAiApiKeyChange: mockHandleAiApiKeyChange, }), ) // Перерендериваем компонент render() // Проверяем, что значение пути скриншотов обновилось const updatedScreenshotsPathInput = screen.getAllByPlaceholderText("public/screenshots")[1] expect(updatedScreenshotsPathInput).toHaveValue("new/path") }) it("should handle folder selection button click using Tauri Dialog Plugin", async () => { // Получаем мок функции open из плагина dialog const { open } = await import("@tauri-apps/plugin-dialog") const mockOpen = open as unknown as ReturnType // Очищаем историю вызовов мока mockOpen.mockClear() // Рендерим компонент const { rerender } = render() // Находим кнопки выбора папки (у нас их две - для screenshotsPath и playerScreenshotsPath) const folderButtons = screen.getAllByRole("button", { name: /folder/i, }) // Берем первую кнопку (для screenshotsPath) const folderButton = folderButtons[0] // Кликаем по кнопке act(() => { act(() => { fireEvent.click(folderButton) }) }) // Ждем, пока асинхронные операции завершатся await vi.waitFor(() => { // Проверяем, что open был вызван с правильными параметрами expect(mockOpen).toHaveBeenCalledWith( expect.objectContaining({ directory: true, multiple: false, title: "dialogs.userSettings.selectFolder", }), ) }) // Имитируем обновление состояния после выбора директории vi.mocked(useUserSettings).mockImplementation(() => ({ screenshotsPath: "selected/directory/path", playerScreenshotsPath: "public/media", openAiApiKey: "", claudeApiKey: "", isBrowserVisible: true, activeTab: "media", layoutMode: "default", handleScreenshotsPathChange: mockHandleScreenshotsPathChange, handleAiApiKeyChange: mockHandleAiApiKeyChange, handleClaudeApiKeyChange: vi.fn(), handlePlayerScreenshotsPathChange: vi.fn(), handleTabChange: vi.fn(), handleLayoutChange: vi.fn(), toggleBrowserVisibility: vi.fn(), })) // Перерендериваем компонент act(() => { rerender() }) // Проверяем, что путь скриншотов был обновлен const screenshotsPathInput = screen.getByPlaceholderText("public/screenshots") expect(screenshotsPathInput).toHaveValue("selected/directory/path") }) it("should handle player screenshots path selection", async () => { // Получаем мок функции open из плагина dialog const { open } = await import("@tauri-apps/plugin-dialog") const mockOpen = open as unknown as ReturnType // Очищаем историю вызовов мока mockOpen.mockClear() // Рендерим компонент const { rerender } = render() // Находим кнопки выбора папки (у нас их две - для screenshotsPath и playerScreenshotsPath) const folderButtons = screen.getAllByRole("button", { name: /folder/i, }) // Берем вторую кнопку (для playerScreenshotsPath) const folderButton = folderButtons[1] // Кликаем по кнопке act(() => { act(() => { fireEvent.click(folderButton) }) }) // Ждем, пока асинхронные операции завершатся await vi.waitFor(() => { // Проверяем, что open был вызван с правильными параметрами expect(mockOpen).toHaveBeenCalledWith( expect.objectContaining({ directory: true, multiple: false, title: "dialogs.userSettings.selectFolder", }), ) }) // Имитируем обновление состояния после выбора директории vi.mocked(useUserSettings).mockImplementation(() => ({ screenshotsPath: "public/screenshots", playerScreenshotsPath: "selected/directory/path", openAiApiKey: "", claudeApiKey: "", isBrowserVisible: true, activeTab: "media", layoutMode: "default", handleScreenshotsPathChange: mockHandleScreenshotsPathChange, handleAiApiKeyChange: mockHandleAiApiKeyChange, handleClaudeApiKeyChange: vi.fn(), handlePlayerScreenshotsPathChange: vi.fn(), handleTabChange: vi.fn(), handleLayoutChange: vi.fn(), toggleBrowserVisibility: vi.fn(), })) // Перерендериваем компонент act(() => { rerender() }) // Проверяем, что путь скриншотов плеера был обновлен const playerScreenshotsPathInput = screen.getByPlaceholderText("public/media") expect(playerScreenshotsPathInput).toHaveValue("selected/directory/path") }) it("should not update path when folder selection is cancelled", async () => { // Получаем мок функции open из плагина dialog и настраиваем его, чтобы он возвращал null const { open } = await import("@tauri-apps/plugin-dialog") const mockOpen = open as unknown as ReturnType mockOpen.mockClear() mockOpen.mockResolvedValue(null) // Рендерим компонент render() // Находим кнопки выбора папки (у нас их две - для screenshotsPath и playerScreenshotsPath) const folderButtons = screen.getAllByRole("button", { name: /folder/i, }) // Берем первую кнопку (для screenshotsPath) const folderButton = folderButtons[0] // Кликаем по кнопке act(() => { act(() => { fireEvent.click(folderButton) }) }) // Ждем, пока асинхронные операции завершатся await vi.waitFor(() => { // Проверяем, что open был вызван с правильными параметрами expect(mockOpen).toHaveBeenCalledWith( expect.objectContaining({ directory: true, multiple: false, title: "dialogs.userSettings.selectFolder", }), ) }) // Проверяем, что путь скриншотов не изменился const screenshotsPathInput = screen.getByPlaceholderText("public/screenshots") expect(screenshotsPathInput).toHaveValue("public/screenshots") }) it("should handle error when folder selection fails", async () => { // Получаем мок функции open из плагина dialog и настраиваем его, чтобы он выбрасывал ошибку const { open } = await import("@tauri-apps/plugin-dialog") const mockOpen = open as unknown as ReturnType mockOpen.mockClear() mockOpen.mockRejectedValue(new Error("Permission denied")) // Мокаем window.prompt const mockPrompt = vi.spyOn(window, "prompt").mockReturnValue("custom/path") // Рендерим компонент render() // Находим кнопки выбора папки const folderButtons = screen.getAllByRole("button", { name: /folder/i, }) // Берем первую кнопку (для screenshotsPath) const folderButton = folderButtons[0] // Кликаем по кнопке act(() => { act(() => { fireEvent.click(folderButton) }) }) // Ждем, пока асинхронные операции завершатся await vi.waitFor(() => { // Проверяем, что open был вызван expect(mockOpen).toHaveBeenCalled() // Проверяем, что был показан prompt expect(mockPrompt).toHaveBeenCalledWith("dialogs.userSettings.selectFolderPrompt", expect.any(String)) }) // Восстанавливаем window.prompt mockPrompt.mockRestore() }) it("should handle player screenshots path change input", () => { render() // Находим инпут пути скриншотов плеера const playerScreenshotsPathInput = screen.getByPlaceholderText("public/media") // Вводим новый путь act(() => { act(() => { fireEvent.change(playerScreenshotsPathInput, { target: { value: "new/player/path" } }) }) }) // Нажимаем кнопку "Сохранить" const saveButton = screen.getByText("dialogs.userSettings.save") act(() => { act(() => { fireEvent.click(saveButton) }) }) // Проверяем, что handlePlayerScreenshotsPathChange был вызван с правильными параметрами const mockHandlePlayerScreenshotsPathChange = vi.mocked(useUserSettings)().handlePlayerScreenshotsPathChange expect(mockHandlePlayerScreenshotsPathChange).toHaveBeenCalledWith("new/player/path") }) it("should clear player screenshots path when X button is clicked", () => { // Переопределяем значение playerScreenshotsPath для этого теста const mockHandlePlayerScreenshotsPathChangeFn = vi.fn() vi.mocked(useUserSettings).mockImplementation(() => getMockUserSettings({ playerScreenshotsPath: "custom/player/path", screenshotsPath: "public/screenshots", // Keep default value handleScreenshotsPathChange: mockHandleScreenshotsPathChange, handleAiApiKeyChange: mockHandleAiApiKeyChange, handlePlayerScreenshotsPathChange: mockHandlePlayerScreenshotsPathChangeFn, }), ) render() // Находим кнопку X рядом с инпутом пути скриншотов плеера // Since the player screenshots path is not default, there should be a clear button for it const clearButtons = screen.getAllByTitle("dialogs.userSettings.clearPath") // Only player screenshots path has non-default value, so it should be the only clear button const clearPlayerPathButton = clearButtons[0] // Кликаем по кнопке X act(() => { act(() => { fireEvent.click(clearPlayerPathButton) }) }) // Проверяем, что handlePlayerScreenshotsPathChange был вызван с правильным значением expect(mockHandlePlayerScreenshotsPathChangeFn).toHaveBeenCalledWith("public/media") }) it("should open cache statistics modal when button is clicked", () => { const mockOpenModal = vi.fn() vi.mocked(useModal).mockImplementation(() => ({ openModal: mockOpenModal, closeModal: mockCloseModal, modalType: null, modalData: null, isOpen: false, submitModal: vi.fn(), })) render() // Находим кнопку статистики кэша const cacheStatsButton = screen.getByText("dialogs.userSettings.cacheStats") // Кликаем по кнопке act(() => { fireEvent.click(cacheStatsButton) }) // Проверяем, что openModal был вызван с правильными параметрами expect(mockOpenModal).toHaveBeenCalledWith("cache-statistics", { returnTo: "user-settings" }) }) it("should open cache settings modal when button is clicked", () => { const mockOpenModal = vi.fn() vi.mocked(useModal).mockImplementation(() => ({ openModal: mockOpenModal, closeModal: mockCloseModal, modalType: null, modalData: null, isOpen: false, submitModal: vi.fn(), })) render() // Находим кнопку настроек кэша const cacheSettingsButton = screen.getByText("dialogs.userSettings.cacheSettings") // Кликаем по кнопке act(() => { fireEvent.click(cacheSettingsButton) }) // Проверяем, что openModal был вызван с правильными параметрами expect(mockOpenModal).toHaveBeenCalledWith("cache-settings", { returnTo: "user-settings" }) }) })