import { render, screen, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import userEvent from "@testing-library/user-event"; import AppSettingsScreen from "#/routes/app-settings"; import OpenHands from "#/api/open-hands"; import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers"; import { AvailableLanguages } from "#/i18n"; import * as CaptureConsent from "#/utils/handle-capture-consent"; import * as ToastHandlers from "#/utils/custom-toast-handlers"; const renderAppSettingsScreen = () => render(, { wrapper: ({ children }) => ( {children} ), }); describe("Content", () => { it("should render the screen", () => { renderAppSettingsScreen(); screen.getByTestId("app-settings-screen"); }); it("should render the correct default values", async () => { const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, language: "no", user_consents_to_analytics: true, enable_sound_notifications: true, }); renderAppSettingsScreen(); await waitFor(() => { const language = screen.getByTestId("language-input"); const analytics = screen.getByTestId("enable-analytics-switch"); const sound = screen.getByTestId("enable-sound-notifications-switch"); expect(language).toHaveValue("Norsk"); expect(analytics).toBeChecked(); expect(sound).toBeChecked(); }); }); it("should render the language options", async () => { renderAppSettingsScreen(); const language = await screen.findByTestId("language-input"); await userEvent.click(language); AvailableLanguages.forEach((lang) => { const option = screen.getByText(lang.label); expect(option).toBeInTheDocument(); }); }); }); describe("Form submission", () => { afterEach(() => { vi.clearAllMocks(); }); it("should submit the form with the correct values", async () => { const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings"); const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS); renderAppSettingsScreen(); const language = await screen.findByTestId("language-input"); const analytics = await screen.findByTestId("enable-analytics-switch"); const sound = await screen.findByTestId( "enable-sound-notifications-switch", ); expect(language).toHaveValue("English"); expect(analytics).not.toBeChecked(); expect(sound).not.toBeChecked(); // change language await userEvent.click(language); const norsk = screen.getByText("Norsk"); await userEvent.click(norsk); expect(language).toHaveValue("Norsk"); // toggle options await userEvent.click(analytics); expect(analytics).toBeChecked(); await userEvent.click(sound); expect(sound).toBeChecked(); // submit the form const submit = await screen.findByTestId("submit-button"); await userEvent.click(submit); expect(saveSettingsSpy).toHaveBeenCalledWith( expect.objectContaining({ language: "no", user_consents_to_analytics: true, enable_sound_notifications: true, }), ); }); it("should only enable the submit button when there are changes", async () => { const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS); renderAppSettingsScreen(); const submit = await screen.findByTestId("submit-button"); expect(submit).toBeDisabled(); // Language check const language = await screen.findByTestId("language-input"); await userEvent.click(language); const norsk = screen.getByText("Norsk"); await userEvent.click(norsk); expect(submit).not.toBeDisabled(); await userEvent.click(language); const english = screen.getByText("English"); await userEvent.click(english); expect(submit).toBeDisabled(); // Analytics check const analytics = await screen.findByTestId("enable-analytics-switch"); await userEvent.click(analytics); expect(submit).not.toBeDisabled(); await userEvent.click(analytics); expect(submit).toBeDisabled(); // Sound check const sound = await screen.findByTestId( "enable-sound-notifications-switch", ); await userEvent.click(sound); expect(submit).not.toBeDisabled(); await userEvent.click(sound); expect(submit).toBeDisabled(); }); it("should call handleCaptureConsents with true when the analytics switch is toggled", async () => { const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS); const handleCaptureConsentsSpy = vi.spyOn( CaptureConsent, "handleCaptureConsent", ); renderAppSettingsScreen(); const analytics = await screen.findByTestId("enable-analytics-switch"); const submit = await screen.findByTestId("submit-button"); await userEvent.click(analytics); await userEvent.click(submit); await waitFor(() => expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(true), ); }); it("should call handleCaptureConsents with false when the analytics switch is toggled", async () => { const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); getSettingsSpy.mockResolvedValue({ ...MOCK_DEFAULT_USER_SETTINGS, user_consents_to_analytics: true, }); const handleCaptureConsentsSpy = vi.spyOn( CaptureConsent, "handleCaptureConsent", ); renderAppSettingsScreen(); const analytics = await screen.findByTestId("enable-analytics-switch"); const submit = await screen.findByTestId("submit-button"); await userEvent.click(analytics); await userEvent.click(submit); await waitFor(() => expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(false), ); }); // flaky test it.skip("should disable the button when submitting changes", async () => { renderAppSettingsScreen(); const submit = await screen.findByTestId("submit-button"); expect(submit).toBeDisabled(); const sound = await screen.findByTestId( "enable-sound-notifications-switch", ); await userEvent.click(sound); expect(submit).not.toBeDisabled(); // submit the form await userEvent.click(submit); expect(submit).toHaveTextContent("Saving..."); expect(submit).toBeDisabled(); await waitFor(() => expect(submit).toHaveTextContent("Save")); }); it("should disable the button after submitting changes", async () => { const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings"); const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS); renderAppSettingsScreen(); const submit = await screen.findByTestId("submit-button"); expect(submit).toBeDisabled(); const sound = await screen.findByTestId( "enable-sound-notifications-switch", ); await userEvent.click(sound); expect(submit).not.toBeDisabled(); // submit the form await userEvent.click(submit); expect(saveSettingsSpy).toHaveBeenCalled(); await waitFor(() => expect(submit).toBeDisabled()); }); }); describe("Status toasts", () => { it("should call displaySuccessToast when the settings are saved", async () => { const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings"); const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS); const displaySuccessToastSpy = vi.spyOn( ToastHandlers, "displaySuccessToast", ); renderAppSettingsScreen(); // Toggle setting to change const sound = await screen.findByTestId( "enable-sound-notifications-switch", ); await userEvent.click(sound); const submit = await screen.findByTestId("submit-button"); await userEvent.click(submit); expect(saveSettingsSpy).toHaveBeenCalled(); await waitFor(() => expect(displaySuccessToastSpy).toHaveBeenCalled()); }); it("should call displayErrorToast when the settings fail to save", async () => { const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings"); const getSettingsSpy = vi.spyOn(OpenHands, "getSettings"); getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS); const displayErrorToastSpy = vi.spyOn(ToastHandlers, "displayErrorToast"); saveSettingsSpy.mockRejectedValue(new Error("Failed to save settings")); renderAppSettingsScreen(); // Toggle setting to change const sound = await screen.findByTestId( "enable-sound-notifications-switch", ); await userEvent.click(sound); const submit = await screen.findByTestId("submit-button"); await userEvent.click(submit); expect(saveSettingsSpy).toHaveBeenCalled(); expect(displayErrorToastSpy).toHaveBeenCalled(); }); });