T0.2 - baseline captured; all six rendering defects confirmed present

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>
This commit is contained in:
2026-08-14 18:06:36 -05:00
parent c2e35b9261
commit fe8a27e022
22 changed files with 774 additions and 2 deletions

View File

@@ -296,6 +296,48 @@ class Page:
self.eval(f"document.dispatchEvent(new KeyboardEvent('keydown',{{key:{name!r}}}))")
time.sleep(settle)
def viewport(self, width, height=900, mobile=False, scale=1):
"""Emulate a viewport width. The two that matter are 390 (the gloved-hands
field tablet, where the worst rendering was found) and 1440 (the desk).
`mobile` also sets the mobile flag and a meta-viewport-aware layout, which
is what a tablet actually reports; without it a 390px-wide desktop window
is not the same test."""
self.ws.call("Emulation.setDeviceMetricsOverride", {
"width": int(width), "height": int(height),
"deviceScaleFactor": scale, "mobile": bool(mobile),
})
time.sleep(0.35) # let media queries and reflow settle
self.ws.drain(0.2)
return self
def screenshot(self, path, full_page=True):
"""Write a PNG to `path`, creating parent directories. Returns the path.
full_page captures the whole document rather than the visible box: the
creator form is roughly 4,700px tall and a viewport-sized shot of it would
hide the thing being compared. Chrome refuses beyond 16,384px, so an
over-tall page is clamped rather than failing the capture."""
params = {"format": "png"}
if full_page:
try:
m = self.ws.call("Page.getLayoutMetrics")
size = m.get("cssContentSize") or m.get("contentSize") or {}
w, h = size.get("width"), size.get("height")
if w and h:
params["clip"] = {"x": 0, "y": 0, "width": w,
"height": min(h, 16384), "scale": 1}
params["captureBeyondViewport"] = True
except (RuntimeError, TimeoutError):
pass # fall back to a viewport-sized shot
shot = self.ws.call("Page.captureScreenshot", params, timeout=45)
parent = os.path.dirname(os.path.abspath(path))
if parent:
os.makedirs(parent, exist_ok=True)
with open(path, "wb") as fh:
fh.write(base64.b64decode(shot["data"]))
return path
def js_errors(self):
"""Everything that means 'this page did not boot cleanly': uncaught
exceptions, console.error calls, and browser-logged errors.