check / check (push) Waiting to run
The shared files are the sneak/prompts copies at dd4027b, plus this repository's own entries. make lint and make test each build one Dockerfile phase without the cache, both covering the frontend; the builder stage waits on both and takes its version from git describe unless VERSION is given. The test phase keeps Go's module and build caches in memory, out of the image make test tags. golangci-lint moves to v2.14.0 with the new .golangci.yml; one test spells X-Request-ID as canonicalheader asks. prettier formats only JavaScript, CSS, HTML and Markdown, so .golangci.yml stays as fetched. script/fmt and script/fmt-check put ~/.local/bin on PATH. script/bootstrap keeps a Go only if it is exactly GO_VERSION, and re-checks the go on PATH after installing. Model: opus-5-5
115 lines
6.0 KiB
Markdown
115 lines
6.0 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 takes minutes, and
|
|
`make test` has to stay under 60 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: one of each single control, and one
|
|
pin button per WAN host row. 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; a `---` placeholder in every row 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 tests
|
|
|
|
Complementary layers, not two stacks. The unit tests in `test/unit/`, which
|
|
`make test` runs with Node's built-in test runner, exercise the functions
|
|
`src/main.js` exports 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 `test/unit/`.
|