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:
parent
50fe0ab1ac
commit
5a54d37de5
1 changed files with 96 additions and 0 deletions
96
src/__integration__/transactions-transfer-icon.test.ts
Normal file
96
src/__integration__/transactions-transfer-icon.test.ts
Normal 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\}/
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue