// Automated per-page control census (QA_PLAN Appendix E / IMP-CODE-07). // Replaces the never-filled manual census tables: every interactive control on // each page is enumerated (role + accessible name) and snapshotted, so a new, // removed, or renamed control shows up as a reviewable diff each cycle instead // of relying on a hand-maintained ledger. Numbers are normalized to '#' so // dates/amounts/counts don't churn the snapshots; the list is deduped + sorted // so ordering and per-row repetition don't either. const { test, expect } = require('@playwright/test'); const { STORAGE_STATE } = require('./constants'); test.use({ storageState: STORAGE_STATE }); const PAGES = [ ['tracker', '/'], ['calendar', '/calendar'], ['summary', '/summary'], ['bills', '/bills'], ['subscriptions', '/subscriptions'], ['categories', '/categories'], ['health', '/health'], ['analytics', '/analytics'], ['spending', '/spending'], ['banking', '/bank-transactions'], ['snowball', '/snowball'], ['payoff', '/payoff'], ['data', '/data'], ['settings', '/settings'], ['profile', '/profile'], ]; const CONTROL_SELECTOR = [ 'button', 'a[href]', 'input', 'select', 'textarea', '[role="button"]', '[role="link"]', '[role="tab"]', '[role="switch"]', '[role="checkbox"]', '[role="menuitem"]', '[role="combobox"]', ].join(', '); for (const [name, path] of PAGES) { test(`control census: ${name}`, async ({ page }) => { await page.goto(path); await page.waitForLoadState('domcontentloaded'); // Self-stabilizing settle: sample the control count until it is non-trivial // and unchanged across two consecutive samples (fixed timeouts race the // data queries; networkidle is flaky with background refetches). let prev = -1; for (let i = 0; i < 20; i++) { const count = await page.locator(CONTROL_SELECTOR).count(); if (count > 3 && count === prev) break; prev = count; await page.waitForTimeout(500); } const controls = await page.$$eval(CONTROL_SELECTOR, (els) => els .filter((el) => { const style = window.getComputedStyle(el); return style.display !== 'none' && style.visibility !== 'hidden'; }) .map((el) => { const role = el.getAttribute('role') || (el.tagName === 'A' ? 'link' : el.tagName === 'INPUT' ? `input:${el.getAttribute('type') || 'text'}` : el.tagName.toLowerCase()); const name = el.getAttribute('aria-label') || el.getAttribute('placeholder') || (el.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 60) || el.getAttribute('title') || '(unnamed)'; return `${role} | ${name}`; }), ); const normalized = [ ...new Set( controls.map((c) => c .replace(/\d[\d,.]*/g, '#') // dates, amounts, counts .replace(/#\s*(am|pm)/gi, '# $1'), ), ), ] .sort() .join('\n'); expect(normalized).toMatchSnapshot(`census-${name}.txt`); }); }