Runs the app from a clean database, captures the before images, and records
which of F1-F6 actually still reproduce. All six do.
Rather than eyeball screenshots, each defect is measured in a browser by
tests/f_items.py, which reports REPRODUCES / FIXED / INCONCLUSIVE and never a
silent pass. That makes it both the wave 0 record and the wave 1-3 regression
check: an item is done when its probe flips to FIXED.
F1 hero says "Job A", app bar still says "Select a project", no reload
F2 "Sign out" spans x382-432 in a 390px viewport - cut in half, 3 rows
F3 chrome paints over the logo by 106x32px; .header-left collapses to 0
F4 comments drawer overlaps the header by 380x91px in the standalone creator
F5 5 of 5 ENABLED wizard inputs compute #f4f4f4 on #e0e0e0
F6 11 cards in one 5,017px scroll, 0 tabs (review said ~4,700px; it grew)
Three probes needed care to avoid reporting a false pass, and the traps are
worth knowing before anyone verifies a fix:
F1 disappears if localStorage is primed first, because then both sources of
truth agree. The probe clears it and drives the real picker.
F3 needs a long project name that is long IN THE DATABASE - any page reached
with ?project= re-pulls it and overwrites a locally-faked one. It also cannot
be measured by comparing .header-left to the chrome: under the long name
.header-left (flex:1, min-width:0) collapses to clientWidth 0, so that
comparison reports a tidy zero gap while the chrome paints across the logo.
It measures against .logo, which is flex-shrink:0. My first two attempts at
this probe both reported FIXED for those reasons; the screenshot did not.
F5 must ignore genuinely disabled inputs or a fix looks done while real
fields stay grey.
14 screenshots, not the 12 the plan asks for, because there are 7 pages
(file-map D1). Capture also measures horizontal overflow, which is how BL-001
was found.
Tooling: cdp.py gains viewport() and screenshot() - it could do neither, and
T0.2 requires 390px and 1440px images. 390px sets the mobile flag rather than
just narrowing the window, since every page declares width=device-width and
Chrome otherwise lays out at 980px and no media query under test fires. Both
new scripts reuse browser_check.py's seed() and start_server() instead of
growing a second fixture. Existing browser_check still passes 71/71.
No application code changed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
server/smoketest.py proves the API works; nothing proved the PAGES work. That gap
is why users.js, wp-sidenav.js and the extracted console.css shipped unexecuted and
had to be written up as a known issue instead of verified. This closes the gap with
a tool rather than a one-off, so the next front-end change is cheap to check.
tests/cdp.py a minimal DevTools Protocol client — hand-rolled stdlib
WebSocket (handshake, masked frames), browser discovery for
Edge/Chrome across platforms, and process teardown.
tests/browser_check.py the fixture and 71 assertions.
Stdlib only, matching smoketest.py's rule: these have to run on a plain Python
install on whatever machine is to hand. No pip, no Selenium, no node.
Self-contained — it builds a throwaway database, seeds a fixture, starts its own
uvicorn on a free port, drives the browser, and tears everything down. The real
database is never touched. Sessions come from minting a token with the app's own
auth.create_token() rather than scripting the login form.
What it asserts, beyond "no JavaScript errors on boot" (the thing that actually
went unverified): the three role-dependent renderings of the directory, one-line
rows and no sideways scroll, the roles each caller may grant, the project-access
dialog opening and closing, the drawer's open/Escape/scrim/focus/aria behaviour and
its role gating, ?project= carried only onto project-scoped links, and — the reason
this matters most — that admin.html still has its tokens, cards, headings and dense
sticky tables after console.css was lifted out of its inline <style>.
Three things the build had to get right, each learned the hard way:
- Teardown kills the browser's whole process tree AND sweeps anything still
holding the unique temp profile, matched on that path so a browser window the
user has open is never touched. proc.kill() alone left 98 strays.
- Launching retries with a fresh profile and port: a browser can hand off to
another instance and exit rc=0 without ever binding the debugging port.
- Cleanup waits for the server to exit and disposes the harness's own SQLAlchemy
engine before removing the temp directory, or the open SQLite file blocks the
delete and ignore_errors hides it.
The fixture includes an account on a project the super user cannot see, without
which the admin and the super user would see the same number of rows and the
scoping assertion would prove nothing.
Documented in DEPLOYMENT.md next to the smoke test. 71/71 across repeated runs,
leaving no stray processes or temp directories.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>