Files
kandev/apps/web/components/settings/system/system-confirmation-dialogs.test.tsx
T
2026-08-15 19:34:52 +01:00

169 lines
6.9 KiB
TypeScript

import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { activateLocale } from "@/lib/i18n";
import { FactoryResetDialog } from "./factory-reset-dialog";
import { RestoreDialog } from "./restore-dialog";
const restoreDialogTestState = vi.hoisted(() => ({
job: null as { state: string; message?: string } | null,
restart: {
phase: "idle" as const,
errorMessage: null as string | null,
isRestarting: false,
start: vi.fn(),
dismiss: vi.fn(),
},
}));
vi.mock("@/hooks/domains/system/use-system-jobs", () => ({
useSystemJob: () => restoreDialogTestState.job,
useSystemJobs: () => [],
}));
vi.mock("@/hooks/domains/system/use-kandev-restart", () => ({
useKandevRestart: () => restoreDialogTestState.restart,
}));
afterEach(() => {
cleanup();
restoreDialogTestState.job = null;
restoreDialogTestState.restart.start.mockReset();
restoreDialogTestState.restart.dismiss.mockReset();
});
/**
* Both dialogs gate their confirm button on `typed === CONFIRM_TOKEN` and send
* the same token to the API, and both actions are irreversible. Translating a
* token would leave a dialog the user cannot satisfy in that locale, and
* nothing would fail until a second language shipped.
*/
describe("system type-to-confirm dialogs", () => {
it("keeps the factory-reset token verbatim and gates the confirm button on it", () => {
render(<FactoryResetDialog open onOpenChange={vi.fn()} />);
// The whole reconstructed <Trans> sentence: a tag index drifting off its
// <code> child reassembles the copy into fragments without failing
// anything else in the suite.
expect(screen.getByText(/to enable the confirm button/).textContent).toBe(
"Type RESET to enable the confirm button. After the wipe completes you'll be asked to " +
"quit and relaunch Kandev - the backend does not auto-restart.",
);
const input = screen.getByTestId("system-factory-reset-input");
const confirm = screen.getByTestId("system-factory-reset-confirm") as HTMLButtonElement;
expect(input.getAttribute("placeholder")).toBe("Type RESET to confirm");
expect(confirm.disabled).toBe(true);
fireEvent.change(input, { target: { value: "reset" } });
expect(confirm.disabled).toBe(true);
fireEvent.change(input, { target: { value: "RESET" } });
expect(confirm.disabled).toBe(false);
});
it("keeps the restore token verbatim and interpolates the snapshot name", () => {
render(<RestoreDialog open onOpenChange={vi.fn()} name="kandev-20260803.db" />);
// The filename is a value; only the frame around it is copy.
expect(screen.getByText(/over the current database/).textContent).toBe(
"Restore kandev-20260803.db over the current database. After the staged copy is in place " +
"you will be asked to quit and relaunch Kandev so the new data is loaded fresh - the " +
"backend does not auto-restart.",
);
const input = screen.getByTestId("system-restore-input");
const confirm = screen.getByTestId("system-restore-confirm") as HTMLButtonElement;
expect(input.getAttribute("placeholder")).toBe("Type RESTORE to confirm");
expect(confirm.disabled).toBe(true);
fireEvent.change(input, { target: { value: "RESTORE" } });
expect(confirm.disabled).toBe(false);
});
it("requires a restart after a successful restore", () => {
restoreDialogTestState.job = { state: "succeeded" };
render(<RestoreDialog open onOpenChange={vi.fn()} name="snapshot-1.db" />);
expect(screen.getByTestId("system-restore-restart")).toBeTruthy();
expect(screen.queryByTestId("system-restore-close")).toBeNull();
fireEvent.click(screen.getByTestId("system-restore-restart"));
expect(restoreDialogTestState.restart.start).toHaveBeenCalledOnce();
});
});
/**
* The pseudo-locale oracle, at unit scale.
*
* `i18next/no-literal-string` runs in `mode: "jsx-only"`, so copy these dialogs
* build outside JSX is invisible to lint and a clean lint is not proof they are
* done. Under `pseudo` every catalog message is accented, so any word-like
* ASCII left in the dialog is a literal that never reached the catalog — except
* the confirmation token, which must stay verbatim for `typed === CONFIRM_TOKEN`
* to be satisfiable.
*
* Accessibility copy is scanned too, and that is the point: the live pseudo
* oracle only sees what is painted, so an `aria-label` is exactly the string
* that survives a migration untranslated because nothing on screen looks wrong.
*/
describe("system type-to-confirm dialogs under the pseudo-locale", () => {
const ACCENTED = /[À-ɏ]/;
const WORDLIKE = /[A-Za-z]{4,}/;
/** Attributes that carry copy, mirroring the eslint guard's include list. */
const COPY_ATTRIBUTES = ["aria-label", "aria-description", "title", "placeholder", "alt"];
function unlocalizedText(): string[] {
const dialog = document.querySelector("[role=dialog]");
if (!dialog) throw new Error("dialog did not render");
const leftovers = new Set<string>();
const consider = (value: string | null | undefined) => {
const text = (value ?? "").trim();
if (text && WORDLIKE.test(text) && !ACCENTED.test(text)) leftovers.add(text);
};
const walker = document.createTreeWalker(dialog, NodeFilter.SHOW_TEXT);
let node = walker.nextNode();
while (node) {
consider(node.textContent);
node = walker.nextNode();
}
for (const element of [dialog, ...dialog.querySelectorAll("*")]) {
for (const attribute of COPY_ATTRIBUTES) consider(element.getAttribute(attribute));
}
return [...leftovers];
}
beforeAll(async () => {
await activateLocale("pseudo");
});
afterAll(async () => {
await activateLocale("en");
});
/**
* `Close` is `@kandev/ui`'s own `DialogContent` dismiss button. Localizing it
* needs a strings-provider seam in that package (see docs/i18n.md, "Shared
* UI"), so it is expected here rather than filtered out — if the package ever
* gains that seam, this assertion is what tells us to drop it.
*/
const UI_PACKAGE_CLOSE = "Close";
it("leaves only the RESET token unaccented in the factory-reset dialog", () => {
render(<FactoryResetDialog open onOpenChange={vi.fn()} />);
// `<data-dir>/backups/` is a path placeholder and stays a value.
expect(unlocalizedText().sort()).toEqual(["<data-dir>/backups/", UI_PACKAGE_CLOSE, "RESET"]);
// Still typeable, and still announced, under a non-English locale.
expect(screen.getByTestId("system-factory-reset-input").getAttribute("aria-label")).toContain(
"RESET",
);
});
it("leaves only the RESTORE token and the snapshot name unaccented", () => {
render(<RestoreDialog open onOpenChange={vi.fn()} name="snapshot-1.db" />);
expect(unlocalizedText().sort()).toEqual([UI_PACKAGE_CLOSE, "RESTORE", "snapshot-1.db"]);
expect(screen.getByTestId("system-restore-input").getAttribute("aria-label")).toContain(
"RESTORE",
);
});
});