Simpl-Resultat/src/components/settings/DocsSubsections.test.tsx
le king fu f5e5897df1 test(i18n): move the raw fake t files to the marked i18nMock
The 15 test files whose fake t returned the bare key (the 12 of the
issue, plus the three category render tests added by #423) now mock
react-i18next with i18nMock. Assertions naming a displayed text are
rewritten in the marked form, t(key[options]), so a text that skipped
t fails them. Components without such an assertion get one:
PriceFetchControl, PriceFetchConsentToggle (rendered through the hook
harness, its loading slot preset), CategoryDetailPanel, SourceList and
SourceCard, and the region label of OnboardingChecklist.

Each component was proved by a mutation removing one t call: every one
turns at least one test red (exit 1).

Refs #389

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:56:27 -04:00

150 lines
4.8 KiB
TypeScript

// DocsSubsections — unit tests (Issue #355).
//
// Same approach as FilterPanel.test.tsx: no jsdom in this project, and the
// component's only hook is `useTranslation` (mocked by `i18nMock` over a
// fixture), so it is invoked directly as a function and its React-element
// tree is walked without a renderer.
import { describe, it, expect, vi } from "vitest";
// Hoisted: the factory of `vi.mock` below reads it when the module loads.
const FIXTURE = vi.hoisted((): Record<string, unknown> => ({
"docs.import.subsections": [
{
id: "import-export",
title: "Export a CSV from your bank",
steps: ["One subfolder per account", "Pick the widest range"],
note: "Trailing note",
},
{
id: "import-bnc",
title: "National Bank",
status: "No public procedure found",
steps: ["Expected header"],
},
],
}));
// The fixture stands for what the locale declares; every other key goes
// through the marked `t` (#389), as i18next returns a string for a missing key.
vi.mock("react-i18next", async () =>
(await import("../../test/i18nMock")).i18nMock({ fixture: FIXTURE }),
);
import DocsSubsections, { readSubsections } from "./DocsSubsections";
// ---------------------------------------------------------------------------
// Minimal React-element tree walker (no renderer/DOM involved)
// ---------------------------------------------------------------------------
interface ElementNode {
type: unknown;
props: Record<string, unknown> & { children?: unknown };
}
type FlatNode = ElementNode | string | number;
function isElementNode(x: unknown): x is ElementNode {
return typeof x === "object" && x !== null && "props" in x;
}
function flatten(node: unknown): FlatNode[] {
if (Array.isArray(node)) return node.flatMap(flatten);
if (node === null || node === undefined || typeof node === "boolean") return [];
if (typeof node === "string" || typeof node === "number") return [node];
if (isElementNode(node)) return [node, ...flatten(node.props.children)];
return [];
}
function elementsOfType(root: unknown, type: string): ElementNode[] {
return flatten(root).filter(
(n): n is ElementNode => isElementNode(n) && n.type === type,
);
}
function textOf(node: ElementNode): string {
return flatten(node.props.children)
.filter((n): n is string => typeof n === "string")
.join("");
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("DocsSubsections", () => {
it("renders nothing for a section without subsections", () => {
const tree = DocsSubsections({
sectionKey: "budget",
registerRef: () => {},
});
expect(tree).toBeNull();
});
it("renders one block per entry with its id and registers each element", () => {
const registered: string[] = [];
const tree = DocsSubsections({
sectionKey: "import",
registerRef: (id) => registered.push(id),
});
const blocks = flatten(tree).filter(
(n): n is ElementNode =>
isElementNode(n) && typeof n.props.id === "string",
);
expect(blocks.map((b) => b.props.id)).toEqual([
"import-export",
"import-bnc",
]);
// The ref callbacks are what DocsContent uses for hash navigation.
for (const block of blocks) {
(block.props.ref as (el: HTMLElement | null) => void)(null);
}
expect(registered).toEqual(["import-export", "import-bnc"]);
const titles = elementsOfType(tree, "h4").map(textOf);
expect(titles).toEqual(["Export a CSV from your bank", "National Bank"]);
const steps = elementsOfType(tree, "li").map(textOf);
expect(steps).toEqual([
"One subfolder per account",
"Pick the widest range",
"Expected header",
]);
});
it("renders the status line in italic only when present, and the note only when present", () => {
const tree = DocsSubsections({
sectionKey: "import",
registerRef: () => {},
});
const paragraphs = elementsOfType(tree, "p");
const italics = paragraphs.filter((p) =>
String(p.props.className).includes("italic"),
);
expect(italics.map(textOf)).toEqual(["No public procedure found"]);
const notes = paragraphs.filter(
(p) => !String(p.props.className).includes("italic"),
);
expect(notes.map(textOf)).toEqual(["Trailing note"]);
});
});
describe("readSubsections", () => {
it("returns an empty array for a missing key (i18next echoes the key)", () => {
expect(readSubsections("docs.budget.subsections")).toEqual([]);
});
it("drops malformed entries", () => {
expect(
readSubsections([
{ id: "ok", title: "Ok", steps: [] },
{ id: "no-steps", title: "Missing steps" },
"garbage",
null,
]),
).toEqual([{ id: "ok", title: "Ok", steps: [] }]);
});
});