Files
jobtrackingapp/job-tracker-ui/src/ai-privacy-settings.test.tsx
T

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();
});