Files
netwatch/test/viewport/README.md
T
clawbot 14eb376d79
check / check (push) Failing after 1s
test: automated responsive-layout harness (closes #13)
`make frontend-viewport-test` builds `dist/`, serves it from the same
digest-pinned nginx image and nginx.conf the shipping container uses, and
drives a digest-pinned headless Chrome over CDP. Viewport widths are derived
from the app's own @media breakpoints rather than a list of phone models: each
breakpoint is tested one pixel below, on, and above, plus four anchor
viewports. Assertions are on computed layout — horizontal overflow, off-screen
elements, clipped text, 44x44 tap targets, host-row reflow — not screenshots,
and each check declares the minimum elements it must find so a stale selector
fails instead of passing blind against a page it is not measuring. Kept out of
`make check`: it needs Docker and takes minutes. Proven able to fail before
being trusted.

Model: opus-4-8
2026-09-21 18:29:20 +02:00

113 lines
5.9 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. The app is
desktop-first today (all narrow rules live in `max-width` blocks); a
`min-width`-only, mobile-first set is handled as its inverse, and a set that
mixes the two makes the run fail loudly rather than test the right widths with
the wrong expectation. 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, _and_ each selector in the control list matched at least the
number of visible elements it declares. The second half is what stops the
check passing vacuously: with size alone, a renamed class would take its
controls out of the measured set and the check would report "all 0 controls
are at least 44x44" and pass. 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`.