test(i18n): one marked fake t for the suite, and a guard (#389) #435

Merged
maximus merged 4 commits from issue-389-i18n-mock into main 2026-10-14 00:28:40 +00:00
15 changed files with 209 additions and 177 deletions
Showing only changes of commit f5e5897df1 - Show all commits

View file

@ -27,19 +27,8 @@ vi.mock("../../services/userPreferenceService", () => ({
setPreference: vi.fn(), setPreference: vi.fn(),
})); }));
// react-i18next: return the key as-is for tests // react-i18next: the shared marked `t` (#389), so a text that skipped `t` fails
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: vi.fn(() => ({
t: (key: string, opts?: Record<string, unknown>) => {
// Include interpolation values in the returned string for assertions
if (opts) {
return `${key}(${JSON.stringify(opts)})`;
}
return key;
},
i18n: { language: "fr" },
})),
}));
// lucide-react: return simple stubs // lucide-react: return simple stubs
vi.mock("lucide-react", () => ({ vi.mock("lucide-react", () => ({
@ -58,7 +47,8 @@ import {
getPreference, getPreference,
setPreference, setPreference,
} from "../../services/userPreferenceService"; } from "../../services/userPreferenceService";
import { import { renderToStaticMarkup } from "react-dom/server";
import PriceFetchControl, {
__resetBestEffortDismissForTests, __resetBestEffortDismissForTests,
} from "./PriceFetchControl"; } from "./PriceFetchControl";
@ -363,3 +353,31 @@ describe("PriceFetchControl — fetchPrice invocation args", () => {
expect(mockSetPreference).not.toHaveBeenCalled(); expect(mockSetPreference).not.toHaveBeenCalled();
}); });
}); });
// ---------------------------------------------------------------------------
// Test: what the control displays (#389)
// ---------------------------------------------------------------------------
describe("PriceFetchControl — displayed texts", () => {
beforeEach(() => {
__resetBestEffortDismissForTests();
vi.resetAllMocks();
setPremium(true);
});
it("names the fetch button and the stock best-effort notice through t", () => {
const html = renderToStaticMarkup(
<PriceFetchControl
symbol="AAPL"
date="2026-04-25"
categoryKind="priced"
assetType="stock"
onPriceFetched={vi.fn()}
/>,
);
expect(html).toContain('aria-label="t(balance.priceFetching.button)"');
expect(html).toContain(">t(balance.priceFetching.button)<");
expect(html).toContain(">t(balance.priceFetching.bestEffortNotice)<");
expect(html).toContain('aria-label="t(common.close)"');
});
});

View file

@ -4,13 +4,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({
t: (k: string, o?: Record<string, unknown>) =>
o ? `${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}]` : k,
i18n: { language: "fr" },
}),
}));
vi.mock("./keywordCategory", () => ({ vi.mock("./keywordCategory", () => ({
loadKeywordCategories: vi.fn(() => new Promise(() => undefined)), loadKeywordCategories: vi.fn(() => new Promise(() => undefined)),
@ -51,12 +45,12 @@ describe("AddKeywordDialog — category field (#423)", () => {
it("starts empty, with the 'no category' placeholder, until the list is in", () => { it("starts empty, with the 'no category' placeholder, until the list is in", () => {
const [input] = markup(7).match(/<input [^>]*role="combobox"[^>]*>/g) ?? []; const [input] = markup(7).match(/<input [^>]*role="combobox"[^>]*>/g) ?? [];
expect(input).toContain('value=""'); expect(input).toContain('value=""');
expect(input).toContain('placeholder="transactions.table.noCategory"'); expect(input).toContain('placeholder="t(transactions.table.noCategory)"');
}); });
it("keeps the apply button disabled before the list is in", () => { it("keeps the apply button disabled before the list is in", () => {
const html = markup(7); const html = markup(7);
const [apply] = html.match(/<button [^>]*>reports\.keyword\.applyAndRecategorize<\/button>/g) ?? []; const [apply] = html.match(/<button [^>]*>t\(reports\.keyword\.applyAndRecategorize\)<\/button>/g) ?? [];
expect(apply).toBeDefined(); expect(apply).toBeDefined();
expect(apply).toContain("disabled"); expect(apply).toContain("disabled");
}); });

View file

@ -3,13 +3,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({
t: (k: string, o?: Record<string, unknown>) =>
o ? `${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}]` : k,
i18n: { language: "fr" },
}),
}));
vi.mock("./parentOptions", async (importOriginal) => { vi.mock("./parentOptions", async (importOriginal) => {
const actual = await importOriginal<typeof import("./parentOptions")>(); const actual = await importOriginal<typeof import("./parentOptions")>();
@ -82,3 +76,14 @@ describe("CategoryDetailPanel — editingId wiring (#423)", () => {
expect(buildParentOptions).toHaveBeenCalledWith(CATEGORIES, { editingId: null, currentParentId: null }); expect(buildParentOptions).toHaveBeenCalledWith(CATEGORIES, { editingId: null, currentParentId: null });
}); });
}); });
describe("CategoryDetailPanel — displayed texts (#389)", () => {
it("titles the edit view and the create view through t", () => {
expect(markup(RENT, formOf(RENT), false)).toContain(">t(categories.editCategory)<");
expect(markup(null, formOf(node(0, null, "")), true)).toContain(">t(categories.addCategory)<");
});
it("asks for a selection through t when nothing is selected", () => {
expect(markup(null, null, false)).toContain(">t(categories.selectCategory)<");
});
});

View file

@ -4,13 +4,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({
t: (k: string, o?: Record<string, unknown>) =>
o ? `${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}]` : k,
i18n: { language: "fr" },
}),
}));
// Pass-through spy on the pure module, to pin what the form hands it. // Pass-through spy on the pure module, to pin what the form hands it.
vi.mock("./parentOptions", async (importOriginal) => { vi.mock("./parentOptions", async (importOriginal) => {
@ -79,20 +73,20 @@ describe("CategoryForm — parent field (#423)", () => {
it("renders the parent as a combobox labelled by the field name, with no parent select left", () => { it("renders the parent as a combobox labelled by the field name, with no parent select left", () => {
const html = markup(form(null)); const html = markup(form(null));
const input = comboboxInput(html); const input = comboboxInput(html);
expect(input).toContain('aria-label="categories.parent"'); expect(input).toContain('aria-label="t(categories.parent)"');
// Only the type select remains. // Only the type select remains.
expect(html.match(/<select/g) ?? []).toHaveLength(1); expect(html.match(/<select/g) ?? []).toHaveLength(1);
expect(html).not.toContain("categories.noParent</option>"); expect(html).not.toContain("categories.noParent</option>");
}); });
it("shows the 'no parent' option as the value when creating without a parent", () => { it("shows the 'no parent' option as the value when creating without a parent", () => {
expect(comboboxInput(markup(form(null)))).toContain('value="categories.noParent"'); expect(comboboxInput(markup(form(null)))).toContain('value="t(categories.noParent)"');
}); });
it("shows the parent's translated name as the value when editing a category with a parent", () => { it("shows the parent's translated name as the value when editing a category with a parent", () => {
// Seed rows translate through their i18n_key with the name as default value. // Seed rows translate through their i18n_key with the name as default value.
expect(comboboxInput(markup(form(1), 10))).toContain( expect(comboboxInput(markup(form(1), 10))).toContain(
'value="categoriesSeed.housing[defaultValue=Logement]"', 'value="t(categoriesSeed.housing[defaultValue=Logement])"',
); );
}); });

View file

@ -8,13 +8,7 @@ import { describe, it, expect, vi } from "vitest";
import { isValidElement, type ReactElement, type ReactNode } from "react"; import { isValidElement, type ReactElement, type ReactNode } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({
t: (key: string, o?: Record<string, unknown>) =>
o ? `${key}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}]` : key,
i18n: { language: "fr" },
}),
}));
vi.mock("lucide-react", () => ({ vi.mock("lucide-react", () => ({
CheckCircle2: () => null, CheckCircle2: () => null,
@ -106,11 +100,12 @@ describe("OnboardingChecklist", () => {
it("renders the title, the counter 1 / 6 and the hide button", () => { it("renders the title, the counter 1 / 6 and the hide button", () => {
const { element, onHide } = render(); const { element, onHide } = render();
const text = texts(element); const text = texts(element);
expect(text).toContain("onboarding.checklist.title"); expect(text).toContain("t(onboarding.checklist.title)");
expect(text).toContain("onboarding.checklist.counter[done=1,total=6]"); expect((element as AnyElement).props["aria-label"]).toBe("t(onboarding.checklist.title)");
expect(text).toContain("t(onboarding.checklist.counter[done=1,total=6])");
const [hide] = find(element, (e) => e.type === "button"); const [hide] = find(element, (e) => e.type === "button");
expect(hide.props.type).toBe("button"); expect(hide.props.type).toBe("button");
expect(texts(hide.props.children)).toContain("onboarding.checklist.hide"); expect(texts(hide.props.children)).toContain("t(onboarding.checklist.hide)");
(hide.props.onClick as () => void)(); (hide.props.onClick as () => void)();
expect(onHide).toHaveBeenCalledTimes(1); expect(onHide).toHaveBeenCalledTimes(1);
}); });
@ -120,7 +115,7 @@ describe("OnboardingChecklist", () => {
expect(find(element, (e) => e.type === "li")).toHaveLength(6); expect(find(element, (e) => e.type === "li")).toHaveLength(6);
const text = texts(element); const text = texts(element);
for (const key of ["profile", "importFolder", "source", "import", "manualCategory", "budget"]) { for (const key of ["profile", "importFolder", "source", "import", "manualCategory", "budget"]) {
expect(text).toContain(`onboarding.checklist.items.${key}`); expect(text).toContain(`t(onboarding.checklist.items.${key})`);
} }
}); });
@ -154,12 +149,12 @@ describe("OnboardingChecklist", () => {
const { element } = render({}, ZERO, false); const { element } = render({}, ZERO, false);
const budget = rowOf(element, "budget"); const budget = rowOf(element, "budget");
expect(find(budget, (e) => e.type === Lock)).toHaveLength(1); expect(find(budget, (e) => e.type === Lock)).toHaveLength(1);
expect(texts(budget)).toContain("onboarding.checklist.lockedBudget"); expect(texts(budget)).toContain("t(onboarding.checklist.lockedBudget)");
const [link] = find(budget, (e) => e.type === Link); const [link] = find(budget, (e) => e.type === Link);
expect(link.props.to).toBe("/settings/users"); expect(link.props.to).toBe("/settings/users");
// The license card carries no step card: nothing to focus there. // The license card carries no step card: nothing to focus there.
expect(link.props.state).toBeUndefined(); expect(link.props.state).toBeUndefined();
expect(texts(element)).toContain("onboarding.checklist.counter[done=1,total=5]"); expect(texts(element)).toContain("t(onboarding.checklist.counter[done=1,total=5])");
}); });
it("while the entitlement is unknown, the budget row is neutral and the counter is absent", () => { it("while the entitlement is unknown, the budget row is neutral and the counter is absent", () => {
@ -169,35 +164,35 @@ describe("OnboardingChecklist", () => {
expect(find(budget, (e) => e.type === CheckCircle2)).toHaveLength(0); expect(find(budget, (e) => e.type === CheckCircle2)).toHaveLength(0);
expect(find(budget, (e) => e.type === Circle)).toHaveLength(1); expect(find(budget, (e) => e.type === Circle)).toHaveLength(1);
expect(find(budget, (e) => e.type === Link)).toHaveLength(0); expect(find(budget, (e) => e.type === Link)).toHaveLength(0);
expect(texts(budget)).not.toContain("onboarding.checklist.lockedBudget"); expect(texts(budget)).not.toContain("t(onboarding.checklist.lockedBudget)");
expect(texts(element).some((s) => s.startsWith("onboarding.checklist.counter"))).toBe(false); expect(texts(element).some((s) => s.startsWith("t(onboarding.checklist.counter"))).toBe(false);
}); });
}); });
describe("OnboardingChecklist — states read out (#384, #397)", () => { describe("OnboardingChecklist — states read out (#384, #397)", () => {
it("reads done, skipped and locked after the label, and nothing for a row still to do", () => { it("reads done, skipped and locked after the label, and nothing for a row still to do", () => {
const { element } = render({}, ZERO, false, ["source"]); const { element } = render({}, ZERO, false, ["source"]);
expect(stateTextsOf(rowOf(element, "profile"))).toEqual(["onboarding.state.done"]); expect(stateTextsOf(rowOf(element, "profile"))).toEqual(["t(onboarding.state.done)"]);
expect(stateTextsOf(rowOf(element, "source"))).toEqual(["onboarding.state.skipped"]); expect(stateTextsOf(rowOf(element, "source"))).toEqual(["t(onboarding.state.skipped)"]);
expect(stateTextsOf(rowOf(element, "budget"))).toEqual(["onboarding.state.locked"]); expect(stateTextsOf(rowOf(element, "budget"))).toEqual(["t(onboarding.state.locked)"]);
expect(stateTextsOf(rowOf(element, "importFolder"))).toEqual([]); expect(stateTextsOf(rowOf(element, "importFolder"))).toEqual([]);
expect(stateTextsOf(rowOf(element, "import"))).toEqual([]); expect(stateTextsOf(rowOf(element, "import"))).toEqual([]);
}); });
it("reads the budget row as pending while the entitlement is unknown", () => { it("reads the budget row as pending while the entitlement is unknown", () => {
const { element } = render({ budgetReady: false }, ZERO, false); const { element } = render({ budgetReady: false }, ZERO, false);
expect(stateTextsOf(rowOf(element, "budget"))).toEqual(["onboarding.state.pending"]); expect(stateTextsOf(rowOf(element, "budget"))).toEqual(["t(onboarding.state.pending)"]);
}); });
it("puts the state text right after the label, inside the same element", () => { it("puts the state text right after the label, inside the same element", () => {
const { element } = render({}, { ...ZERO, sources: 1 }); const { element } = render({}, { ...ZERO, sources: 1 });
const [labelled] = find( const [labelled] = find(
rowOf(element, "source"), rowOf(element, "source"),
(e) => e.type === "span" && texts(e.props.children).includes("onboarding.checklist.items.source"), (e) => e.type === "span" && texts(e.props.children).includes("t(onboarding.checklist.items.source)"),
); );
expect(texts(labelled.props.children).map((s) => s.trim()).filter(Boolean)).toEqual([ expect(texts(labelled.props.children).map((s) => s.trim()).filter(Boolean)).toEqual([
"onboarding.checklist.items.source", "t(onboarding.checklist.items.source)",
"onboarding.state.done", "t(onboarding.state.done)",
]); ]);
}); });
}); });
@ -210,11 +205,11 @@ describe("OnboardingChecklist — skipped rows (#397)", () => {
expect(find(source, (e) => e.type === Circle)).toHaveLength(0); expect(find(source, (e) => e.type === Circle)).toHaveLength(0);
const [badge] = find( const [badge] = find(
source, source,
(e) => e.type === "span" && texts(e.props.children).includes("onboarding.checklist.skippedBadge"), (e) => e.type === "span" && texts(e.props.children).includes("t(onboarding.checklist.skippedBadge)"),
); );
// The hidden "(skipped)" text already says it: the badge is not read twice. // The hidden "(skipped)" text already says it: the badge is not read twice.
expect(badge.props["aria-hidden"]).toBe("true"); expect(badge.props["aria-hidden"]).toBe("true");
expect(texts(element)).toContain("onboarding.checklist.counter[done=2,total=6]"); expect(texts(element)).toContain("t(onboarding.checklist.counter[done=2,total=6])");
}); });
it("'Resume' takes the step back, then opens its page with the step's navigation state", () => { it("'Resume' takes the step back, then opens its page with the step's navigation state", () => {
@ -223,7 +218,7 @@ describe("OnboardingChecklist — skipped rows (#397)", () => {
// The label is no link: only "Resume" leads to the page, after taking the step back. // The label is no link: only "Resume" leads to the page, after taking the step back.
expect(sourceLinks).toHaveLength(1); expect(sourceLinks).toHaveLength(1);
const [resume] = sourceLinks; const [resume] = sourceLinks;
expect(texts(resume.props.children)).toEqual(["onboarding.checklist.resume"]); expect(texts(resume.props.children)).toEqual(["t(onboarding.checklist.resume)"]);
expect(resume.props.to).toBe("/import"); expect(resume.props.to).toBe("/import");
expect(resume.props.state).toEqual({ onboarding: { focus: true } }); expect(resume.props.state).toEqual({ onboarding: { focus: true } });
(resume.props.onClick as () => void)(); (resume.props.onClick as () => void)();
@ -242,7 +237,7 @@ describe("OnboardingChecklist — skipped rows (#397)", () => {
expect(find(source, (e) => e.type === CheckCircle2)).toHaveLength(1); expect(find(source, (e) => e.type === CheckCircle2)).toHaveLength(1);
expect(find(source, (e) => e.type === SkipForward)).toHaveLength(0); expect(find(source, (e) => e.type === SkipForward)).toHaveLength(0);
expect(find(source, (e) => e.type === Link)).toHaveLength(0); expect(find(source, (e) => e.type === Link)).toHaveLength(0);
expect(texts(source)).not.toContain("onboarding.checklist.resume"); expect(texts(source)).not.toContain("t(onboarding.checklist.resume)");
}); });
it("a locked row stays locked even when skipped", () => { it("a locked row stays locked even when skipped", () => {
@ -250,7 +245,7 @@ describe("OnboardingChecklist — skipped rows (#397)", () => {
const budget = rowOf(element, "budget"); const budget = rowOf(element, "budget");
expect(find(budget, (e) => e.type === Lock)).toHaveLength(1); expect(find(budget, (e) => e.type === Lock)).toHaveLength(1);
expect(find(budget, (e) => e.type === SkipForward)).toHaveLength(0); expect(find(budget, (e) => e.type === SkipForward)).toHaveLength(0);
expect(texts(budget)).not.toContain("onboarding.checklist.resume"); expect(texts(budget)).not.toContain("t(onboarding.checklist.resume)");
const [link] = find(budget, (e) => e.type === Link); const [link] = find(budget, (e) => e.type === Link);
expect(link.props.to).toBe("/settings/users"); expect(link.props.to).toBe("/settings/users");
}); });

View file

@ -9,13 +9,7 @@ import { describe, it, expect, vi } from "vitest";
import { isValidElement, type ReactElement, type ReactNode } from "react"; import { isValidElement, type ReactElement, type ReactNode } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({
t: (key: string, o?: Record<string, unknown>) =>
o ? `${key}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}]` : key,
i18n: { language: "fr" },
}),
}));
vi.mock("lucide-react", () => ({ vi.mock("lucide-react", () => ({
FolderOpen: () => null, FolderOpen: () => null,
@ -83,11 +77,11 @@ describe("ImportEmptyState — no folder (#356)", () => {
it("offers one primary 'choose a folder' button wired to onBrowse, and the guide link", () => { it("offers one primary 'choose a folder' button wired to onBrowse, and the guide link", () => {
const { props, tree, buttons, links } = render({ hasFolder: false }); const { props, tree, buttons, links } = render({ hasFolder: false });
expect(texts(tree)).toContain("import.empty.noFolder.title"); expect(texts(tree)).toContain("t(import.empty.noFolder.title)");
expect(texts(tree)).toContain("import.empty.noFolder.description"); expect(texts(tree)).toContain("t(import.empty.noFolder.description)");
expect(buttons).toHaveLength(1); expect(buttons).toHaveLength(1);
expect(texts(buttons[0].props.children)).toContain("import.empty.noFolder.cta"); expect(texts(buttons[0].props.children)).toContain("t(import.empty.noFolder.cta)");
expect(buttons[0].props.disabled).toBe(false); expect(buttons[0].props.disabled).toBe(false);
buttons[0].props.onClick(); buttons[0].props.onClick();
expect(props.onBrowse).toHaveBeenCalledTimes(1); expect(props.onBrowse).toHaveBeenCalledTimes(1);
@ -96,13 +90,13 @@ describe("ImportEmptyState — no folder (#356)", () => {
expect(links).toHaveLength(1); expect(links).toHaveLength(1);
expect(links[0].props.to).toBe(CSV_EXPORT_GUIDE_PATH); expect(links[0].props.to).toBe(CSV_EXPORT_GUIDE_PATH);
expect(links[0].props.to).toBe("/settings/users#import-export"); expect(links[0].props.to).toBe("/settings/users#import-export");
expect(texts(links[0].props.children)).toContain("import.empty.guideLink"); expect(texts(links[0].props.children)).toContain("t(import.empty.guideLink)");
}); });
it("never shows the sample or refresh actions without a folder", () => { it("never shows the sample or refresh actions without a folder", () => {
const { tree } = render({ hasFolder: false }); const { tree } = render({ hasFolder: false });
expect(texts(tree)).not.toContain("import.empty.createSample"); expect(texts(tree)).not.toContain("t(import.empty.createSample)");
expect(texts(tree)).not.toContain("import.empty.refresh"); expect(texts(tree)).not.toContain("t(import.empty.refresh)");
}); });
}); });
@ -110,22 +104,22 @@ describe("ImportEmptyState — folder without sources (#356)", () => {
it("offers 'create a sample folder' as primary, 'refresh' as secondary, and the guide link", () => { it("offers 'create a sample folder' as primary, 'refresh' as secondary, and the guide link", () => {
const { props, tree, buttons, links } = render({ hasFolder: true, folderName: "Relevés" }); const { props, tree, buttons, links } = render({ hasFolder: true, folderName: "Relevés" });
expect(texts(tree)).toContain("import.empty.noSources.title"); expect(texts(tree)).toContain("t(import.empty.noSources.title)");
expect(buttons).toHaveLength(2); expect(buttons).toHaveLength(2);
expect(texts(buttons[0].props.children)).toContain("import.empty.createSample"); expect(texts(buttons[0].props.children)).toContain("t(import.empty.createSample)");
expect(buttons[0].props.disabled).toBe(false); expect(buttons[0].props.disabled).toBe(false);
buttons[0].props.onClick(); buttons[0].props.onClick();
expect(props.onCreateSample).toHaveBeenCalledTimes(1); expect(props.onCreateSample).toHaveBeenCalledTimes(1);
expect(texts(buttons[1].props.children)).toContain("import.empty.refresh"); expect(texts(buttons[1].props.children)).toContain("t(import.empty.refresh)");
buttons[1].props.onClick(); buttons[1].props.onClick();
expect(props.onRefresh).toHaveBeenCalledTimes(1); expect(props.onRefresh).toHaveBeenCalledTimes(1);
expect(props.onBrowse).not.toHaveBeenCalled(); expect(props.onBrowse).not.toHaveBeenCalled();
expect(links).toHaveLength(1); expect(links).toHaveLength(1);
expect(links[0].props.to).toBe("/settings/users#import-export"); expect(links[0].props.to).toBe("/settings/users#import-export");
expect(texts(links[0].props.children)).toContain("import.empty.guideLink"); expect(texts(links[0].props.children)).toContain("t(import.empty.guideLink)");
}); });
it("disables the create button while the sample folders are being written", () => { it("disables the create button while the sample folders are being written", () => {
@ -138,9 +132,9 @@ describe("ImportEmptyState — folder without sources (#356)", () => {
it("names the folder in the description, never the full path", () => { it("names the folder in the description, never the full path", () => {
const folderName = folderNameFromPath("C:\\Users\\max\\Documents\\Relevés"); const folderName = folderNameFromPath("C:\\Users\\max\\Documents\\Relevés");
const { tree } = render({ hasFolder: true, folderName }); const { tree } = render({ hasFolder: true, folderName });
const description = texts(tree).find((s) => s.startsWith("import.empty.noSources.description")); const description = texts(tree).find((s) => s.startsWith("t(import.empty.noSources.description"));
expect(description).toBe("import.empty.noSources.description[folderName=Relevés]"); expect(description).toBe("t(import.empty.noSources.description[folderName=Relevés])");
expect(description).not.toContain("Users"); expect(description).not.toContain("Users");
}); });
}); });
@ -154,14 +148,14 @@ describe("ImportEmptyState — getting-started target (#398)", () => {
const targets = highlighted(tree); const targets = highlighted(tree);
expect(targets).toHaveLength(1); expect(targets).toHaveLength(1);
expect(targets[0].type).toBe("button"); expect(targets[0].type).toBe("button");
expect(texts(targets[0].props.children)).toContain("import.empty.noFolder.cta"); expect(texts(targets[0].props.children)).toContain("t(import.empty.noFolder.cta)");
}); });
it("highlights 'Create a sample folder' in a folder without sources, not 'Refresh'", () => { it("highlights 'Create a sample folder' in a folder without sources, not 'Refresh'", () => {
const { tree } = render({ hasFolder: true, folderName: "Relevés", onboardingTarget: true }); const { tree } = render({ hasFolder: true, folderName: "Relevés", onboardingTarget: true });
const targets = highlighted(tree); const targets = highlighted(tree);
expect(targets).toHaveLength(1); expect(targets).toHaveLength(1);
expect(texts(targets[0].props.children)).toContain("import.empty.createSample"); expect(texts(targets[0].props.children)).toContain("t(import.empty.createSample)");
}); });
it("highlights nothing without the property", () => { it("highlights nothing without the property", () => {

View file

@ -4,13 +4,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({
t: (k: string, o?: Record<string, unknown>) =>
o ? `${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}]` : k,
i18n: { language: "fr" },
}),
}));
import SourceList from "./SourceList"; import SourceList from "./SourceList";
import { SOURCE_CARD_TARGET } from "./SourceCard"; import { SOURCE_CARD_TARGET } from "./SourceCard";
@ -61,3 +55,27 @@ describe("SourceList — getting-started target (#398)", () => {
expect(markup(false)).not.toContain("data-onboarding"); expect(markup(false)).not.toContain("data-onboarding");
}); });
}); });
describe("SourceList and SourceCard — displayed texts (#389)", () => {
it("titles the list and counts each source's files through t", () => {
const html = markup();
expect(html).toContain(">t(import.sources.title)<");
expect(cards(html)[0]).toContain("t(import.sources.fileCount[count=1])");
});
it("marks the files not yet imported through t", () => {
const html = renderToStaticMarkup(
<SourceList
sources={[source("Desjardins")]}
configuredSourceNames={new Set()}
importedFileNames={new Map([["Desjardins", new Set<string>()]])}
onSelectSource={vi.fn()}
/>,
);
expect(html).toContain("t(import.sources.new)");
});
it("says the folder is empty through t", () => {
expect(markup(false, [])).toContain(">t(import.sources.empty)<");
});
});

View file

@ -4,9 +4,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({ t: (k: string) => k, i18n: { language: "fr" } }),
}));
import WizardNavigation, { WIZARD_NEXT_TARGET } from "./WizardNavigation"; import WizardNavigation, { WIZARD_NEXT_TARGET } from "./WizardNavigation";
@ -36,9 +34,9 @@ describe("WizardNavigation — getting-started target (#398)", () => {
expect(next).toContain("NEXT_LABEL"); expect(next).toContain("NEXT_LABEL");
expect(next).toContain(HIGHLIGHT); expect(next).toContain(HIGHLIGHT);
expect(next).toContain(`data-onboarding-target="${WIZARD_NEXT_TARGET}"`); expect(next).toContain(`data-onboarding-target="${WIZARD_NEXT_TARGET}"`);
expect(cancel).toContain("common.cancel"); expect(cancel).toContain("t(common.cancel)");
expect(cancel).not.toContain("data-onboarding"); expect(cancel).not.toContain("data-onboarding");
expect(back).toContain("import.wizard.back"); expect(back).toContain("t(import.wizard.back)");
expect(back).not.toContain("data-onboarding"); expect(back).not.toContain("data-onboarding");
}); });

View file

@ -12,12 +12,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: vi.fn(() => ({
t: (key: string) => key,
i18n: { language: "fr" },
})),
}));
vi.mock("lucide-react", () => ({ vi.mock("lucide-react", () => ({
Inbox: () => null, Inbox: () => null,
@ -164,8 +159,8 @@ describe("FilterPanel — render", () => {
it("uses i18n keys for the accounts filter label and hint (never a hardcoded string)", () => { it("uses i18n keys for the accounts filter label and hint (never a hardcoded string)", () => {
const { element } = renderPanel(); const { element } = renderPanel();
const text = flatten(element); const text = flatten(element);
expect(text).toContain("reports.filters.accounts.label"); expect(text).toContain("t(reports.filters.accounts.label)");
expect(text).toContain("reports.filters.accounts.hint"); expect(text).toContain("t(reports.filters.accounts.hint)");
}); });
}); });

View file

@ -1,13 +1,14 @@
// DocsSubsections — unit tests (Issue #355). // DocsSubsections — unit tests (Issue #355).
// //
// Same approach as FilterPanel.test.tsx: no jsdom in this project, and the // Same approach as FilterPanel.test.tsx: no jsdom in this project, and the
// component's only hook is `useTranslation` (mocked into a lookup over a // component's only hook is `useTranslation` (mocked by `i18nMock` over a
// fixture), so it is invoked directly as a function and its React-element // fixture), so it is invoked directly as a function and its React-element
// tree is walked without a renderer. // tree is walked without a renderer.
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
const FIXTURE: Record<string, unknown> = { // Hoisted: the factory of `vi.mock` below reads it when the module loads.
const FIXTURE = vi.hoisted((): Record<string, unknown> => ({
"docs.import.subsections": [ "docs.import.subsections": [
{ {
id: "import-export", id: "import-export",
@ -22,16 +23,14 @@ const FIXTURE: Record<string, unknown> = {
steps: ["Expected header"], steps: ["Expected header"],
}, },
], ],
};
vi.mock("react-i18next", () => ({
useTranslation: vi.fn(() => ({
// i18next returns the key itself when nothing is declared under it.
t: (key: string) => (key in FIXTURE ? FIXTURE[key] : key),
i18n: { language: "fr" },
})),
})); }));
// 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"; import DocsSubsections, { readSubsections } from "./DocsSubsections";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -4,12 +4,7 @@ import { describe, it, expect, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn() })); vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn() }));
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({
t: (k: string, o?: Record<string, unknown>) =>
o ? `${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}]` : k,
}),
}));
import LicenseKeyForm from "./LicenseKeyForm"; import LicenseKeyForm from "./LicenseKeyForm";
@ -30,18 +25,18 @@ function render(validationError: string | null, busy = false): string {
describe("LicenseKeyForm", () => { describe("LicenseKeyForm", () => {
it("names a refused key in the app language", () => { it("names a refused key in the app language", () => {
const html = render('{"code":"invalid_prefix"}'); const html = render('{"code":"invalid_prefix"}');
expect(html).toContain("license.errors.invalidPrefix"); expect(html).toContain("t(license.errors.invalidPrefix)");
expect(html).not.toContain("{&quot;code&quot;"); expect(html).not.toContain("{&quot;code&quot;");
}); });
it("never prints a raw backend string", () => { it("never prints a raw backend string", () => {
const html = render("License key must start with SR-BASE- or SR-PREMIUM-"); const html = render("License key must start with SR-BASE- or SR-PREMIUM-");
expect(html).toContain("license.errors.unknown"); expect(html).toContain("t(license.errors.unknown)");
expect(html).not.toContain("License key must start"); expect(html).not.toContain("License key must start");
}); });
it("shows no error line before a submission is refused", () => { it("shows no error line before a submission is refused", () => {
expect(render(null)).not.toContain("license.errors."); expect(render(null)).not.toContain("t(license.errors.");
}); });
it("blocks the submission while a machine activation runs", () => { it("blocks the submission while a machine activation runs", () => {

View file

@ -11,6 +11,10 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
// Mocks — declared before imports to satisfy vi.mock hoisting // Mocks — declared before imports to satisfy vi.mock hoisting
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// React's hooks from the shared harness (#409): the toggle renders nothing while
// it loads, so the render test below presets its `loading` slot.
vi.mock("react", async (orig) => (await import("../../test/hookHarness")).fakeReact(await orig()));
vi.mock("../../hooks/useIsPremium", () => ({ vi.mock("../../hooks/useIsPremium", () => ({
useIsPremium: vi.fn(), useIsPremium: vi.fn(),
})); }));
@ -21,16 +25,16 @@ vi.mock("../../services/userPreferenceService", () => ({
deletePreference: vi.fn(), deletePreference: vi.fn(),
})); }));
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: vi.fn(() => ({
t: (key: string) => key,
})),
}));
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Imports (after mock declarations) // Imports (after mock declarations)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
import type { ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { hookHarness } from "../../test/hookHarness";
import { PriceFetchConsentToggle } from "./PriceFetchConsentToggle";
import { useIsPremium } from "../../hooks/useIsPremium"; import { useIsPremium } from "../../hooks/useIsPremium";
import { import {
getPreference, getPreference,
@ -42,6 +46,7 @@ import {
// Helpers // Helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const harness = hookHarness();
const mockUseIsPremium = vi.mocked(useIsPremium); const mockUseIsPremium = vi.mocked(useIsPremium);
const mockGetPreference = vi.mocked(getPreference); const mockGetPreference = vi.mocked(getPreference);
const mockSetPreference = vi.mocked(setPreference); const mockSetPreference = vi.mocked(setPreference);
@ -210,3 +215,39 @@ describe("PriceFetchConsentToggle — premium guard", () => {
expect(buttonDisabled).toBe(false); expect(buttonDisabled).toBe(false);
}); });
}); });
// ---------------------------------------------------------------------------
// Test: what the toggle displays (#389)
// ---------------------------------------------------------------------------
describe("PriceFetchConsentToggle — displayed texts", () => {
beforeEach(() => {
vi.resetAllMocks();
harness.clear();
});
/** One render once loaded: slots by call order, hasConsent, showConfirm, loading. */
function markup(premium: boolean, consent: boolean, confirming: boolean): string {
setPremium(premium);
harness.slots[0] = consent;
harness.slots[1] = confirming;
harness.slots[2] = false;
return renderToStaticMarkup(harness.render(() => PriceFetchConsentToggle() as ReactNode));
}
it("names the section, the consent row and the not-premium tooltip through t", () => {
const html = markup(false, false, false);
expect(html).toContain(">t(settings.privacy.title)<");
expect(html).toContain(">t(settings.privacy.priceFetchConsent.label)<");
expect(html).toContain(">t(settings.privacy.priceFetchConsent.description)<");
expect(html).toContain('title="t(settings.privacy.priceFetchConsent.notPremium)"');
});
it("names the revoke confirmation through t", () => {
const html = markup(true, true, true);
expect(html).toContain('aria-label="t(settings.privacy.priceFetchConsent.revokeButton)"');
expect(html).toContain(">t(settings.privacy.priceFetchConsent.confirmRevoke)<");
expect(html).toContain(">t(common.cancel)<");
expect(html).toContain(">t(settings.privacy.priceFetchConsent.revokeButton)<");
});
});

View file

@ -16,13 +16,7 @@ vi.mock("../../services/logService", async (importOriginal) => ({
})); }));
let language = "fr"; let language = "fr";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock({ language: () => language }));
useTranslation: () => ({
t: (k: string, o?: Record<string, unknown>) =>
o ? `${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}]` : k,
i18n: { language },
}),
}));
// The locked screen and the Updates card read one thing from the license context: // The locked screen and the Updates card read one thing from the license context:
// whether the stored key was revoked (#365). // whether the stored key was revoked (#365).
@ -69,19 +63,19 @@ describe("UpsellGate with a revoked key (#365)", () => {
); );
it("says nothing about a revocation for a key that is not revoked", () => { it("says nothing about a revocation for a key that is not revoked", () => {
expect(gate()).not.toContain("upsell.revoked"); expect(gate()).not.toContain("t(upsell.revoked)");
}); });
it("names the revocation between the feature description and the CTAs", () => { it("names the revocation between the feature description and the CTAs", () => {
licenseRevoked = true; licenseRevoked = true;
const html = gate(); const html = gate();
const line = html.indexOf("upsell.revoked"); const line = html.indexOf("t(upsell.revoked)");
expect(line).toBeGreaterThan(html.indexOf("upsell.features.budget")); expect(line).toBeGreaterThan(html.indexOf("t(upsell.features.budget)"));
expect(line).toBeLessThan(html.indexOf("upsell.ctaGet")); expect(line).toBeLessThan(html.indexOf("t(upsell.ctaGet["));
expect(html.split("upsell.revoked")).toHaveLength(2); expect(html.split("t(upsell.revoked)")).toHaveLength(2);
// The way out stays: buying, or entering another key. // The way out stays: buying, or entering another key.
expect(html).toContain("upsell.ctaGet[tier=license.editions.base]"); expect(html).toContain("t(upsell.ctaGet[tier=t(license.editions.base)])");
expect(html).toContain("upsell.ctaHaveKey"); expect(html).toContain("t(upsell.ctaHaveKey)");
}); });
}); });
@ -92,10 +86,10 @@ describe("UpsellGate wiring", () => {
<UpsellGate feature="budget" requiredTier="base" /> <UpsellGate feature="budget" requiredTier="base" />
</MemoryRouter>, </MemoryRouter>,
); );
expect(html).toContain("upsell.ctaGet[tier=license.editions.base]"); expect(html).toContain("t(upsell.ctaGet[tier=t(license.editions.base)])");
expect(html).not.toContain("disabled"); expect(html).not.toContain("disabled");
expect(html).not.toContain("upsell.premiumNotForSale"); expect(html).not.toContain("t(upsell.premiumNotForSale)");
expect(html).toContain("upsell.ctaHaveKey"); expect(html).toContain("t(upsell.ctaHaveKey)");
}); });
it("Premium screen: disabled button with the not-for-sale note", () => { it("Premium screen: disabled button with the not-for-sale note", () => {
@ -105,14 +99,14 @@ describe("UpsellGate wiring", () => {
</MemoryRouter>, </MemoryRouter>,
); );
expect(html).toContain('disabled=""'); expect(html).toContain('disabled=""');
expect(html).toContain("upsell.ctaGet[tier=license.editions.premium]"); expect(html).toContain("t(upsell.ctaGet[tier=t(license.editions.premium)])");
expect(html).toContain("upsell.premiumNotForSale"); expect(html).toContain("t(upsell.premiumNotForSale)");
}); });
it("outside any router (ProfileSelectionPage, ErrorPage): renders, without I already have a key", () => { it("outside any router (ProfileSelectionPage, ErrorPage): renders, without I already have a key", () => {
const html = renderToStaticMarkup(<UpsellGate feature="multi-profile" requiredTier="base" />); const html = renderToStaticMarkup(<UpsellGate feature="multi-profile" requiredTier="base" />);
expect(html).toContain("upsell.ctaGet"); expect(html).toContain("t(upsell.ctaGet[tier=t(license.editions.base)])");
expect(html).not.toContain("upsell.ctaHaveKey"); expect(html).not.toContain("t(upsell.ctaHaveKey)");
}); });
it("forwards onNavigate to I already have a key only", () => { it("forwards onNavigate to I already have a key only", () => {
@ -157,11 +151,11 @@ describe("UpdateCard in the free edition", () => {
<UpdateCard /> <UpdateCard />
</MemoryRouter>, </MemoryRouter>,
); );
expect(html).toContain("settings.updates.notEntitled"); expect(html).toContain("t(settings.updates.notEntitled)");
expect(html).toContain("upsell.ctaGet[tier=license.editions.base]"); expect(html).toContain("t(upsell.ctaGet[tier=t(license.editions.base)])");
expect(html).toContain("upsell.ctaHaveKey"); expect(html).toContain("t(upsell.ctaHaveKey)");
expect(html).toContain("settings.updates.downloadLatest"); expect(html).toContain("t(settings.updates.downloadLatest)");
expect(html).not.toContain("upsell.revoked"); expect(html).not.toContain("t(upsell.revoked)");
}); });
it("names a revoked key between the message and the CTAs (#365)", () => { it("names a revoked key between the message and the CTAs (#365)", () => {
@ -171,9 +165,9 @@ describe("UpdateCard in the free edition", () => {
<UpdateCard /> <UpdateCard />
</MemoryRouter>, </MemoryRouter>,
); );
const line = html.indexOf("upsell.revoked"); const line = html.indexOf("t(upsell.revoked)");
expect(line).toBeGreaterThan(html.indexOf("settings.updates.notEntitled")); expect(line).toBeGreaterThan(html.indexOf("t(settings.updates.notEntitled)"));
expect(line).toBeLessThan(html.indexOf("upsell.ctaGet")); expect(line).toBeLessThan(html.indexOf("t(upsell.ctaGet["));
expect(html.split("upsell.revoked")).toHaveLength(2); expect(html.split("t(upsell.revoked)")).toHaveLength(2);
}); });
}); });

View file

@ -5,13 +5,7 @@
import { describe, it, expect, vi } from "vitest"; import { describe, it, expect, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({
t: (k: string, o?: Record<string, unknown>) =>
o ? `${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}]` : k,
i18n: { language: "fr" },
}),
}));
import TransactionTable, { TRANSACTION_CATEGORY_TARGET } from "./TransactionTable"; import TransactionTable, { TRANSACTION_CATEGORY_TARGET } from "./TransactionTable";
import type { Category, TransactionRow } from "../../shared/types"; import type { Category, TransactionRow } from "../../shared/types";
@ -66,11 +60,11 @@ const markup = (rows: TransactionRow[], onboardingTarget?: boolean) =>
/** The rows of the table's body, in order. */ /** The rows of the table's body, in order. */
const bodyRows = (html: string) => html.split("<tbody")[1].split("<tr").slice(1); const bodyRows = (html: string) => html.split("<tbody")[1].split("<tr").slice(1);
/** The highlighted wrapper around a closed combobox, whose input shows `value`. */ /** The highlighted wrapper around a closed combobox, whose input shows `value` (escaped). */
const ringedCategory = (value: string) => const ringedCategory = (value: string) =>
new RegExp( new RegExp(
`<div class="rounded-lg" data-onboarding-target="${TRANSACTION_CATEGORY_TARGET}" ${HIGHLIGHT}>` + `<div class="rounded-lg" data-onboarding-target="${TRANSACTION_CATEGORY_TARGET}" ${HIGHLIGHT}>` +
`<div class="relative"><input [^>]*role="combobox"[^>]*value="${value}"[^>]*/></div></div>`, `<div class="relative"><input [^>]*role="combobox"[^>]*value="${value.replace(/[()[\]]/g, "\\$&")}"[^>]*/></div></div>`,
); );
describe("TransactionTable — getting-started target (#399)", () => { describe("TransactionTable — getting-started target (#399)", () => {
@ -79,7 +73,7 @@ describe("TransactionTable — getting-started target (#399)", () => {
expect(count(html, HIGHLIGHT)).toBe(1); expect(count(html, HIGHLIGHT)).toBe(1);
const [first, ...others] = bodyRows(html); const [first, ...others] = bodyRows(html);
expect(first).toContain("IGA 123"); expect(first).toContain("IGA 123");
expect(first).toMatch(ringedCategory("transactions.table.noCategory")); expect(first).toMatch(ringedCategory("t(transactions.table.noCategory)"));
expect(others).toHaveLength(2); expect(others).toHaveLength(2);
for (const other of others) { for (const other of others) {
expect(other).not.toContain("data-onboarding"); expect(other).not.toContain("data-onboarding");
@ -102,7 +96,7 @@ describe("TransactionTable — getting-started target (#399)", () => {
it("highlights nothing without a row: the card stays, without a ring", () => { it("highlights nothing without a row: the card stays, without a ring", () => {
const html = markup([], true); const html = markup([], true);
expect(html).toContain("transactions.noTransactions"); expect(html).toContain("t(transactions.noTransactions)");
expect(html).not.toContain("data-onboarding"); expect(html).not.toContain("data-onboarding");
}); });
}); });

View file

@ -9,9 +9,7 @@ import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
import type { OnboardingValue } from "../hooks/useOnboardingChecklist"; import type { OnboardingValue } from "../hooks/useOnboardingChecklist";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../test/i18nMock")).i18nMock());
useTranslation: () => ({ t: (k: string) => k, i18n: { language: "fr" } }),
}));
let onboarding: OnboardingValue; let onboarding: OnboardingValue;
vi.mock("../contexts/OnboardingContext", () => ({ useOnboarding: () => onboarding })); vi.mock("../contexts/OnboardingContext", () => ({ useOnboarding: () => onboarding }));
@ -116,7 +114,7 @@ describe("DashboardPage on the shared path (#352, #396, #397)", () => {
it("renders only its header while the path first loads", () => { it("renders only its header while the path first loads", () => {
onboarding = path({ status: "loading", checklist: null, counters: null }); onboarding = path({ status: "loading", checklist: null, counters: null });
const html = render(); const html = render();
expect(html).toContain("dashboard.title"); expect(html).toContain("t(dashboard.title)");
expect(html).not.toContain('data-marker="empty-state"'); expect(html).not.toContain('data-marker="empty-state"');
expect(html).not.toContain('data-marker="filters"'); expect(html).not.toContain('data-marker="filters"');
}); });