71 lines
5.4 KiB
TypeScript
71 lines
5.4 KiB
TypeScript
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<T>() { let resolve!: (value: T) => void; let reject!: (error: Error) => void; const promise = new Promise<T>((yes, no) => { resolve=yes; reject=no; }); return { promise, resolve, reject }; }
|
|
function Locale() { const { setLanguage }=useI18n(); return <button onClick={() => void setLanguage("nb")}>Bokmål</button>; }
|
|
function mount() { return render(<I18nProvider><Locale /><AiPrivacySettingsCard /></I18nProvider>); }
|
|
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();
|
|
});
|