test: pin the \r path stop and the surviving marked-t keys (#437)
- db.test.ts: `\r` joins the path stops of withoutFolders (removing it from PATH_STOP now fails). - The six keys whose "t removed" mutation survived the review of PR #435 (comment 23499) are asserted in their marked form: transactions.transferIcon.ariaLabel (a row linked to a transfer) and transactions.notes.placeholder (title of every row's notes button) in TransactionTable.render.test.tsx; license.keyPlaceholder and license.activate in LicenseKeyForm.render.test.tsx; dashboard.kpiSectionTitle in DashboardPage.render.test.tsx; balance.priceFetching.consent.accept in PriceFetchControl.test.tsx, the consent dialog opened by presetting the control's first harness slot (the dialog's title, body and decline texts are pinned with it). - TransactionTable.wiring.test.ts opens a row's notes through its button and pins the textarea placeholder, the second call of the notes key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8a9d9909f9
commit
3e8c79f057
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
|
// 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", () => ({
|
vi.mock("../../hooks/useIsPremium", () => ({
|
||||||
useIsPremium: vi.fn(),
|
useIsPremium: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
@ -40,6 +44,7 @@ vi.mock("lucide-react", () => ({
|
||||||
// Imports (after mock declarations)
|
// Imports (after mock declarations)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
import { useIsPremium } from "../../hooks/useIsPremium";
|
import { useIsPremium } from "../../hooks/useIsPremium";
|
||||||
import { prices } from "../../services/balance.service";
|
import { prices } from "../../services/balance.service";
|
||||||
import type { PriceResult } from "../../services/balance.service";
|
import type { PriceResult } from "../../services/balance.service";
|
||||||
|
|
@ -48,6 +53,7 @@ import {
|
||||||
setPreference,
|
setPreference,
|
||||||
} from "../../services/userPreferenceService";
|
} from "../../services/userPreferenceService";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import { hookHarness } from "../../test/hookHarness";
|
||||||
import PriceFetchControl, {
|
import PriceFetchControl, {
|
||||||
__resetBestEffortDismissForTests,
|
__resetBestEffortDismissForTests,
|
||||||
} from "./PriceFetchControl";
|
} from "./PriceFetchControl";
|
||||||
|
|
@ -56,6 +62,7 @@ import PriceFetchControl, {
|
||||||
// Helpers
|
// Helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const harness = hookHarness();
|
||||||
const mockUseIsPremium = vi.mocked(useIsPremium);
|
const mockUseIsPremium = vi.mocked(useIsPremium);
|
||||||
const mockFetchPrice = vi.mocked(prices.fetchPrice);
|
const mockFetchPrice = vi.mocked(prices.fetchPrice);
|
||||||
const mockGetPreference = vi.mocked(getPreference);
|
const mockGetPreference = vi.mocked(getPreference);
|
||||||
|
|
@ -363,18 +370,35 @@ describe("PriceFetchControl — displayed texts", () => {
|
||||||
__resetBestEffortDismissForTests();
|
__resetBestEffortDismissForTests();
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
setPremium(true);
|
setPremium(true);
|
||||||
|
harness.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("names the fetch button and the stock best-effort notice through t", () => {
|
/**
|
||||||
const html = renderToStaticMarkup(
|
* One render of a stock's control, its hooks from the harness. `consentOpen`
|
||||||
<PriceFetchControl
|
* presets slot 0, the control's first hook (`showConsentModal`). The slots
|
||||||
symbol="AAPL"
|
* follow the order of the control's hook calls: a hook of any kind added
|
||||||
date="2026-04-25"
|
* before it shifts them without a word, and the preset lands on another hook
|
||||||
categoryKind="priced"
|
* (one slot per hook call: the `slot` helper of `fakeReact`,
|
||||||
assetType="stock"
|
* src/test/hookHarness.ts).
|
||||||
onPriceFetched={vi.fn()}
|
*/
|
||||||
/>,
|
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('aria-label="t(balance.priceFetching.button)"');
|
||||||
expect(html).toContain(">t(balance.priceFetching.button)<");
|
expect(html).toContain(">t(balance.priceFetching.button)<");
|
||||||
expect(html).toContain(">t(balance.priceFetching.bestEffortNotice)<");
|
expect(html).toContain(">t(balance.priceFetching.bestEffortNotice)<");
|
||||||
|
|
@ -382,4 +406,15 @@ describe("PriceFetchControl — displayed texts", () => {
|
||||||
expect(html).not.toContain(">best-effort<");
|
expect(html).not.toContain(">best-effort<");
|
||||||
expect(html).toContain('aria-label="t(common.close)"');
|
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:
|
// 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 { describe, it, expect, vi } from "vitest";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
|
||||||
|
|
@ -44,4 +45,10 @@ describe("LicenseKeyForm", () => {
|
||||||
expect(render(null, true)).toMatch(/<button type="submit" disabled=""/);
|
expect(render(null, true)).toMatch(/<button type="submit" disabled=""/);
|
||||||
expect(render(null, false)).not.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 TransactionTable, { TRANSACTION_CATEGORY_TARGET } from "./TransactionTable";
|
||||||
import type { Category, TransactionRow } from "../../shared/types";
|
import type { Category, TransactionRow } from "../../shared/types";
|
||||||
import type { SplitAccess } from "../../shared/splitGate";
|
import type { SplitAccess } from "../../shared/splitGate";
|
||||||
|
import type { LinkedTransferTooltipRow } from "../../services/balance.service";
|
||||||
|
|
||||||
const HIGHLIGHT = 'data-onboarding-highlight="true"';
|
const HIGHLIGHT = 'data-onboarding-highlight="true"';
|
||||||
const count = (html: string, needle: string) => html.split(needle).length - 1;
|
const count = (html: string, needle: string) => html.split(needle).length - 1;
|
||||||
|
|
@ -46,7 +47,12 @@ const row = (
|
||||||
/** The guided entry: uncategorized rows only. */
|
/** The guided entry: uncategorized rows only. */
|
||||||
const UNCATEGORIZED = [row(1, "IGA 123"), row(2, "METRO 456"), row(3, "SAQ 789")];
|
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(
|
renderToStaticMarkup(
|
||||||
<TransactionTable
|
<TransactionTable
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
|
@ -60,6 +66,7 @@ const markup = (rows: TransactionRow[], onboardingTarget?: boolean, splitAccess:
|
||||||
onSaveSplit={vi.fn(async () => undefined)}
|
onSaveSplit={vi.fn(async () => undefined)}
|
||||||
onDeleteSplit={vi.fn(async () => undefined)}
|
onDeleteSplit={vi.fn(async () => undefined)}
|
||||||
splitAccess={splitAccess}
|
splitAccess={splitAccess}
|
||||||
|
linkedTransfersByTxId={linkedTransfersByTxId}
|
||||||
onboardingTarget={onboardingTarget}
|
onboardingTarget={onboardingTarget}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
@ -130,3 +137,22 @@ describe("TransactionTable — the Répartition button (#369)", () => {
|
||||||
expect(buttons[0]).toContain("text-orange-500");
|
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
|
// 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
|
// 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
|
// (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
|
// 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
|
// 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 { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
import { isValidElement, type ReactElement, type ReactNode } from "react";
|
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);
|
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"');
|
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'");
|
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`);
|
expect(withoutFolders(`/home/marie claire/x.db${stop} I/O`)).toBe(`x.db${stop} I-O`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue