Files
siftlode/frontend/src/components/DiagnosticsSettings.dom.test.tsx
T
peter bd6aaa1e43 feat(design): the Copy rule — names, verbs, dates and units (E28 S3g)
The owner's picks (all the recommended options) become DESIGN.md's Copy
chapter and a sweep of both languages (742 values, no key drift):

- One name per feature: the rail's label, which may shorten it (Alerts
  -> Notifications, Fiókok -> Felhasználók, Download Center -> Downloads,
  Channel manager -> Channels); one "sync from YouTube" verb.
- Four verbs, four meanings (Delete / Remove / Dismiss / Discard, and
  their Hungarian pairs), checked against what each action does.
- Sentence case, the app's places capitalised; "…" on a command that
  asks first (a popover or form title got its own key without it);
  one-sentence messages without a stop or a tick, "Couldn't …"; curly
  quotes.
- Dates only from lib/format.ts, in the UI language: formatDateTime
  names the month and stops at the minute (it printed "9/27/2026,
  8:30:14 AM"), formatDateTimeSec for the log, the audit and the
  recorder; ui/RelativeTime pairs "3d ago" with the exact minute on
  hover. local/date-format refuses toLocale*String and Intl date
  formatters elsewhere.
- Units from Intl, short (formatUnit): runtimes, totals, the Scheduler
  countdown, the stats range, the player's tap skip. "2h 13m" was
  English in both languages.
- src/i18n/copyRule.test.ts holds the machine-readable part (quotes,
  "...", ticks, bare unit letters).
2026-09-27 05:46:17 +02:00

239 lines
10 KiB
TypeScript

// @vitest-environment jsdom
//
// E22 S5: Settings → Diagnosztika. Raising the level is a consent, so choosing Részletes or Teljes
// must change NOTHING until the consent box's own button is pressed; going back to Alap needs no
// consent. i18n is a key-passthrough (with the count, so the retention days are visible); the level
// endpoint is stubbed, and `me` lives in a real query cache so a saved level re-renders like in the app.
import { afterEach, describe, expect, it, vi } from "vitest";
import { cleanup, fireEvent, render, screen, waitFor, within } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { DiagnosticsLevel, DiagnosticsState, Me } from "../lib/api";
import { qk } from "../lib/queryKeys";
vi.mock("react-i18next", async (importOriginal) => {
const actual = await importOriginal<typeof import("react-i18next")>();
return {
...actual,
useTranslation: () => ({
t: (key: string, opts?: { count?: number; h?: number; m?: number; time?: string }) =>
opts?.count != null
? `${key}:${opts.count}`
: opts?.m != null
? `${key}:${opts.h ?? 0}h${opts.m}m`
: opts?.time != null
? `${key}:${opts.time}`
: key,
i18n: { language: "en" },
}),
};
});
const setLevel = vi.fn(
(level: DiagnosticsLevel): Promise<{ ok: true; diagnostics: DiagnosticsState }> =>
Promise.resolve({
ok: true,
diagnostics: {
level,
until: level === "INFO" ? null : new Date(Date.now() + 86_400_000).toISOString(),
retention_days: 7,
session_minutes: 60,
},
}),
);
vi.mock("../lib/api", async (importOriginal) => {
const actual = await importOriginal<typeof import("../lib/api")>();
return {
...actual,
api: { ...actual.api, setDiagnosticsLevel: (l: DiagnosticsLevel) => setLevel(l) },
};
});
import DiagnosticsSettings from "./DiagnosticsSettings";
afterEach(() => {
cleanup();
vi.clearAllMocks();
});
function mount(me: Partial<Me>) {
const qc = new QueryClient();
qc.setQueryData(qk.me(), {
id: 1,
is_demo: false,
preferences: {},
diagnostics: { level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
...me,
});
render(
<QueryClientProvider client={qc}>
<DiagnosticsSettings />
</QueryClientProvider>,
);
return qc;
}
const levelButton = (name: string) =>
within(screen.getByRole("group", { name: "settings.diagnostics.level" })).getByRole("button", {
name,
});
describe("DiagnosticsSettings", () => {
it("choosing a detailed level asks first and changes nothing until confirmed", async () => {
mount({});
fireEvent.click(levelButton("settings.diagnostics.levels.DEBUG"));
const consent = screen.getByTestId("diagnostics-consent");
expect(consent.textContent).toContain("settings.diagnostics.consent.what.DEBUG");
expect(consent.textContent).toContain("settings.diagnostics.consent.server:7");
expect(setLevel).not.toHaveBeenCalled();
fireEvent.click(
within(consent).getByRole("button", { name: "settings.diagnostics.consent.cancel" }),
);
expect(screen.queryByTestId("diagnostics-consent")).toBeNull();
expect(levelButton("settings.diagnostics.levels.INFO").getAttribute("aria-pressed")).toBe(
"true",
);
expect(setLevel).not.toHaveBeenCalled();
});
it("the consent button raises the level, and the status line offers the way back", async () => {
mount({});
fireEvent.click(levelButton("settings.diagnostics.levels.TRACE"));
fireEvent.click(screen.getByTestId("diagnostics-consent-confirm"));
await waitFor(() => expect(screen.getByTestId("diagnostics-status")).toBeTruthy());
expect(setLevel).toHaveBeenCalledWith("TRACE");
expect(screen.queryByTestId("diagnostics-consent")).toBeNull();
expect(levelButton("settings.diagnostics.levels.TRACE").getAttribute("aria-pressed")).toBe(
"true",
);
fireEvent.click(screen.getByRole("button", { name: "settings.diagnostics.revert" }));
await waitFor(() => expect(screen.queryByTestId("diagnostics-status")).toBeNull());
expect(setLevel).toHaveBeenLastCalledWith("INFO");
});
it("counts the 24 hours from the moment the level was raised, not from when the page opened", async () => {
vi.useFakeTimers({ toFake: ["Date"] });
try {
vi.setSystemTime(new Date("2026-09-19T02:54:00Z"));
mount({});
vi.setSystemTime(new Date("2026-09-19T02:56:30Z")); // the page sat open for 2.5 minutes
fireEvent.click(levelButton("settings.diagnostics.levels.DEBUG"));
fireEvent.click(screen.getByTestId("diagnostics-consent-confirm"));
await waitFor(() => expect(screen.getByTestId("diagnostics-status")).toBeTruthy());
expect(screen.getByTestId("diagnostics-status").textContent).toContain("24 hr 0 min");
} finally {
vi.useRealTimers();
}
});
it("going back to the base level needs no consent", async () => {
const until = new Date(Date.now() + 3_600_000).toISOString();
mount({ diagnostics: { level: "DEBUG", until, retention_days: 7, session_minutes: 60 } });
fireEvent.click(levelButton("settings.diagnostics.levels.INFO"));
expect(screen.queryByTestId("diagnostics-consent")).toBeNull();
await waitFor(() => expect(setLevel).toHaveBeenCalledWith("INFO"));
});
it("the focus goes back to the level picker when the box it was in goes away", async () => {
mount({});
fireEvent.click(levelButton("settings.diagnostics.levels.DEBUG"));
const cancel = screen.getByRole("button", { name: "settings.diagnostics.consent.cancel" });
cancel.focus();
fireEvent.click(cancel);
expect(document.activeElement).toBe(levelButton("settings.diagnostics.levels.INFO"));
fireEvent.click(levelButton("settings.diagnostics.levels.TRACE"));
const confirm = screen.getByTestId("diagnostics-consent-confirm");
confirm.focus();
fireEvent.click(confirm);
await waitFor(() => expect(screen.getByTestId("diagnostics-status")).toBeTruthy());
expect(document.activeElement).toBe(levelButton("settings.diagnostics.levels.TRACE"));
const revert = screen.getByRole("button", { name: "settings.diagnostics.revert" });
revert.focus();
fireEvent.click(revert);
await waitFor(() => expect(screen.queryByTestId("diagnostics-status")).toBeNull());
expect(document.activeElement).toBe(levelButton("settings.diagnostics.levels.INFO"));
});
it("the watch outlives a render in which the element it is watching is still there", async () => {
// The distinguishing property of the restore, and the one the browser found: it keeps looking
// until the element is GONE, instead of spending itself on the first render after the change.
// Chrome commits a saved level as two renders and jsdom as one, so this drives the general
// case directly — the user moved the focus somewhere else while the request was out, and that
// somewhere else disappears later, several renders on.
vi.useFakeTimers();
try {
// Raised, so the status line's countdown is mounted and its 30 s tick keeps re-rendering.
const until = new Date(Date.now() + 3_600_000).toISOString();
mount({ diagnostics: { level: "DEBUG", until, retention_days: 7, session_minutes: 60 } });
fireEvent.click(levelButton("settings.diagnostics.levels.TRACE"));
const elsewhere = document.createElement("button");
document.body.append(elsewhere);
elsewhere.focus();
// Cancel closes the box; the watch is armed on `elsewhere`, which is still in the page.
fireEvent.click(screen.getByRole("button", { name: "settings.diagnostics.consent.cancel" }));
expect(document.activeElement).toBe(elsewhere); // their place is kept, nothing is stolen
// Renders keep coming (the countdown ticks) while it is still there, and must not spend it.
await vi.advanceTimersByTimeAsync(60_000);
expect(document.activeElement).toBe(elsewhere);
elsewhere.remove(); // now the focus really has fallen to <body>
expect(document.activeElement).toBe(document.body);
await vi.advanceTimersByTimeAsync(30_000);
expect(document.activeElement).toBe(levelButton("settings.diagnostics.levels.DEBUG"));
} finally {
vi.useRealTimers();
}
});
it("the ticking status line is not a live region", async () => {
const until = new Date(Date.now() + 3_600_000).toISOString();
mount({ diagnostics: { level: "DEBUG", until, retention_days: 7, session_minutes: 60 } });
const status = screen.getByTestId("diagnostics-status");
expect(status.closest('[role="status"], [aria-live]')).toBeNull();
});
it("a `me` fetch that left before the change cannot put the old level back", async () => {
const qc = mount({});
let answerStale: (me: unknown) => void = () => {};
const stale = qc
.fetchQuery({
queryKey: qk.me(),
queryFn: () => new Promise((resolve) => (answerStale = resolve)),
})
.catch(() => null);
fireEvent.click(levelButton("settings.diagnostics.levels.TRACE"));
fireEvent.click(screen.getByTestId("diagnostics-consent-confirm"));
await waitFor(() => expect(screen.getByTestId("diagnostics-status")).toBeTruthy());
answerStale({
id: 1,
is_demo: false,
preferences: {},
diagnostics: { level: "INFO", until: null, retention_days: 7, session_minutes: 60 },
});
await stale;
expect(qc.getQueryData<Me>(qk.me())?.diagnostics.level).toBe("TRACE");
expect(screen.getByTestId("diagnostics-status")).toBeTruthy();
});
it("a lapsed level shows as the base level", () => {
const until = new Date(Date.now() - 60_000).toISOString();
mount({ diagnostics: { level: "TRACE", until, retention_days: 7, session_minutes: 60 } });
expect(levelButton("settings.diagnostics.levels.INFO").getAttribute("aria-pressed")).toBe(
"true",
);
expect(screen.queryByTestId("diagnostics-status")).toBeNull();
});
it("the shared demo account gets an explanation instead of the picker", () => {
mount({ is_demo: true });
expect(screen.queryByRole("group", { name: "settings.diagnostics.level" })).toBeNull();
expect(screen.getByText("settings.diagnostics.demo")).toBeTruthy();
});
});