QA: test responsive mobile layout on real devices #13
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Per the decision in #8,
mainkeeps the responsive mobile layout from #5 (closes #2) rather than a "not available on mobile" message. This issue tracks manual QA of that layout on actual mobile hardware.What to verify
max-width: 768pxmedia block insrc/styles.cssrenders correctlyRef #2, #5, #8
Assigning to @sneak: this QA pass needs real physical devices (phones/tablets), which automation here cannot provide. The responsive layout itself landed via PR #5 and the CI gate via PR #12; what remains is hands-on verification on actual hardware. If you find rendering problems, please note device/browser/viewport details here and reassign to clawbot for fixes.
Reopening for automation — my earlier "needs physical devices" call was wrong
I parked this with @sneak on the grounds that "this QA pass needs real physical devices (phones/tablets), which automation here cannot provide." That was incorrect, and it parked a whole issue on the wrong premise. Correction relayed from sneak via the fleet manager: this host has full build tooling and Docker, so a scriptable headless browser with device emulation can verify essentially all of it.
Taking it back off sneak's queue and rewriting the definition of done accordingly.
The mistake is worth naming precisely, because it is a reasoning error rather than a missing fact: I conflated "testing a mobile layout" with "testing on mobile hardware." The overwhelming majority of what this issue actually cares about — does the layout reflow, does anything overflow horizontally, are tap targets big enough, is content clipped — is a function of viewport width and CSS, not of the physical device. Emulation covers it.
Revised definition of done
The harness
:latest, no version tag. Version+date comment above the reference.maketarget and ascript/entrypoint, consistent with everything else in this repo. Never a raw browser or node invocation.dist/(or the built image) rather than a dev server, so what is tested is what ships.The viewports
src/styles.cssand the Tailwind classes insrc/main.jsand enumerate them. Include one width just below and one just above each breakpoint — off-by-one at a media query boundary is the classic failure and a generic 375px test sails straight past it.The assertions — these must be able to fail
document.documentElement.scrollWidth <= innerWidthat every tested width.scrollWidthmaterially exceeds itsclientWidthin a way that hides text.Prove the harness can fail — non-negotiable
Document the limits, so this does not get re-parked later
Implementation requirements
make checkpath unless it is fast and hermetic; if it is slow, give it its own target and wire it into CI separately.make testmust stay under 20 seconds.vitest). Coordinate — this harness is browser-level and complementary, not a replacement, but they should not end up as two unrelated test stacks. Say in the PR how they relate.TODO.mdupdated in the same commit; commit title ends with(closes #13).Note on provenance
This reassignment reached me via the fleet manager relaying sneak, not from sneak directly in the tracker. Flagging that so the record is accurate. It is a low-risk instruction — working a tracker issue is my normal job, and the technical premise is verifiably correct — so I am proceeding rather than round-tripping for confirmation. @sneak, if you did not intend this, say so and I will park it again.
If the harness turns out to be reusable, that is worth knowing beyond this repo — another repo on the roster reportedly wrote off much of its 1.0.0 milestone as unverifiable for exactly the reason I got wrong here.
Implementation plan
Driver:
puppeteer-core(devDependency), notplaywright. Reason, and theresolution of the bundled-browser problem the DoD flags:
puppeteer-coreis theone variant of either library that never downloads or bundles a browser — it
only speaks CDP to a browser you point it at. So the browser stays a
digest-pinned container image and the npm side is pinned by
yarn.lockintegrity hashes.
playwrightexpects its own version-matched browser download,which would either fight the digest pin or force us onto the ~2 GB official
Playwright image.
Browser image:
chromedp/headless-shell151.0.7922.109, referenced by@sha256:digest with a version+date comment.Topology:
script/frontend-viewport-testbuildsdist/via the existingbuild entrypoint, then brings up two
--rmcontainers on a private--internaldocker network:nginxdigest (same oneDockerfileuses) servingdist/with the repo's ownnginx.conf— so the harness runs against thebuilt artifact and the shipping server config, not a dev server;
127.0.0.1port.The network is
--internal, so the browser physically cannot reach the realinternet; on top of that every off-origin request is intercepted and served a
deterministic canned response (a fixed delay table, plus a couple of forced
failures) so host rows render a realistic mix of numeric latencies, TIMEOUT and
offline states rather than 24 identical
---placeholders. Containers andnetwork are torn down in a trap.
Widths are derived, not hardcoded. The harness parses
src/styles.cssfor@mediamin-width/max-widthvalues and scanssrc/main.js+index.htmlfor Tailwind responsive prefixes, mapping them to Tailwind v4 default
breakpoints. Every breakpoint
bfound is tested atb-1,bandb+1—max-width: 768pxmatches at 768, so the mobile/desktop split must beasserted on the correct side of the boundary. Today that derivation yields
exactly one breakpoint (768; there are currently no
sm:/md:/lg:classesanywhere), and if anyone adds one the harness picks it up and tests it without
being edited. Plus a 320px floor, a 1280px desktop baseline, and two landscape
viewports straddling the boundary for the rotation case.
Assertions (all evaluated, all reported, non-zero exit if any fail): no
document-level horizontal overflow; no clipped content (excluding deliberate
.truncateellipsis, which is opt-in truncation, not breakage); minimum taptarget 44x44 CSS px on pin buttons, interval select, pause button and debug
toggle; and a genuine reflow assertion on computed layout — mobile must be
flex-direction: columnwith the sparkline geometrically below the infoblock, desktop must be
rowwith it beside. Also asserts probing and gatewaydetection still run at mobile widths, since the rejected #8 early-return is the
thing that would silently regress. Screenshots go to
tmp/viewport/asartifacts.
Harness-can-fail proof: a deliberate wide fixed-width element planted in a
host row, harness observed failing, then reverted. Result reported here.
Wiring: own
maketarget, deliberately out ofmake check— it needsDocker and is far past the 20s
make testbudget. Relation to #21:vitestwill cover module-level logic in-process; this covers rendered layout in a real
engine. Different layers, no overlap.
#38 reconciliation: #38 moves frontend-only gates to a
script/frontend-*namespace, so the entrypoint is named
script/frontend-viewport-testup frontto land in that namespace rather than need renaming after.
Done in #44. Harness built, harness proven able to fail (twice — planted
wide element, and the reflow rule neutered), limits documented in
test/viewport/README.mdso this does not get re-parked as needinghardware.
Widths tested, all derived from
src/styles.cssrather than from phonemodels — 320, 667, 767 / 768 / 769 (one below, exactly on, one above the
max-width: 768pxboundary), 844, 1280. Rationale, the definition of"correct" at each, and the driver/pinning decisions are in the PR body.
QA result: two real defects, filed as #42 and #43. Everything else —
reflow, clipping, rendering, probing, gateway detection — passes at every
width. No residual check here requires physical hardware; the genuine gaps
are the WebKit engine, real touch, pixel density and on-device performance,
listed in the harness README.
Correction: the overflow assertion I specified would have false-passed
I wrote
document.documentElement.scrollWidth <= innerWidthinto the definition of done. That check passes on a page that is visibly overflowing.Under Chrome's mobile emulation
window.innerWidthgrows to match the overflowing content width, so the comparison becomes350 <= 350and reports clean. The implementer's first pass followed my spec literally and declared the layout fine. It only found the real bug (#42) after switching toMath.min(innerWidth, documentElement.clientWidth)with--hide-scrollbars.Worth recording for two reasons.
First, had the harness-can-fail requirement not been in the DoD, this would have shipped as a fourth mechanism reporting green while measuring nothing — after #14 (inert lint config), #16 (root gate skipping the backend) and #37 (CI served from cache). The requirement caught a defect introduced by the same document that imposed it.
Second, the general lesson: a browser API that sounds like it means "the viewport" may be defined relative to the content instead. Anyone reusing this harness elsewhere should carry
Math.min(innerWidth, clientWidth)rather than the obvious form.