diff --git a/.gitignore b/.gitignore index 5bfa543..c1cba00 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,14 @@ simplefin-bank-sync-issue.md project-wide-data-input-and-sync-issue.md docs/SIMPLEFIN_CONSUMER_GUARDRAILS.md +# Playwright E2E run artifacts (visual baselines under e2e/**/*-snapshots/ ARE committed) +test-results/ +playwright-report/ +blob-report/ +.playwright/ +e2e/.auth/ +db/e2e.db* + # MkDocs docs site (auto-generated, not part of app source) mkdocs/ diff --git a/HISTORY.md b/HISTORY.md index 3b21440..5e8310d 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,13 @@ # Bill Tracker β€” Changelog ## v0.41.0 +### πŸ› QA Fixes + +- **[Data] Seed Demo Data amounts were 100Γ— too small** β€” `scripts/seedDemoData.js` inserted demo dollar amounts straight into `bills.expected_amount` / `current_balance` / `minimum_payment`, which became integer-cents columns in the v1.03 migration, so a seeded "$85" bill showed as $0.85 (a whole demo month totalled ~$32 instead of ~$3,200). Now wraps demo values in `toCents()` before insert. Regression guard added in `e2e/api.probe.spec.js`. (was QA-B9-01) +- **[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) + ### ✨ Spending - **Category groups** β€” Organize spending categories into named groups (e.g. "Bills", "Everyday", "Subscriptions"). New `category_groups` table with CRUD endpoints. Categories can be assigned to a group via the Spending page or API. Groups appear as collapsible headers in the category breakdown. diff --git a/client/lib/utils.js b/client/lib/utils.js index 575028e..afbf7c3 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -6,7 +6,9 @@ export function cn(...inputs) { } export function fmt(amount) { - return '$' + Number(amount || 0).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); + const n = Number(amount) || 0; + const sign = n < 0 ? '-' : ''; + return sign + '$' + Math.abs(n).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); } export function fmtDate(dateStr) { diff --git a/client/lib/utils.test.js b/client/lib/utils.test.js index 1c65422..ab8c31f 100644 --- a/client/lib/utils.test.js +++ b/client/lib/utils.test.js @@ -12,6 +12,12 @@ describe('fmt (money display)', () => { expect(fmt(null)).toBe('$0.00'); expect(fmt(undefined)).toBe('$0.00'); }); + + it('places the minus sign before the currency symbol (QA-B6-01)', () => { + expect(fmt(-50)).toBe('-$50.00'); + expect(fmt(-1234.5)).toBe('-$1,234.50'); + expect(fmt(-0.99)).toBe('-$0.99'); + }); }); describe('local dates', () => { diff --git a/client/pages/AnalyticsPage.jsx b/client/pages/AnalyticsPage.jsx index 1f77fb8..7340d35 100644 --- a/client/pages/AnalyticsPage.jsx +++ b/client/pages/AnalyticsPage.jsx @@ -74,10 +74,10 @@ function ChartCard({ title, subtitle, children, summary }) { ); } -function SvgFrame({ children, height = 260 }) { +function SvgFrame({ children, height = 260, label = 'Chart' }) { return (
- + {children}
@@ -102,7 +102,7 @@ function LineChart({ rows, area = false }) { const areaPoints = `${pad.left},${pad.top + chartH} ${line} ${pad.left + chartW},${pad.top + chartH}`; return ( - + {[0, 0.25, 0.5, 0.75, 1].map(tick => { const y = pad.top + chartH - tick * chartH; return ( @@ -142,7 +142,7 @@ function GroupedBarChart({ rows }) { const barW = Math.max(5, Math.min(17, groupW * 0.28)); return ( - + {[0, 0.5, 1].map(tick => { const y = pad.top + chartH - tick * chartH; return ( @@ -191,7 +191,7 @@ function DonutChart({ rows }) { return (
- + {rows.map((row, index) => { const value = Number(row.total || 0); @@ -378,7 +378,7 @@ function ForecastChart({ historical, forecast }) { const showLabel = (index) => allRows.length <= 14 || index % Math.ceil(allRows.length / 14) === 0; return ( - + {/* Grid */} {[0, 0.25, 0.5, 0.75, 1].map(tick => { const y = pad.top + chartH - tick * chartH; diff --git a/client/pages/BillsPage.jsx b/client/pages/BillsPage.jsx index b3a34fa..51f3fcb 100644 --- a/client/pages/BillsPage.jsx +++ b/client/pages/BillsPage.jsx @@ -921,7 +921,7 @@ export default function BillsPage() {
setFilterValue('category', value)}> - + @@ -981,7 +981,7 @@ export default function BillsPage() { setFilterValue('category', value)}> - + @@ -723,7 +723,7 @@ export default function TrackerPage() {