Files
kandev/apps/web/components/settings/system/system-route-copy.test.ts
T
2026-09-04 00:37:41 +00:00

167 lines
6.1 KiB
TypeScript

import { cleanup, render, screen } from "@testing-library/react";
import { createElement, type ReactNode } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { t } from "@/lib/i18n";
import { DataLogsSettings } from "./data-logs-settings";
import { BACKUP_SQL_COMMAND } from "./system-route-shell";
const databaseState = vi.hoisted(() => ({ value: null as unknown }));
vi.mock("@/components/state-provider", () => ({
useAppStore: (selector: (state: { system: { database: unknown } }) => unknown) =>
selector({ system: { database: databaseState.value } }),
}));
vi.mock("@/components/settings/settings-target", () => ({
SettingsTarget: ({ children }: { children?: ReactNode }) => children ?? null,
}));
vi.mock("./backups-table", () => ({ BackupsTable: () => null }));
vi.mock("./database-stats-card", () => ({ DatabaseStatsCard: () => null }));
vi.mock("./log-viewer", () => ({ LogViewer: () => null }));
afterEach(() => {
cleanup();
databaseState.value = null;
});
/**
* Byte-for-byte English for the nine System route headers, as `SETTINGS_ROUTES`
* in `src/settings-routes.tsx` rendered them before this migration.
*
* This exists because the copy was duplicated. Each of these routes had an
* unreferenced `app/settings/system/<route>/page.tsx` twin, and two of the
* twins had already drifted from the live route table — `logs` was the worse
* one, because the dead page rendered `settings:logsPageDescription`
* ("Download a bounded diagnostic ZIP containing frontend and backend logs.")
* while the live route rendered "Create a diagnostic ZIP with frontend and
* backend logs.". Pointing the live route at the existing key silently changed
* user-visible English; only `logs-page.spec.ts` pins that sentence, so the
* other eight routes had no check at all.
*
* An i18n migration must not change copy. This table is the check that says so
* for all nine, not just the one route that happened to have an e2e assertion.
*/
const ROUTE_COPY: Array<{ route: string; titleKey: string; title: string; description: string }> = [
{
route: "status",
titleKey: "common:status",
title: "Status",
description: "Health checks, disk usage, and version summary.",
},
{
route: "feature-toggles",
titleKey: "system:navFeatureToggles",
title: "Feature Toggles",
description: "Manage Kandev feature and diagnostic switches.",
},
{
route: "database",
titleKey: "system:navDatabase",
title: "Database",
description: "Database driver, size, and available maintenance controls.",
},
{
// /settings/system/logs now redirects into Data & Logs, whose Logs
// section titles itself with `system:navLogs`. The description key is
// unchanged, so the sentence users read is still pinned below.
route: "logs",
titleKey: "system:navLogs",
title: "Logs",
description: "Create a diagnostic ZIP with frontend and backend logs.",
},
{
route: "updates",
titleKey: "system:navUpdates",
title: "Updates",
description: "Current vs latest release plus the full kandev changelog.",
},
{
route: "about",
titleKey: "system:navAbout",
title: "About",
description: "Version, build metadata, and links.",
},
{
route: "licenses",
titleKey: "system:navLicenses",
title: "Licenses",
description: "Open-source licenses for every npm and Go dependency shipped with kandev.",
},
{
route: "users",
titleKey: "system:navUsers",
title: "Users",
description: "Manage accounts, roles, and invite links for this instance.",
},
];
describe("System route headers keep their pre-migration English", () => {
it.each(ROUTE_COPY)("$route", ({ titleKey, title, description }) => {
expect(t(titleKey)).toBe(title);
const descriptionKey =
titleKey === "system:navLogs"
? "settings:logsPageDescription"
: `system:${camelRoute(title)}PageDescription`;
expect(t(descriptionKey)).toBe(description);
});
/**
* Backups is separate: its description interpolates the SQL statement and the
* snapshot directory, so both survive translation as values rather than being
* pseudo-localized into dead pointers.
*/
it("backups", () => {
const path = "/var/lib/kandev/backups";
expect(t("system:navBackups")).toBe("Backups");
expect(t("system:backupsPageDescription", { command: BACKUP_SQL_COMMAND, path })).toBe(
`VACUUM INTO snapshots stored under ${path}.`,
);
});
});
describe("Data & Logs backup location copy", () => {
it("renders the resolved SQLite backup directory", () => {
const path = "/var/lib/kandev/backups";
databaseState.value = { backup_directory: path };
render(createElement(DataLogsSettings));
expect(screen.getByText(`VACUUM INTO snapshots stored under ${path}.`)).toBeTruthy();
});
it("omits the location when database information is unavailable", () => {
render(createElement(DataLogsSettings));
expect(screen.queryByText(/VACUUM INTO snapshots stored under/)).toBeNull();
});
it("omits the location when the backend has no backup directory", () => {
databaseState.value = { backup_directory: "" };
render(createElement(DataLogsSettings));
expect(screen.queryByText(/VACUUM INTO snapshots stored under/)).toBeNull();
});
});
describe("Data & Logs composition", () => {
it("keeps database, backups, and logs without the Storage section", () => {
render(createElement(DataLogsSettings));
expect(screen.getByText(t("system:navDatabase"))).toBeTruthy();
expect(screen.getByText(t("system:navBackups"))).toBeTruthy();
expect(screen.getByText(t("system:navLogs"))).toBeTruthy();
expect(screen.queryByText(t("system:storageTitle"))).toBeNull();
});
it("describes only database statistics, backups, and server logs", () => {
expect(t("system:dataStoragePageDescription")).toBe(
"Database statistics, backups, and server logs.",
);
});
});
/** "Feature Toggles" -> "featureToggles", matching the catalog key suffix. */
function camelRoute(title: string): string {
const [first, ...rest] = title.split(" ");
return first.toLowerCase() + rest.join("");
}