Files
netwatch/test/viewport
clawbot 1e290a63cf
All checks were successful
check / check (push) Successful in 38s
test: automated responsive-layout harness (closes #13)
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).
2026-08-09 14:52:52 +00:00
..

Responsive-layout harness

Automated verification of the responsive layout that landed in #5. Run it with:

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-overflowdocumentElement.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.