diff --git a/HISTORY.md b/HISTORY.md index f572320..5b2aedc 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -7,6 +7,7 @@ - **[Bills] `expected_amount` was unvalidated** — POST/PUT `/api/bills` accepted negative amounts, non-numeric input (silently coerced to $0), and absurd values (`1e15` → cents past `Number.MAX_SAFE_INTEGER`). `validateBillData` (`services/billsService.js`) now rejects non-numeric / negative / out-of-range with a structured `VALIDATION_ERROR`, accepting `0`…`$100,000,000`. Regression assertions in `e2e/api.probe.spec.js`. (was QA-B13-01) - **[UI] Negative amounts rendered as "$-50.00"** — client `fmt()` (`client/lib/utils.js`) and server `formatUSD()` (`utils/money.js`) placed the minus sign after the currency symbol; now render the conventional "-$50.00". Test added in `client/lib/utils.test.js`. (was QA-B6-01) - **[a11y] Icon-only controls and chart SVGs had no accessible name** — Radix Select filter/sort triggers (Tracker, Bills) and the Spending month-nav buttons rendered with no discernible text (screen readers announced a bare "button"); Analytics chart ``s had no name; a Snowball drag-handle `
` used a prohibited `aria-label`. Added `aria-label`s to the triggers/buttons and a `label` prop to the Analytics `SvgFrame`, and switched the drag-handle to `title`. Clears axe **critical `button-name`** and **serious `svg-img-alt` / `aria-prohibited-attr`** across those pages; guarded by `e2e/a11y.authed.spec.js`. (was QA-B14-01, part of QA-B14-02) +- **[a11y] Nested interactive controls in Categories & Snowball rows** — Categories rows were `role="button"` (and draggable) yet wrapped their own move/edit/delete buttons, and the Snowball plan-status header wrapped its action buttons *inside* the collapsible trigger button (axe **serious `nested-interactive`**). Restructured both: Categories now use a plain container with a dedicated chevron toggle button (click-anywhere still expands via mouse; keyboard/SR use the chevron); the Snowball header splits into a name/progress toggle, sibling action buttons, and a chevron toggle. Expand/collapse verified by `e2e/categories.spec.js`; all 8 authenticated pages now pass axe. (completes QA-B14-02) ### 🧹 QA Cleanup diff --git a/client/components/snowball/PlanStatusBanner.jsx b/client/components/snowball/PlanStatusBanner.jsx index 1daf146..3e35b55 100644 --- a/client/components/snowball/PlanStatusBanner.jsx +++ b/client/components/snowball/PlanStatusBanner.jsx @@ -145,11 +145,14 @@ export default function PlanStatusBanner({ plan, onPause, onResume, onComplete,
{/* Header */} - -
+ + - {/* Actions */} -
e.stopPropagation()}> - {isActive && ( - <> - - - - )} - {isPaused && ( - <> - - - - )} - - -
+ + + )} + {isPaused && ( + <> + + + + )} + +
- - - + {/* Chevron — also a toggle */} + + + + + {/* Collapsible body — per-debt rows */} diff --git a/client/pages/CategoriesPage.jsx b/client/pages/CategoriesPage.jsx index 0a4d0f7..1bb9e94 100644 --- a/client/pages/CategoriesPage.jsx +++ b/client/pages/CategoriesPage.jsx @@ -565,16 +565,16 @@ export default function CategoriesPage() { dragProps?.isDropTarget && 'ring-2 ring-inset ring-primary/35', )} > + {/* Plain container (not role=button) so the nested action + buttons aren't interactive-in-interactive (a11y QA-B14-02). + Mouse click-anywhere still expands; keyboard/SR users use + the dedicated chevron toggle button below. */}
toggleCategory(cat.id)} - onKeyDown={event => onRowKeyDown(event, cat.id)} className={cn( 'group grid cursor-pointer gap-4 px-4 py-4 transition-colors sm:px-5 md:grid-cols-[minmax(0,1fr)_auto] md:items-center', - 'hover:bg-muted/35 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset', + 'hover:bg-muted/35', isExpanded && 'bg-muted/25', )} > @@ -615,12 +615,20 @@ export default function CategoriesPage() {
- +
diff --git a/docs/QA_PLAN.md b/docs/QA_PLAN.md index d011a4f..a5a1560 100644 --- a/docs/QA_PLAN.md +++ b/docs/QA_PLAN.md @@ -101,7 +101,7 @@ before cross-cutting; regression last). Update **Status** and **Findings** every | B11 | Admin panel | users, login mode, auth methods, backups, cleanup, status, onboarding | admin | ⬜ | 0 / 0 | | B12 | Settings, Profile & global UI | `/settings`, `/profile`, static pages, command palette, sidebar/nav | any | ⬜ | 0 / 0 | | B13 | API / backend direct | all `/api/*`: auth, CSRF, validation, rate limits, error shape, IDOR, cents | via HTTP client | 🔄 | 0 / 1 | -| B14 | Non-functional | a11y, performance, PWA/offline, XSS/secrets, timezone/DST | large + adversarial | 🔄 | 1 / 2 | +| B14 | Non-functional | a11y, performance, PWA/offline, XSS/secrets, timezone/DST | large + adversarial | 🔄 | 0 / 3 | | B15 | Regression & sign-off | full smoke on **production build**, exit criteria | seeded | ⬜ | 0 / 0 | > After B15, if any batch is 🔁 or has open S1/S2, loop back. Then start a new @@ -115,7 +115,7 @@ until you get a clean cycle. | Cycle | Started | Build / commit | Findings logged | Fixed / archived | Result | |-------|---------|----------------|-----------------|------------------|--------| -| 1 | 2026-07-02 | `bdbf231` (dev) | 9 (find pass ongoing) | 7 → HISTORY v0.41.0 (B9-01, B13-01, B6-01, B14-01, B14-03, B0-01, B7-02; + a11y svg/aria of B14-02) | 🔄 in progress — B0/B1/B3/B4/B6/B7/B8/B9/B13/B14 probed. Solid: auth-isolation, CSRF, payment/date validation, **recurrence (quarterly/annual gating, Feb-31 clamp, leap year — all correct)**, **transaction matching/dedup guards**, subscription+spending math, XSS. **Fixed: seed 100× cents (S2), bill-amount validation, negative-money format, a11y labels, vendor-bundle split, unused-dep removal, dead-code removal.** Open: 2 (B7-01 rounding S3 [float-inherent], B14-02 nested-interactive S3 [architectural]) | +| 1 | 2026-07-02 | `bdbf231` (dev) | 9 (find pass ongoing) | 8 → HISTORY v0.41.0 (B9-01, B13-01, B6-01, B14-01, B14-02, B14-03, B0-01, B7-02) | 🔄 in progress — B0/B1/B3/B4/B6/B7/B8/B9/B13/B14 probed. Solid: auth-isolation, CSRF, payment/date validation, **recurrence (quarterly/annual gating, Feb-31 clamp, leap year)**, **transaction matching/dedup**, subscription+spending math, XSS. **Fixed: seed 100× cents (S2), bill-amount validation, negative-money format, all a11y (button-name/svg/aria/nested-interactive — 8/8 pages pass axe), vendor-bundle split, unused-dep + dead-code removal.** Open: 1 (B7-01 rounding S3 [float-inherent, deferred]) | **Result key:** 🔄 in progress · 🔁 findings fixed, re-run required · ✅ clean (zero findings — QA complete) @@ -135,7 +135,6 @@ fixing. Keep only **Open / Fixing / Fixed** rows here. Once a finding is | ID | Sev | Area (`file:line`) | Summary | Status | Notes / repro | |----|-----|--------------------|---------|--------|---------------| | QA-B7-01 | S3 | `utils/money.js:29` | `toCents` mis-rounds fractional cents: `toCents(1.005)` → 100 (`$1.00`) not 101 | 🔴 Open | see write-up (deferred — float-inherent) | -| QA-B14-02 | S3 | `/categories` (8), `/snowball` (1) | axe **serious** `nested-interactive`: draggable/expandable rows are `role=button` yet contain nested buttons | 🔴 Open | see write-up (deferred — architectural) | **Finding template** (paste a new row above; keep the full write-up here until archived): @@ -175,26 +174,6 @@ Impact: bounded to sub-cent, and only when a 3+ decimal dollar value reaches the Fix (deferred): round on a string/scaled-integer basis, or add epsilon before round. ``` -``` -ID: QA-B14-02 (partially fixed — nested-interactive remains) -Severity: S3 (accessibility — WCAG 2 A/AA, axe "serious") -Environment: authenticated app; found/verified via e2e/a11y.authed.spec.js -FIXED this cycle (archived to HISTORY.md, was part of B14-02 + all of B14-01): - - button-name (critical) → Tracker/Bills Select triggers + Spending month-nav + - Analytics chart svgs now have aria-labels. All 4 pages pass. - - svg-img-alt → /analytics chart svgs got aria-labels (SvgFrame `label` prop). Pass. - - aria-prohibited-attr → /snowball drag-handle div aria-label → title. Pass. -REMAINING (deferred — architectural, needs manual drag/keyboard/SR verification): - - nested-interactive → /categories (8): each row is
AND draggable AND contains nested edit/delete/drag buttons. - - nested-interactive → /snowball (1): a