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
12 changed files with 15 additions and 71 deletions
Showing only changes of commit 063f6c3d7d - Show all commits

View file

@ -10,9 +10,7 @@ import { renderToStaticMarkup } from "react-dom/server";
import type { BudgetYearRow } from "../../shared/types"; import type { BudgetYearRow } from "../../shared/types";
import type { CollapseAccessors } from "../../utils/collapsibleRows"; import type { CollapseAccessors } from "../../utils/collapsibleRows";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({ t: (k: string) => `t(${k})`, i18n: { language: "fr" } }),
}));
const harness = vi.hoisted(() => ({ const harness = vi.hoisted(() => ({
/** Keys of the open groups (`p:<category id>`): the grid opens collapsed. */ /** Keys of the open groups (`p:<category id>`): the grid opens collapsed. */

View file

@ -16,9 +16,7 @@ const ctx = vi.hoisted(() => ({
/** Keys of the open groups (`p:<category id>`): the grid opens collapsed. */ /** Keys of the open groups (`p:<category id>`): the grid opens collapsed. */
expanded: new Set<string>(), expanded: new Set<string>(),
})); }));
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({ t: (k: string) => `t(${k})`, i18n: { language: "fr" } }),
}));
vi.mock("../../hooks/useCollapsibleGroups", async () => { vi.mock("../../hooks/useCollapsibleGroups", async () => {
const { collapsibleKeys, isCollapsedFor, visibleRows } = await import("../../utils/collapsibleRows"); const { collapsibleKeys, isCollapsedFor, visibleRows } = await import("../../utils/collapsibleRows");
return { return {

View file

@ -7,13 +7,7 @@ import type { ReactElement, ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import { MemoryRouter } from "react-router-dom"; import { MemoryRouter } from "react-router-dom";
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({
t: (k: string, o?: Record<string, unknown>) =>
o ? `t(${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}])` : `t(${k})`,
i18n: { language: "fr" },
}),
}));
import DashboardEmptyState from "./DashboardEmptyState"; import DashboardEmptyState from "./DashboardEmptyState";
import OnboardingChecklist from "./OnboardingChecklist"; import OnboardingChecklist from "./OnboardingChecklist";

View file

@ -13,13 +13,7 @@ import { MemoryRouter, Route, Routes } from "react-router-dom";
// plugins) and useEntitlement (→ LicenseContext): stub both. // plugins) and useEntitlement (→ LicenseContext): stub both.
vi.mock("../../services/db", () => ({ getDb: vi.fn(), withTransaction: vi.fn() })); vi.mock("../../services/db", () => ({ getDb: vi.fn(), withTransaction: vi.fn() }));
vi.mock("../../contexts/LicenseContext", () => ({ useLicenseContext: vi.fn() })); vi.mock("../../contexts/LicenseContext", () => ({ useLicenseContext: 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 ? `t(${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}])` : `t(${k})`,
i18n: { language: "fr" },
}),
}));
vi.mock("./Sidebar", () => ({ default: () => <nav>sidebar</nav> })); vi.mock("./Sidebar", () => ({ default: () => <nav>sidebar</nav> }));
let onboarding: OnboardingValue; let onboarding: OnboardingValue;

View file

@ -7,15 +7,9 @@ import { isValidElement, type ReactElement, type ReactNode } from "react";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import { Link, MemoryRouter } from "react-router-dom"; import { Link, MemoryRouter } from "react-router-dom";
// The fake `t` marks what it translates, `t(key[options])`: a string that // The shared fake `t` (src/test/i18nMock.ts) marks what it translates, `t(key[options])`: a string that
// skipped `t` would show unmarked. // skipped `t` would show unmarked.
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({
t: (k: string, o?: Record<string, unknown>) =>
o ? `t(${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}])` : `t(${k})`,
i18n: { language: "fr" },
}),
}));
import OnboardingBanner, { import OnboardingBanner, {
GETTING_STARTED_GUIDE, GETTING_STARTED_GUIDE,

View file

@ -7,15 +7,9 @@ import { createRef, isValidElement, type ReactElement, type ReactNode } from "re
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import { Link, MemoryRouter } from "react-router-dom"; import { Link, MemoryRouter } from "react-router-dom";
// The fake `t` marks what it translates, `t(key[options])`: a string that // The shared fake `t` (src/test/i18nMock.ts) marks what it translates, `t(key[options])`: a string that
// skipped `t` would show unmarked. // skipped `t` would show unmarked.
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({
t: (k: string, o?: Record<string, unknown>) =>
o ? `t(${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}])` : `t(${k})`,
i18n: { language: "fr" },
}),
}));
import StepCard, { type StepCardProps } from "./StepCard"; import StepCard, { type StepCardProps } from "./StepCard";
import { GETTING_STARTED_GUIDE } from "./OnboardingBanner"; import { GETTING_STARTED_GUIDE } from "./OnboardingBanner";

View file

@ -10,16 +10,10 @@ vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn() }));
vi.mock("@tauri-apps/plugin-opener", () => ({ openUrl: vi.fn(() => Promise.resolve()) })); vi.mock("@tauri-apps/plugin-opener", () => ({ openUrl: vi.fn(() => Promise.resolve()) }));
let language = "fr"; let language = "fr";
// The fake `t` marks what it translates, `t(key[options])`: the assertions written in that form // The shared fake `t` (src/test/i18nMock.ts) marks what it translates, `t(key[options])`: the assertions written in that form
// fail on a string that skipped `t` (#387 review). Substring assertions on a bare key still read // fail on a string that skipped `t` (#387 review). Substring assertions on a bare key still read
// the key inside the mark. // the key inside the mark.
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 ? `t(${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}])` : `t(${k})`,
i18n: { language },
}),
}));
let context: Record<string, unknown>; let context: Record<string, unknown>;
vi.mock("../../contexts/LicenseContext", async (importOriginal) => ({ vi.mock("../../contexts/LicenseContext", async (importOriginal) => ({

View file

@ -11,13 +11,7 @@ import { MemoryRouter } from "react-router-dom";
// The budget, collapse and preference services pull the database: stub the connection. // The budget, collapse and preference services pull the database: stub the connection.
vi.mock("../services/db", () => ({ getDb: vi.fn(), withTransaction: vi.fn() })); vi.mock("../services/db", () => ({ getDb: vi.fn(), withTransaction: 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 ? `t(${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}])` : `t(${k})`,
i18n: { language: "fr" },
}),
}));
vi.mock("../components/shared/PageHelp", () => ({ PageHelp: () => null })); vi.mock("../components/shared/PageHelp", () => ({ PageHelp: () => null }));
const harness = vi.hoisted(() => ({ budget: null as unknown, onboarding: null as unknown })); const harness = vi.hoisted(() => ({ budget: null as unknown, onboarding: null as unknown }));

View file

@ -19,9 +19,7 @@ vi.mock("react-router-dom", async (importOriginal) => ({
...(await importOriginal<typeof import("react-router-dom")>()), ...(await importOriginal<typeof import("react-router-dom")>()),
useLocation: () => ({ pathname: ctx.pathname }), useLocation: () => ({ pathname: ctx.pathname }),
})); }));
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../test/i18nMock")).i18nMock());
useTranslation: () => ({ t: (k: string) => `t(${k})`, i18n: { language: "fr" } }),
}));
// The page's modules pull the database: stub the connection. // The page's modules pull the database: stub the connection.
vi.mock("../services/db", () => ({ getDb: vi.fn(), withTransaction: vi.fn() })); vi.mock("../services/db", () => ({ getDb: vi.fn(), withTransaction: vi.fn() }));
vi.mock("../contexts/OnboardingContext", () => ({ useOnboarding: () => ctx.onboarding })); vi.mock("../contexts/OnboardingContext", () => ({ useOnboarding: () => ctx.onboarding }));

View file

@ -12,13 +12,7 @@ import { MemoryRouter } from "react-router-dom";
// Importing the real wizard module (for its initial state) pulls the import // Importing the real wizard module (for its initial state) pulls the import
// services, then the database: stub the connection. // services, then the database: stub the connection.
vi.mock("../services/db", () => ({ getDb: vi.fn(), withTransaction: vi.fn() })); vi.mock("../services/db", () => ({ getDb: vi.fn(), withTransaction: 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 ? `t(${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}])` : `t(${k})`,
i18n: { language: "fr" },
}),
}));
const harness = vi.hoisted(() => ({ wizard: null as unknown, onboarding: null as unknown })); const harness = vi.hoisted(() => ({ wizard: null as unknown, onboarding: null as unknown }));

View file

@ -12,13 +12,7 @@ import type { TransactionFilters, TransactionRow } from "../shared/types";
// The page's services pull the database: stub the connection. // The page's services pull the database: stub the connection.
vi.mock("../services/db", () => ({ getDb: vi.fn(), withTransaction: vi.fn() })); vi.mock("../services/db", () => ({ getDb: vi.fn(), withTransaction: 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 ? `t(${k}[${Object.entries(o).map(([a, b]) => `${a}=${String(b)}`).join(",")}])` : `t(${k})`,
i18n: { language: "fr" },
}),
}));
vi.mock("../components/shared/PageHelp", () => ({ PageHelp: () => null })); vi.mock("../components/shared/PageHelp", () => ({ PageHelp: () => null }));
const harness = vi.hoisted(() => ({ const harness = vi.hoisted(() => ({

View file

@ -11,9 +11,7 @@ import { isValidElement, type ReactElement, type ReactNode } from "react";
vi.mock("react", async (orig) => (await import("../../test/hookHarness")).fakeReact(await orig())); vi.mock("react", async (orig) => (await import("../../test/hookHarness")).fakeReact(await orig()));
const ctx = vi.hoisted(() => ({ onboarding: null as unknown })); const ctx = vi.hoisted(() => ({ onboarding: null as unknown }));
vi.mock("react-i18next", () => ({ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nMock());
useTranslation: () => ({ t: (k: string) => `t(${k})`, i18n: { language: "fr" } }),
}));
vi.mock("../../contexts/OnboardingContext", () => ({ vi.mock("../../contexts/OnboardingContext", () => ({
useOnboarding: () => ctx.onboarding, useOnboarding: () => ctx.onboarding,
})); }));