import React from "react"; import "@testing-library/jest-dom"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import AiPrivacySettingsCard from "./components/AiPrivacySettingsCard"; import { I18nProvider, useI18n } from "./i18n/I18nProvider"; import { translations } from "./i18n/translations"; import { api } from "./api"; const mockToast = jest.fn(); jest.mock("./api", () => ({ api: { get: jest.fn(), put: jest.fn() } })); jest.mock("./toast", () => ({ useToast: () => ({ toast: mockToast }), useOptionalToast: () => null })); const saved = { enabled: true, externalProcessingAllowed: false, externalProcessingAvailable: true, effectiveExternalProcessing: false, provider: "local" }; function deferred() { let resolve!: (value: T) => void; let reject!: (error: Error) => void; const promise = new Promise((yes, no) => { resolve=yes; reject=no; }); return { promise, resolve, reject }; } function Locale() { const { setLanguage }=useI18n(); return ; } function mount() { return render(); } beforeEach(() => { jest.clearAllMocks(); window.localStorage.clear(); (api.get as jest.Mock).mockResolvedValue({ data: saved }); }); test("[P7-09 privacy] failed initial read has a read-only Retry and no perpetual loading", async () => { (api.get as jest.Mock).mockRejectedValueOnce(new Error("offline")); mount(); await screen.findByRole("alert"); expect(screen.queryByRole("status")).not.toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: translations.en.retry })); expect(await screen.findByRole("switch", { name: translations.en.settingsAiEnabled })).toBeChecked(); await waitFor(() => expect(screen.getByRole("group", { name: translations.en.settingsAiPrivacyTitle })).toHaveFocus()); expect(api.get).toHaveBeenCalledTimes(2); expect(api.put).not.toHaveBeenCalled(); }); test("[P7-09 privacy] submitted acknowledgement preserves a newer edit and serializes Save", async () => { const held=deferred<{ data: typeof saved }>(); (api.put as jest.Mock).mockReturnValue(held.promise); mount(); const enabled=await screen.findByRole("switch", { name: translations.en.settingsAiEnabled }); fireEvent.click(enabled); const save=screen.getByRole("button", { name: translations.en.settingsAiSave }); fireEvent.click(save); fireEvent.click(save); expect(api.put).toHaveBeenCalledTimes(1); expect(api.put).toHaveBeenCalledWith("/ai/settings", { enabled: false, externalProcessingAllowed: false }); fireEvent.click(enabled); await act(async () => held.resolve({ data: { ...saved, enabled: false } })); expect(enabled).toBeChecked(); expect(screen.getByText(translations.en.assetsUnsavedChanges)).toBeInTheDocument(); }); test("[P7-09 privacy] a failed save retains the draft and never replays through read recovery", async () => { (api.put as jest.Mock).mockRejectedValue(new Error("offline")); mount(); const enabled=await screen.findByRole("switch", { name: translations.en.settingsAiEnabled }); fireEvent.click(enabled); fireEvent.click(screen.getByRole("button", { name: translations.en.settingsAiSave })); await screen.findByRole("alert"); expect(enabled).not.toBeChecked(); expect(api.get).toHaveBeenCalledTimes(1); expect(api.put).toHaveBeenCalledTimes(1); expect(screen.getByText(translations.en.assetsUnsavedChanges)).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: translations.en.assetsDiscardChanges })); expect(enabled).toBeChecked(); expect(screen.queryByRole("alert")).not.toBeInTheDocument(); expect(api.put).toHaveBeenCalledTimes(1); }); test("[P7-09 privacy] unmount ignores an old save acknowledgement and success notification", async () => { const held=deferred<{ data: typeof saved }>(); (api.put as jest.Mock).mockReturnValue(held.promise); const view=mount(); await screen.findByRole("switch", { name: translations.en.settingsAiEnabled }); fireEvent.click(screen.getByRole("button", { name: translations.en.settingsAiSave })); view.unmount(); await act(async () => held.resolve({ data: saved })); expect(mockToast).not.toHaveBeenCalled(); }); test("[P7-09 privacy] pending save uses the current language without refetching the draft", async () => { const held=deferred<{ data: typeof saved }>(); (api.put as jest.Mock).mockReturnValue(held.promise); mount(); await screen.findByRole("switch", { name: translations.en.settingsAiEnabled }); fireEvent.click(screen.getByRole("button", { name: translations.en.settingsAiSave })); fireEvent.click(screen.getByRole("button", { name: "Bokmål" })); await screen.findByRole("switch", { name: translations.nb.settingsAiEnabled }); await act(async () => held.resolve({ data: saved })); await waitFor(() => expect(mockToast).toHaveBeenCalledWith(translations.nb.settingsAiSaved, "success")); expect(api.get).toHaveBeenCalledTimes(1); }); test("[P7-09 privacy] local-only capability permits revoking a historical external consent", async () => { (api.get as jest.Mock).mockResolvedValue({ data: { ...saved, externalProcessingAvailable: false, externalProcessingAllowed: true } }); mount(); const external=await screen.findByRole("switch", { name: translations.en.settingsAiExternalAllowed }); expect(external).toBeEnabled(); fireEvent.click(external); expect(external).not.toBeChecked(); expect(external).toBeDisabled(); expect(api.put).not.toHaveBeenCalled(); });