|
| 1 | +--- |
| 2 | +name: verifier-gui |
| 3 | +description: This project's GUI verifier for the /verify skill — drives the DTS Expense Tracker in a real browser (Playwright) to visually confirm a UI change, beyond what vitest/RTL asserts. |
| 4 | +--- |
| 5 | + |
| 6 | +The generic `/verify` skill looks for a `verifier-*` skill matching the |
| 7 | +surface before cold-starting; this is that skill for this repo's surface |
| 8 | +(browser/GUI — it's a PWA with no CLI or server API). It's a **manual |
| 9 | +verification recipe**, not a test suite — it isn't wired into `npm test` or |
| 10 | +CI. Reach for it when a change touches rendering/styling that component tests |
| 11 | +(jsdom) can assert structurally (class names, text) but can't show you |
| 12 | +visually — e.g. color precedence between two highlight states, layout, or the |
| 13 | +real download flow for CSV/`.xlsx` exports. |
| 14 | + |
| 15 | +## One-time setup |
| 16 | + |
| 17 | +```bash |
| 18 | +npx playwright install chromium # ~100MB, cached per-machine, only needed once |
| 19 | +``` |
| 20 | + |
| 21 | +`playwright` is a devDependency (added for this purpose only — not imported |
| 22 | +by any app or test code), so `npm ci` already pulls the package; only the |
| 23 | +browser binary needs the explicit install above. |
| 24 | + |
| 25 | +## Drive it |
| 26 | + |
| 27 | +```bash |
| 28 | +npm run dev -- --port 5183 # note the printed base path, e.g. /dts-expense-tracker/ |
| 29 | +``` |
| 30 | + |
| 31 | +Write a throwaway script at the **repo root** (not `/tmp` — it needs |
| 32 | +`node_modules` resolution) and run it with plain `node`, e.g. |
| 33 | +`node verify-scratch.mjs`. Delete it before committing anything — it's |
| 34 | +scratch, not a fixture. |
| 35 | + |
| 36 | +```js |
| 37 | +import { chromium } from 'playwright'; |
| 38 | + |
| 39 | +const BASE = 'http://localhost:5183/dts-expense-tracker/'; // match the dev server's base path |
| 40 | + |
| 41 | +const browser = await chromium.launch(); |
| 42 | +const page = await browser.newPage({ viewport: { width: 420, height: 900 } }); // iPhone-ish width; this is a mobile PWA |
| 43 | +await page.goto(BASE); |
| 44 | +await page.evaluate(() => localStorage.clear()); // fresh trip, no leftover IndexedDB/localForage state |
| 45 | +await page.reload(); |
| 46 | +``` |
| 47 | + |
| 48 | +Key selectors (all accessible-name based; the tab bar's icon glyphs are |
| 49 | +`aria-hidden`, so the button's name is just the label): |
| 50 | + |
| 51 | +- Tabs: `page.getByRole('button', { name: 'Entry', exact: true })` — same |
| 52 | + pattern for `List`, `M&IE`, `Totals`, `Export`. |
| 53 | +- Entry form: `getByLabel('GBP (receipt)')`, `getByLabel('USD (DTS)')`, |
| 54 | + category via `page.locator('select')` + `.selectOption('TRANSPORT')`, |
| 55 | + payment via `page.locator('.toggle__opt', { hasText: 'Personal' }).click()`. |
| 56 | + Save with `getByRole('button', { name: 'Save & view list' })`. |
| 57 | +- Totals inputs: `getByLabel('DTS USD total for LODGING')`, |
| 58 | + `getByLabel('DTS USD reimbursement for GTCC')` (also `Personal`). |
| 59 | +- Recon row state: `page.$$eval('.recon__row', els => els.map(el => |
| 60 | + ({ text: el.textContent, className: el.className })))` — cheapest way to |
| 61 | + confirm which highlight class (`recon__row--mismatch`, |
| 62 | + `recon__row--incomplete`, etc.) actually won, and read off the rendered |
| 63 | + text in one shot. |
| 64 | +- Screenshot: `page.screenshot({ path: '/tmp/whatever.png', fullPage: true })`, |
| 65 | + then `Read` the PNG to eyeball it. |
| 66 | + |
| 67 | +## Exports |
| 68 | + |
| 69 | +`ExportView`'s download buttons use `<a download>` + an object URL — capture |
| 70 | +them with Playwright's download event, not by inspecting the DOM: |
| 71 | + |
| 72 | +```js |
| 73 | +const [download] = await Promise.all([ |
| 74 | + page.waitForEvent('download'), |
| 75 | + page.getByRole('button', { name: 'Download CSV' }).click(), // or 'Download .xlsx' |
| 76 | +]); |
| 77 | +const buf = fs.readFileSync(await download.path()); |
| 78 | +``` |
| 79 | + |
| 80 | +CSV is readable directly (`buf.toString('utf8')`); for `.xlsx`, load it with |
| 81 | +`exceljs` (already a project dependency) the same way `xlsx.test.ts`'s |
| 82 | +`readBack` helper does. |
| 83 | + |
| 84 | +## Cleanup |
| 85 | + |
| 86 | +- Kill the dev server (`pkill -f "vite --port 5183"` or just Ctrl-C the |
| 87 | + foreground job). |
| 88 | +- Delete the scratch script — it's not a fixture, don't commit it. |
| 89 | +- Fill colors are intentionally not asserted in vitest (see `xlsx.test.ts` |
| 90 | + comment) — this recipe is the only way those actually get eyeballed, so |
| 91 | + don't skip it for changes that touch `pendingFill`/`mismatchFill`/CSS. |
0 commit comments