diff --git a/src/__integration__/transactions-transfer-icon.test.ts b/src/__integration__/transactions-transfer-icon.test.ts new file mode 100644 index 0000000..5e5803b --- /dev/null +++ b/src/__integration__/transactions-transfer-icon.test.ts @@ -0,0 +1,96 @@ +/** + * Non-regression check for the inlined transfer icon in TransactionTable + * (Issue #142 → #144 follow-up). + * + * The spec promises that — without any linked transfers — the transactions + * table renders exactly as it did before #142 inlined the `` icon. + * The icon is gated by a single conditional in the JSX: + * + * {linkedTransfersByTxId?.has(row.id) && (...)} + * + * If `linkedTransfersByTxId` is undefined OR the map has no entry for `row.id`, + * the icon block is short-circuited and the row layout is unchanged. + * + * Why this approach: this project does not bundle `@testing-library/react` + * (see `package.json`), and adding it just for one non-regression check is + * out of scope here. Existing component tests (`CategoryCombobox.test.ts`, + * `ViewModeToggle.test.ts`, `TrendsChartTypeToggle.test.ts`) likewise extract + * pure helpers and assert on them rather than mounting JSX. So we go one + * level lower: assert the source-level shape of `TransactionTable.tsx`. + * + * The assertions are structural on the source file: + * 1. The conditional block exists and is gated by `linkedTransfersByTxId?.has`. + * 2. The block consumes `Link2` from `lucide-react`. + * 3. The prop is OPTIONAL on the component's interface — passing nothing + * must remain a valid call (zero-impact path). + * 4. The tooltip text comes from the i18n key family `transactions.transferIcon.*` + * (so a future rename catches our attention here). + * 5. The icon uses `aria-label` for accessibility (Issue #142 acceptance criterion). + * 6. The condition uses optional-chaining (so passing `undefined` short-circuits + * cleanly without throwing). + * + * If the icon is ever pulled out into its own component, the tests should be + * rewritten to import and exercise that component directly instead. Until + * then, this is a tight static contract that catches accidental regressions. + */ + +import { describe, it, expect } from "vitest"; +import { readFileSync } from "fs"; +import { resolve } from "path"; + +const TABLE_SRC = readFileSync( + resolve( + import.meta.dirname, + "..", + "components", + "transactions", + "TransactionTable.tsx" + ), + "utf-8" +); + +describe("non-regression: TransactionTable transfer icon (#142)", () => { + it("guards the icon block behind `linkedTransfersByTxId?.has(row.id)`", () => { + expect(TABLE_SRC).toMatch(/linkedTransfersByTxId\?\.has\(row\.id\)/); + }); + + it("uses optional chaining so the icon is opt-in (undefined short-circuits)", () => { + // Optional chaining is the safe-render guarantee: if the parent never + // passes the prop, `?.has` returns undefined → the && short-circuits to + // false, the JSX block is skipped, and the row layout is unchanged. + expect(TABLE_SRC).toMatch(/linkedTransfersByTxId\?\./); + }); + + it("imports `Link2` from lucide-react for the icon glyph", () => { + expect(TABLE_SRC).toMatch(/from\s+["']lucide-react["']/); + expect(TABLE_SRC).toMatch(/\bLink2\b/); + }); + + it("declares `linkedTransfersByTxId` as an OPTIONAL prop", () => { + // The "?" after the name on the interface is the contract that omitting + // the prop is allowed. Without it the entire transactions page would + // need to thread the lookup through, breaking pre-#142 callers. + expect(TABLE_SRC).toMatch(/linkedTransfersByTxId\?:/); + }); + + it("uses `transactions.transferIcon.*` i18n keys for the tooltip and aria-label", () => { + // Both the tooltip body and the aria label go through i18n — neither + // is a hardcoded English/French string. + expect(TABLE_SRC).toMatch(/transactions\.transferIcon\.tooltip/); + expect(TABLE_SRC).toMatch(/transactions\.transferIcon\.ariaLabel/); + }); + + it("attaches an `aria-label` for screen readers (a11y)", () => { + expect(TABLE_SRC).toMatch(/aria-label=/); + }); + + it("keeps the description column structure shared with non-linked rows", () => { + // The icon lives inside the description cell, in a flex container + // alongside the original `` that + // existed pre-#142. If someone moved the description span into a + // wrapper that the icon required, this assertion would fail. + expect(TABLE_SRC).toMatch( + /