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>
107 lines
4.9 KiB
TypeScript
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",
|
|
);
|
|
}
|
|
});
|
|
});
|