mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 22:02:16 +00:00
test(settings): coverage for renamed/consolidated sections
Phase B6 commit 4 of 4. * `sections.test.tsx` gains two SSR smoke tests for the renamed `<ThresholdsSection>` (was `<ThresholdsSettingsSection>`) — one asserts EN heading + editor mount-point, one asserts the DE heading. The mock for the inner editor was already updated in commit 2 of this phase. * New `sections-i18n-parity.test.ts` walks every slug in `SETTINGS_SECTION_SLUGS` and asserts both `.title` and `.description` resolve to non-empty strings in EN AND DE — and that the DE description is not a verbatim copy of the EN one. The test caught a real instance: notifications.description was "Telegram, ntfy, Web Push." in both locales because the channel names are proper nouns. Rewrote the description in both locales so it actually reads as German vs. English while still listing the channels. Settings tests: 63/63 green. Typecheck clean. Lint clean (12 pre-existing warnings carried over from the baseline gate, 0 errors). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ed0cfda13f
commit
d914f766dc
4 files changed
+114
-2
No files matched your search
+1
-1
@@ -980,7 +980,7 @@
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Benachrichtigungen",
|
||||
"description": "Telegram, ntfy, Web Push."
|
||||
"description": "Zustellung pro Kanal — Telegram, ntfy, Web Push — und die Live-Übersicht der Kanalzuverlässigkeit."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
|
||||
+1
-1
@@ -980,7 +980,7 @@
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"description": "Telegram, ntfy, Web Push."
|
||||
"description": "Per-channel delivery — Telegram, ntfy, Web Push — plus the live channel-reliability board."
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* v1.4.16 phase B6 — settings i18n parity.
|
||||
*
|
||||
* Every slug declared in `SETTINGS_SECTION_SLUGS` MUST have a
|
||||
* `settings.sections.<slug>.title` AND a `settings.sections.<slug>.description`
|
||||
* resolved in both English and German. The audit doc
|
||||
* (`docs/audit/v1416-settings-audit.md`) calls this out as the canonical
|
||||
* shape — the sidebar, the page `<h1>`, and the page subtitle all read
|
||||
* from these two keys, so a missing key paints a raw key in production.
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import en from "../../../../messages/en.json" with { type: "json" };
|
||||
import de from "../../../../messages/de.json" with { type: "json" };
|
||||
import { SETTINGS_SECTION_SLUGS } from "../section-slugs";
|
||||
|
||||
type Bag = Record<string, unknown>;
|
||||
|
||||
function lookup(bag: Bag, path: string[]): string | null {
|
||||
let cursor: unknown = bag;
|
||||
for (const segment of path) {
|
||||
if (typeof cursor !== "object" || cursor === null) return null;
|
||||
cursor = (cursor as Bag)[segment];
|
||||
}
|
||||
return typeof cursor === "string" ? cursor : null;
|
||||
}
|
||||
|
||||
describe("settings sections — i18n parity (B6)", () => {
|
||||
for (const slug of SETTINGS_SECTION_SLUGS) {
|
||||
it(`every locale resolves settings.sections.${slug}.title`, () => {
|
||||
const enValue = lookup(en as Bag, ["settings", "sections", slug, "title"]);
|
||||
const deValue = lookup(de as Bag, ["settings", "sections", slug, "title"]);
|
||||
expect(enValue, `EN missing title for ${slug}`).toBeTypeOf("string");
|
||||
expect(enValue?.length ?? 0).toBeGreaterThan(0);
|
||||
expect(deValue, `DE missing title for ${slug}`).toBeTypeOf("string");
|
||||
expect(deValue?.length ?? 0).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it(`every locale resolves settings.sections.${slug}.description`, () => {
|
||||
const enValue = lookup(en as Bag, [
|
||||
"settings",
|
||||
"sections",
|
||||
slug,
|
||||
"description",
|
||||
]);
|
||||
const deValue = lookup(de as Bag, [
|
||||
"settings",
|
||||
"sections",
|
||||
slug,
|
||||
"description",
|
||||
]);
|
||||
expect(enValue, `EN missing description for ${slug}`).toBeTypeOf("string");
|
||||
expect(enValue?.length ?? 0).toBeGreaterThan(0);
|
||||
expect(deValue, `DE missing description for ${slug}`).toBeTypeOf("string");
|
||||
expect(deValue?.length ?? 0).toBeGreaterThan(0);
|
||||
});
|
||||
}
|
||||
|
||||
it("EN and DE description for every slug are different (catches accidental copy-paste)", () => {
|
||||
for (const slug of SETTINGS_SECTION_SLUGS) {
|
||||
const enValue = lookup(en as Bag, [
|
||||
"settings",
|
||||
"sections",
|
||||
slug,
|
||||
"description",
|
||||
]);
|
||||
const deValue = lookup(de as Bag, [
|
||||
"settings",
|
||||
"sections",
|
||||
slug,
|
||||
"description",
|
||||
]);
|
||||
// It's legitimate for some titles to coincide between locales when
|
||||
// the proper noun is the same (e.g. "Dashboard", "API & Tokens").
|
||||
// Descriptions, however, MUST be translated — a copy-paste of the
|
||||
// EN value into the DE bag is a bug.
|
||||
expect(
|
||||
deValue,
|
||||
`DE description for ${slug} is identical to EN — needs translation`,
|
||||
).not.toBe(enValue);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -86,6 +86,12 @@ import { ApiSection } from "../api-section";
|
||||
import { DashboardSection } from "../dashboard-section";
|
||||
import { IntegrationsSection } from "../integrations-section";
|
||||
import { NotificationsSection } from "../notifications-section";
|
||||
// v1.4.16 phase B6: route-level wrapper. The historic
|
||||
// `<ThresholdsSettingsSection>` was renamed to `<ThresholdsSection>` and
|
||||
// its filename changed from `thresholds-settings-section.tsx` to
|
||||
// `thresholds-section.tsx` so the component name + filename match the
|
||||
// slug like every other settings section.
|
||||
import { ThresholdsSection } from "../thresholds-section";
|
||||
|
||||
function render(node: React.ReactElement, locale: "en" | "de" = "en") {
|
||||
return renderToStaticMarkup(
|
||||
@@ -180,4 +186,27 @@ describe("settings sections — SSR smoke", () => {
|
||||
expect(html).toContain("Jetzt prüfen");
|
||||
expect(html).toContain("Quellen & Dokumentation");
|
||||
});
|
||||
|
||||
it("<ThresholdsSection> renders heading and embeds the editor (B6 rename)", () => {
|
||||
// v1.4.16 phase B6 contract: the route wrapper is named
|
||||
// `<ThresholdsSection>` (was `<ThresholdsSettingsSection>`), lives
|
||||
// at `thresholds-section.tsx`, and embeds the inner editor under
|
||||
// `<ThresholdsEditorSection>` (was `<ThresholdsSection>`). The
|
||||
// mock above stubs the editor as `data-testid="thresholds-editor"`,
|
||||
// so this test asserts:
|
||||
// - the wrapper renders without throwing
|
||||
// - the localised page title surfaces
|
||||
// - the editor mount-point is present
|
||||
// - no raw i18n key leaks
|
||||
const html = render(<ThresholdsSection />);
|
||||
expect(html).toContain("Personal targets");
|
||||
expect(html).toContain('data-testid="thresholds-editor"');
|
||||
expect(html).not.toContain("settings.sections.thresholds.");
|
||||
});
|
||||
|
||||
it("<ThresholdsSection> resolves the German title (B6 rename)", () => {
|
||||
const html = render(<ThresholdsSection />, "de");
|
||||
expect(html).toContain("Persönliche Zielwerte");
|
||||
expect(html).toContain('data-testid="thresholds-editor"');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user