All checks were successful
check / check (push) Successful in 38s
Verifies the mobile layout from #5 with a real browser engine instead of by hand on a phone. make frontend-viewport-test builds dist/, serves it from the same digest-pinned nginx image and the same nginx.conf the shipping container uses, and drives a digest-pinned headless Chrome against it over CDP. Viewport widths are derived from the app's own CSS rather than from a list of phone models: the @media conditions in src/styles.css and any Tailwind responsive prefixes in the markup are parsed, and each breakpoint is tested one pixel below, exactly on, and one pixel above. max-width: 768px matches at 768, and a generic 375px test sails past that boundary entirely. Four anchor viewports are added with stated reasons: a 320px floor, a desktop baseline, and two phone-landscape sizes straddling the breakpoint. Assertions are on computed layout, not screenshots: horizontal overflow, elements past the viewport edge, clipped text (deliberate ellipsis truncation excluded), 44x44 minimum tap targets, and genuine reflow of the host rows checked on both flex-direction and geometry. Probing and gateway detection are asserted to still run at narrow widths, since the early-return mobile path rejected in #8 is what would silently regress. Screenshots are written to tmp/viewport/ as artifacts alongside the results, not as the evidence. puppeteer-core rather than playwright: it is the one variant of either that never downloads or bundles a browser, so the browser stays a digest-pinned image and the npm side is pinned by yarn.lock integrity. The browser container runs on an --internal docker network with no route off the host; the harness answers the app's latency probes itself from a fixed delay table so the rows render a realistic spread of value widths. Kept out of make check: it needs Docker and takes minutes, where make test has to stay under 20 seconds. The harness was observed failing before being trusted, twice: a planted 900px fixed-width element in a host row, and the mobile reflow rule neutered. Both reverted. Against the current layout it reports two real defects, filed as #42 (horizontal overflow at 320px) and #43 (tap targets below 44x44).
106 lines
5.4 KiB
Markdown
106 lines
5.4 KiB
Markdown
# Responsive-layout harness
|
|
|
|
Automated verification of the responsive layout that landed in #5. Run it with:
|
|
|
|
```bash
|
|
make frontend-viewport-test
|
|
```
|
|
|
|
It builds `dist/`, serves it from the same digest-pinned `nginx` image and the
|
|
same `nginx.conf` the shipping container uses, drives a digest-pinned headless
|
|
Chrome against it over CDP, and asserts on computed layout at every viewport
|
|
width derived from the app's own CSS. Screenshots land in `tmp/viewport/`
|
|
alongside a `results.json`; they are artifacts for a human to look at when
|
|
something fails, not the evidence. The assertions are the evidence.
|
|
|
|
The target is deliberately outside `make check`: it needs Docker and takes
|
|
minutes, and `make test` has to stay under 20 seconds.
|
|
|
|
## How the widths are chosen
|
|
|
|
Not from a list of phone models. `viewports.js` parses the `@media` conditions
|
|
out of `src/styles.css` and scans `src/main.js` and `index.html` for Tailwind
|
|
responsive prefixes, then tests every breakpoint it finds at one pixel below it,
|
|
exactly on it, and one pixel above it. A generic 375px "phone" test sails
|
|
straight past an off-by-one at a media query boundary; `max-width: 768px`
|
|
matches _at_ 768, and the sweep pins down which side of that line each layout is
|
|
on.
|
|
|
|
Nothing hardcodes 768. Add a second media block or start using `md:` classes and
|
|
the new breakpoint is covered without this directory being touched. Four further
|
|
viewports are fixed anchors, each with a stated reason: a 320px floor, a 1280px
|
|
desktop baseline, and two phone-landscape sizes straddling the breakpoint for
|
|
the rotation case.
|
|
|
|
## What it asserts
|
|
|
|
- **app-rendered** — enough host rows exist and enough of them show a numeric
|
|
latency. This one exists so the rest cannot pass vacuously against a blank
|
|
page.
|
|
- **no-horizontal-overflow** — `documentElement.scrollWidth` fits the layout
|
|
viewport, with the widest offending element named.
|
|
- **nothing-past-viewport-edge** — no visible element's box extends past the
|
|
viewport edge.
|
|
- **no-clipped-text** — nothing hides text behind `overflow: hidden`. Deliberate
|
|
ellipsis truncation (Tailwind's `truncate`, used on host names and URLs) is
|
|
excluded: it is a design choice, not breakage.
|
|
- **tap-targets-44px** — every interactive control is at least 44x44 CSS px on
|
|
touch viewports. See below.
|
|
- **host-rows-stacked / host-rows-side-by-side** — the rows genuinely reflow.
|
|
Computed `flex-direction` _and_ the actual geometry are checked, and in the
|
|
narrow layout the info block and the sparkline must each occupy essentially
|
|
the full row width. A row that merely shrank its 420px column would fail.
|
|
- **probing-still-runs / gateway-detection-still-runs** — narrow viewports keep
|
|
probing and keep detecting the gateway. The mobile early-return path proposed
|
|
in #8 was rejected; this is what would catch it coming back.
|
|
|
|
### The tap-target threshold
|
|
|
|
44x44 CSS px. That is the figure in Apple's Human Interface Guidelines and in
|
|
WCAG 2.2 SC 2.5.5 "Target Size (Enhanced)". WCAG 2.2 SC 2.5.8 (level AA) sets a
|
|
lower 24x24 floor, but that floor comes with a spacing exception these controls
|
|
do not qualify for — the pin buttons sit directly against the host name they
|
|
belong to.
|
|
|
|
## Determinism
|
|
|
|
The browser container runs on an `--internal` docker network and has no route to
|
|
the internet, so the app's latency probes cannot reach anything real. The
|
|
harness answers them itself from a fixed delay table, with a deterministic
|
|
fraction failed outright, so the rows render a realistic spread of one-, two-
|
|
and three-digit latencies plus some unreachable rows. That spread is what the
|
|
layout has to survive; 24 identical `---` placeholders would not exercise it.
|
|
|
|
## What this cannot verify
|
|
|
|
Real limits, so nobody re-parks this issue as needing hardware:
|
|
|
|
- **Non-Chromium engines.** This is Chrome. iOS Safari is WebKit and cannot be
|
|
emulated by it; Safari-specific bugs (viewport units under a collapsing URL
|
|
bar, `-webkit-fill-available`, form control metrics) will not show up here.
|
|
- **Real touch input.** `hasTouch` emulation changes what the page is told, not
|
|
how a finger behaves. Gesture handling, scroll momentum, double-tap zoom and
|
|
hover-state fallbacks on touch are out of scope.
|
|
- **Physical pixel density and rendering.** `deviceScaleFactor` is set, but
|
|
subpixel antialiasing, OLED colour rendering and actual legibility at a given
|
|
physical size are not measurable here.
|
|
- **Fonts.** The container has DejaVu, not the platform's own UI monospace. Text
|
|
metrics are therefore close to, but not identical to, a real device — a layout
|
|
that fits here by a few pixels might not there.
|
|
- **On-device performance.** Canvas sparkline redraw cost, battery, and
|
|
behaviour on a slow radio are not measured.
|
|
- **Browser chrome.** The address bar, safe-area insets and notch cutouts are
|
|
not simulated.
|
|
|
|
Everything else this issue was actually about — does the layout reflow, does
|
|
anything overflow, is content clipped, are the controls big enough — is a
|
|
function of viewport width and CSS, and is covered above.
|
|
|
|
## Relation to the unit test framework (#21)
|
|
|
|
Complementary layers, not two stacks. `vitest` (#21) will exercise module-level
|
|
logic in-process with no browser. This harness exercises rendered layout in a
|
|
real engine and is the only thing here that can see a media query. Neither
|
|
replaces the other; assertions about computed styles and element geometry belong
|
|
here, assertions about functions belong in `vitest`.
|