test(transactions): add non-regression test for inline transfer icon

Source-level structural test on `TransactionTable.tsx` to lock down the
inlined transfer icon contract introduced in #142. Without RTL or jsdom
in the dev-deps, the test reads the component source and asserts:

  - the icon is gated by `linkedTransfersByTxId?.has(row.id)`,
  - optional-chaining short-circuits cleanly when the prop is omitted
    (zero-impact on pre-#142 callers),
  - the prop is declared OPTIONAL on the component interface,
  - the `Link2` glyph comes from lucide-react,
  - tooltip + aria-label go through `transactions.transferIcon.*` i18n
    keys,
  - the row's description cell layout (truncate span + title) stays
    shared between linked and non-linked rows.

Catches the specific regression vectors: someone removing the gate,
renaming the prop, or breaking the optional-chaining pattern that
guarantees the page renders identically when no transfers are linked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
le king fu 2026-04-25 16:53:59 -04:00
parent 50fe0ab1ac
commit 5a54d37de5

View file

@ -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 `<Link2>` 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 `<span class="truncate" title=...>` 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(
/<span\s+className="truncate"\s+title=\{row\.description\}/
);
});
});