Simpl-Resultat/src/utils/onboardingPath.locales.test.ts
le king fu 63d8ba7589 feat(onboarding): Transactions card suggests Add as keyword, Budget card names leaves and the Annual column
Both Transactions sentences name the context menu's "Add as keyword" and
carry the advice on the word and the boxes (D18); both Budget sentences say
only last-level rows take an amount and what the Annual column does. The
labels go through StepCardText.labels, quoted in the locale files (D11).
The keyword dialog is unchanged; a test pins that it marks the transactions
as categorized by hand, so the step still checks off through it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:12:25 -04:00

107 lines
4.9 KiB
TypeScript

// The Import step card's sentences, as the locale files write them (#422).
//
// A sentence names a control through an interpolation (`StepCardText.labels`,
// translated by `StepCard`); the label then reads between quotation marks,
// « » in French and “ ” in English (D11), so the reader tells the button's
// name from the sentence around it. The folder name of `noSources` is quoted
// the same way. These tests hold the real files to that rule.
import { describe, it, expect } from "vitest";
import fr from "../i18n/locales/fr.json";
import en from "../i18n/locales/en.json";
import { budgetCardModel, transactionsCardModel } from "./onboardingPath";
type Cards = Record<string, string>;
const IMPORT_CARDS: Record<"fr" | "en", Cards> = {
fr: (fr as { onboarding: { card: { import: Cards } } }).onboarding.card.import,
en: (en as { onboarding: { card: { import: Cards } } }).onboarding.card.import,
};
const QUOTED: Record<"fr" | "en", (name: string) => RegExp> = {
fr: (name) => new RegExp(`« \\{\\{${name}\\}\\} »`),
en: (name) => new RegExp(`“\\{\\{${name}\\}\\}”`),
};
/** The names of the plain interpolations of a sentence (`{{name}}`). */
function interpolations(sentence: string): string[] {
return [...sentence.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]);
}
describe("Import step card sentences quote every control they name (#422)", () => {
it.each(["fr", "en"] as const)("%s: each interpolation sits between quotation marks", (lang) => {
const cards = IMPORT_CARDS[lang];
let named = 0;
for (const [key, sentence] of Object.entries(cards)) {
for (const name of interpolations(sentence)) {
named += 1;
expect(sentence, `onboarding.card.import.${key}: ${name}`).toMatch(QUOTED[lang](name));
}
}
// noFolder 1, noSources 3, sourceConfig 1, filePreview 2, duplicateCheck 1, confirm 1.
expect(named).toBe(9);
});
it.each(["fr", "en"] as const)("%s: the preview names the flip and the next button", (lang) => {
expect(interpolations(IMPORT_CARDS[lang].filePreview).sort()).toEqual(["button", "flip"]);
});
});
// The Transactions and Budget cards name their controls the same way (#430):
// "Add as keyword" on both Transactions sentences, "Expand all" and the
// "Annual" column on the Budget ones.
type PageCards = { transactions: Cards; budget: Cards };
const PAGE_CARDS: Record<"fr" | "en", PageCards> = {
fr: (fr as { onboarding: { card: PageCards } }).onboarding.card,
en: (en as { onboarding: { card: PageCards } }).onboarding.card,
};
/** Every sentence of the two cards, with the model that names its controls. */
const PAGE_SENTENCES = [
{ page: "transactions", key: "uncategorized", text: transactionsCardModel({ uncategorizedOnly: true }, null).text },
{ page: "transactions", key: "all", text: transactionsCardModel({ uncategorizedOnly: false }, null).text },
{ page: "budget", key: "collapsed", text: budgetCardModel({ target: "expandAll" }, null).text },
{ page: "budget", key: "flat", text: budgetCardModel({ target: "cell" }, null).text },
] as const;
/** The names of every interpolation of a sentence, formatted ones included (`{{filter, lowercase}}`). */
function allInterpolations(sentence: string): string[] {
return [...sentence.matchAll(/\{\{(\w+)(?:,[^}]*)?\}\}/g)].map((match) => match[1]);
}
describe("Transactions and Budget step card sentences quote every control they name (#430)", () => {
it.each(["fr", "en"] as const)("%s: each control interpolation sits between quotation marks", (lang) => {
let named = 0;
for (const page of ["transactions", "budget"] as const) {
for (const [key, sentence] of Object.entries(PAGE_CARDS[lang][page])) {
for (const name of interpolations(sentence)) {
named += 1;
expect(sentence, `onboarding.card.${page}.${key}: ${name}`).toMatch(QUOTED[lang](name));
}
}
}
// transactions: uncategorized 1, all 1; budget: collapsed 2, flat 1.
expect(named).toBe(5);
});
it.each(["fr", "en"] as const)("%s: the model labels exactly the interpolations of its sentence", (lang) => {
for (const { page, key, text } of PAGE_SENTENCES) {
expect(text?.key).toBe(`onboarding.card.${page}.${key}`);
const sentence = PAGE_CARDS[lang][page][key];
expect(Object.keys(text?.labels ?? {}).sort(), `${page}.${key}`).toEqual(
allInterpolations(sentence).sort(),
);
}
});
it.each(["fr", "en"] as const)("%s: both Transactions sentences name 'Add as keyword', both Budget ones the 'Annual' column", (lang) => {
for (const { page, key, text } of PAGE_SENTENCES) {
const expected = page === "transactions" ? "addKeyword" : "annual";
expect(interpolations(PAGE_CARDS[lang][page][key]), `${page}.${key}`).toContain(expected);
expect(text?.labels?.[expected], `${page}.${key}`).toBe(
page === "transactions" ? "reports.keyword.addFromTransaction" : "budget.annual",
);
}
});
});