test: pin the surviving marked-t keys and document the known test limits (#437) #473
6 changed files with 105 additions and 14 deletions
|
|
@ -11,6 +11,10 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
|
|||
// Mocks — declared before imports to satisfy vi.mock hoisting
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// React's hooks from the shared harness (#409): the consent dialog opens on a
|
||||
// click (internal state), so the render test below presets its slot (#437).
|
||||
vi.mock("react", async (orig) => (await import("../../test/hookHarness")).fakeReact(await orig()));
|
||||
|
||||
vi.mock("../../hooks/useIsPremium", () => ({
|
||||
useIsPremium: vi.fn(),
|
||||
}));
|
||||
|
|
@ -40,6 +44,7 @@ vi.mock("lucide-react", () => ({
|
|||
// Imports (after mock declarations)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useIsPremium } from "../../hooks/useIsPremium";
|
||||
import { prices } from "../../services/balance.service";
|
||||
import type { PriceResult } from "../../services/balance.service";
|
||||
|
|
@ -48,6 +53,7 @@ import {
|
|||
setPreference,
|
||||
} from "../../services/userPreferenceService";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { hookHarness } from "../../test/hookHarness";
|
||||
import PriceFetchControl, {
|
||||
__resetBestEffortDismissForTests,
|
||||
} from "./PriceFetchControl";
|
||||
|
|
@ -56,6 +62,7 @@ import PriceFetchControl, {
|
|||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const harness = hookHarness();
|
||||
const mockUseIsPremium = vi.mocked(useIsPremium);
|
||||
const mockFetchPrice = vi.mocked(prices.fetchPrice);
|
||||
const mockGetPreference = vi.mocked(getPreference);
|
||||
|
|
@ -363,18 +370,35 @@ describe("PriceFetchControl — displayed texts", () => {
|
|||
__resetBestEffortDismissForTests();
|
||||
vi.resetAllMocks();
|
||||
setPremium(true);
|
||||
harness.clear();
|
||||
});
|
||||
|
||||
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()}
|
||||
/>,
|
||||
/**
|
||||
* One render of a stock's control, its hooks from the harness. `consentOpen`
|
||||
* presets slot 0, the control's first hook (`showConsentModal`). The slots
|
||||
* follow the order of the control's hook calls: a hook of any kind added
|
||||
* before it shifts them without a word, and the preset lands on another hook
|
||||
* (one slot per hook call: the `slot` helper of `fakeReact`,
|
||||
* src/test/hookHarness.ts).
|
||||
*/
|
||||
function markup(consentOpen = false): string {
|
||||
if (consentOpen) harness.slots[0] = true;
|
||||
return renderToStaticMarkup(
|
||||
harness.render(
|
||||
() =>
|
||||
PriceFetchControl({
|
||||
symbol: "AAPL",
|
||||
date: "2026-04-25",
|
||||
categoryKind: "priced",
|
||||
assetType: "stock",
|
||||
onPriceFetched: vi.fn(),
|
||||
}) as ReactNode,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
it("names the fetch button and the stock best-effort notice through t", () => {
|
||||
const html = markup();
|
||||
expect(html).toContain('aria-label="t(balance.priceFetching.button)"');
|
||||
expect(html).toContain(">t(balance.priceFetching.button)<");
|
||||
expect(html).toContain(">t(balance.priceFetching.bestEffortNotice)<");
|
||||
|
|
@ -382,4 +406,15 @@ describe("PriceFetchControl — displayed texts", () => {
|
|||
expect(html).not.toContain(">best-effort<");
|
||||
expect(html).toContain('aria-label="t(common.close)"');
|
||||
});
|
||||
|
||||
it("names the consent dialog through t once it is open (#437)", () => {
|
||||
expect(markup()).not.toContain('role="dialog"');
|
||||
harness.clear();
|
||||
const html = markup(true);
|
||||
expect(html).toContain('role="dialog"');
|
||||
expect(html).toContain(">t(balance.priceFetching.consent.title)<");
|
||||
expect(html).toContain(">t(balance.priceFetching.consent.body)<");
|
||||
expect(html).toContain(">t(balance.priceFetching.consent.decline)<");
|
||||
expect(html).toContain(">t(balance.priceFetching.consent.accept)<");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Render tests for the key entry form of the License card (#347). No jsdom needed:
|
||||
// react-dom/server renders in vitest's node environment, and `t` echoes its key.
|
||||
// react-dom/server renders in vitest's node environment, and the marked `t` of
|
||||
// src/test/i18nMock.ts renders `t(key)`.
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
|
|
@ -44,4 +45,10 @@ describe("LicenseKeyForm", () => {
|
|||
expect(render(null, true)).toMatch(/<button type="submit" disabled=""/);
|
||||
expect(render(null, false)).not.toMatch(/<button type="submit" disabled=""/);
|
||||
});
|
||||
|
||||
it("names the key field and the submit button through t (#437)", () => {
|
||||
const html = render(null);
|
||||
expect(html).toContain('placeholder="t(license.keyPlaceholder)"');
|
||||
expect(html).toMatch(/<button type="submit"[^>]*>t\(license\.activate\)<\/button>/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ vi.mock("react-i18next", async () => (await import("../../test/i18nMock")).i18nM
|
|||
import TransactionTable, { TRANSACTION_CATEGORY_TARGET } from "./TransactionTable";
|
||||
import type { Category, TransactionRow } from "../../shared/types";
|
||||
import type { SplitAccess } from "../../shared/splitGate";
|
||||
import type { LinkedTransferTooltipRow } from "../../services/balance.service";
|
||||
|
||||
const HIGHLIGHT = 'data-onboarding-highlight="true"';
|
||||
const count = (html: string, needle: string) => html.split(needle).length - 1;
|
||||
|
|
@ -46,7 +47,12 @@ const row = (
|
|||
/** The guided entry: uncategorized rows only. */
|
||||
const UNCATEGORIZED = [row(1, "IGA 123"), row(2, "METRO 456"), row(3, "SAQ 789")];
|
||||
|
||||
const markup = (rows: TransactionRow[], onboardingTarget?: boolean, splitAccess: SplitAccess = "open") =>
|
||||
const markup = (
|
||||
rows: TransactionRow[],
|
||||
onboardingTarget?: boolean,
|
||||
splitAccess: SplitAccess = "open",
|
||||
linkedTransfersByTxId?: Map<number, LinkedTransferTooltipRow[]>,
|
||||
) =>
|
||||
renderToStaticMarkup(
|
||||
<TransactionTable
|
||||
rows={rows}
|
||||
|
|
@ -60,6 +66,7 @@ const markup = (rows: TransactionRow[], onboardingTarget?: boolean, splitAccess:
|
|||
onSaveSplit={vi.fn(async () => undefined)}
|
||||
onDeleteSplit={vi.fn(async () => undefined)}
|
||||
splitAccess={splitAccess}
|
||||
linkedTransfersByTxId={linkedTransfersByTxId}
|
||||
onboardingTarget={onboardingTarget}
|
||||
/>,
|
||||
);
|
||||
|
|
@ -130,3 +137,22 @@ describe("TransactionTable — the Répartition button (#369)", () => {
|
|||
expect(buttons[0]).toContain("text-orange-500");
|
||||
});
|
||||
});
|
||||
|
||||
describe("TransactionTable — texts that go through t (#437)", () => {
|
||||
it("names the transfer icon through t, on the linked row only", () => {
|
||||
const links = new Map<number, LinkedTransferTooltipRow[]>([
|
||||
[2, [{ transaction_id: 2, account_id: 5, account_name: "CELI", direction: "in" }]],
|
||||
]);
|
||||
const html = markup(UNCATEGORIZED, undefined, "open", links);
|
||||
const icon = 'aria-label="t(transactions.transferIcon.ariaLabel)"';
|
||||
expect(count(html, icon)).toBe(1);
|
||||
expect(bodyRows(html)[1]).toContain(icon);
|
||||
});
|
||||
|
||||
it("names every row's notes button through t", () => {
|
||||
// The textarea that shows the same key renders for an expanded row only
|
||||
// (internal state): TransactionTable.wiring.test.ts opens one.
|
||||
const html = markup(UNCATEGORIZED);
|
||||
expect(count(html, 'title="t(transactions.notes.placeholder)"')).toBe(UNCATEGORIZED.length);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
// TransactionTable — what the Répartition button opens (#369).
|
||||
// TransactionTable — what the Répartition button opens (#369), and the notes
|
||||
// row its notes button opens (#437).
|
||||
//
|
||||
// A static render never shows a window: they open on a click, through the
|
||||
// table's own state. React's hooks come from the shared harness
|
||||
// (src/test/hookHarness.ts, #409): the table runs as a plain function, its
|
||||
// state survives from one call to the next as between two renders of a mounted
|
||||
// component, and the test clicks through the props the table hands to each
|
||||
// SplitButton, then renders again to read what opened.
|
||||
// SplitButton (or to a row's notes button), then renders again to read what
|
||||
// opened.
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { isValidElement, type ReactElement, type ReactNode } from "react";
|
||||
|
||||
|
|
@ -140,3 +142,18 @@ describe("TransactionTable — the Répartition button's wiring (#369)", () => {
|
|||
expect(splitWindows(upgraded)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("TransactionTable — the notes row (#437)", () => {
|
||||
/** The rows' notes buttons, named by their title (pinned in TransactionTable.render.test.tsx). */
|
||||
const notesButtons = (tree: ReactNode) =>
|
||||
find(tree, "button").filter((b) => b.props.title === "t(transactions.notes.placeholder)");
|
||||
|
||||
it("a row's notes button opens one textarea, named through t", () => {
|
||||
expect(find(render("open"), "textarea")).toHaveLength(0);
|
||||
(notesButtons(render("open"))[1].props.onClick as () => void)();
|
||||
const opened = render("open");
|
||||
const areas = find(opened, "textarea");
|
||||
expect(areas).toHaveLength(1);
|
||||
expect(areas[0].props.placeholder).toBe("t(transactions.notes.placeholder)");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -163,3 +163,9 @@ describe("DashboardPage on the shared path (#352, #396, #397)", () => {
|
|||
expect(html).toContain('data-marker="filters"');
|
||||
});
|
||||
});
|
||||
|
||||
describe("DashboardPage — texts that go through t (#437)", () => {
|
||||
it("names the KPI section through t", () => {
|
||||
expect(render()).toContain(">t(dashboard.kpiSectionTitle)<");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -736,7 +736,7 @@ describe("describePreparation (#390)", () => {
|
|||
expect(withoutFolders("'C:\\a\\b.db' and 'C:\\Users\\Jean Dupont\\d.db'")).toBe("'b.db' and 'd.db'");
|
||||
});
|
||||
|
||||
it.each([["\""], [";"], ["|"], ["`"], ["\n"]])("withoutFolders ends the folders at %j", (stop) => {
|
||||
it.each([["\""], [";"], ["|"], ["`"], ["\n"], ["\r"]])("withoutFolders ends the folders at %j", (stop) => {
|
||||
expect(withoutFolders(`/home/marie claire/x.db${stop} I/O`)).toBe(`x.db${stop} I-O`);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue