Files
netwatch/test/viewport
clawbot 64e142c17f
check / check (push) Waiting to run
README, TODO and the viewport README say what the tree does (closes #24)
README.md: Getting Started leads with make targets; a Backend section
gives netwatch-server's routes and how the image builds and runs it;
the checks are GET requests; the WAN host list, health states, summary
figures, sorting and missing features match src/main.js; the TODO
section points to TODO.md, which holds the one to-do list.

backend/README.md: its TODO section points to TODO.md too, whose
Future Steps take its three open items.

TODO.md: Workflow branches from next and opens the PR against next;
Status, Next Step and Future Steps describe the open work, linked to
its issue where one exists.

test/viewport/README.md: the unit tests run on Node's test runner,
not vitest.

Model: opus-5-5
2026-10-04 07:03:05 +02: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. 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/.