check / check (push) Canceled after 0s
test/viewport/README.md is rewritten from scratch so that every sentence reads on the first pass. It says what the layout test checks, at which widths, how to run it and what it cannot cover, and drops the history of how the test came about. Part of #189. Model: fable-5-1 (rewrite, review); opus-5-5 (merge) Co-authored-by: clawbot <clawbot@noreply.example.org>
147 lines
7.2 KiB
Markdown
147 lines
7.2 KiB
Markdown
# The viewport test
|
|
|
|
`make frontend-viewport-test` checks the page's layout at several window widths
|
|
in a real browser:
|
|
|
|
```bash
|
|
make frontend-viewport-test
|
|
```
|
|
|
|
First it runs the unit tests. Then it builds the page into `dist/`. It serves
|
|
`dist/` with the nginx image and the `configs/nginx.conf` that the Docker image
|
|
ships. Before the browser starts, it fetches `/` and `/no-such-page`, which
|
|
nginx answers with the page. Both must carry `Cache-Control: no-cache` and a
|
|
`Content-Security-Policy` header. Then it starts a headless Chrome in a
|
|
container and opens the page in it at each width. For each width it measures the
|
|
layout inside the page and checks the measurements in Node. The checks decide
|
|
pass or fail. Screenshots and a `results.json` land in `tmp/viewport/`, for a
|
|
person to look at when a check fails.
|
|
|
|
The test is not part of `make check`: it takes minutes, and `make test` has to
|
|
finish in under 60 seconds.
|
|
|
|
## The widths
|
|
|
|
The breakpoints are read from the page's own files. `viewports.js` reads the
|
|
`min-width` and `max-width` conditions out of the `@media` blocks in
|
|
`web/src/styles.css`. It also looks in `web/src/main.js` and `web/index.html`
|
|
for Tailwind's responsive class prefixes, such as `md:`, and takes their
|
|
breakpoints. Each breakpoint is tested at one pixel below it, exactly on it, and
|
|
one pixel above it. Today the CSS has one breakpoint, `max-width: 768px`, and no
|
|
responsive prefix is used.
|
|
|
|
A `max-width: 768px` block matches at 768 pixels. Testing both sides of each
|
|
breakpoint catches a layout that switches one pixel too early or too late. A
|
|
second media block, or a first `md:` class, is covered without a change to this
|
|
directory.
|
|
|
|
The page is desktop-first: the wide layout is the default, and every narrow rule
|
|
is in a `max-width` block. So a width is expected to show the narrow layout when
|
|
one of those blocks matches it. A page with only `min-width` blocks would be
|
|
read the other way round. A page that mixes the two kinds fails the run with a
|
|
message, because the right expectation for each width can no longer be read from
|
|
the breakpoints alone.
|
|
|
|
Four more widths are fixed, each for a stated reason:
|
|
|
|
- 320 by 568 pixels: the narrowest width still in common use
|
|
- 667 by 375 pixels: a phone in landscape, inside the narrow layout
|
|
- 844 by 390 pixels: a large phone in landscape, in the wide layout but still a
|
|
touch device
|
|
- 1280 by 800 pixels: a desktop window
|
|
|
|
Every width but 1280 by 800 is tested as a touch device.
|
|
|
|
## The checks
|
|
|
|
A failed check does not stop the others.
|
|
|
|
- `app-rendered`: at least 10 WAN host rows exist, and at least 5 rows show a
|
|
number as their latency. Without this, the other checks could pass against a
|
|
blank page.
|
|
- `no-horizontal-overflow`: the document is no wider than the window. On a
|
|
failure, the widest content is named.
|
|
- `nothing-past-viewport-edge`: no visible element extends past the window's
|
|
edge.
|
|
- `no-clipped-text`: no element hides its text behind `overflow: hidden` or
|
|
`overflow: clip`. Text cut with an ellipsis is allowed: that cut is a design
|
|
choice, not a defect. Tailwind's `truncate` cuts host names and URLs that way.
|
|
- `tap-targets-44px`, on touch widths: each measured control is at least 44 by
|
|
44 CSS pixels. The controls are `#pause-btn`, `#interval-select`,
|
|
`#debug-toggle` and the `.pin-btn` in each WAN host row. The check also
|
|
demands that each selector matches enough visible elements: one of each single
|
|
control, and one pin button per WAN host row. A renamed class would otherwise
|
|
take its controls out of the measured set, and the check would pass on
|
|
nothing.
|
|
- `host-rows-stacked` or `host-rows-side-by-side`: each host row shows the
|
|
layout expected at that width. In the narrow layout, the row's
|
|
`flex-direction` is `column`, the graph sits below the host's details, and
|
|
each takes at least 90% of the row's width. In the wide layout, the direction
|
|
is `row` and the graph sits to the right of the details. A row that only
|
|
shrank its fixed 420-pixel column fails.
|
|
- `probing-still-runs`: the page sent at least one check to a target at this
|
|
width. The narrow layout must not skip the checks.
|
|
- `gateway-detection-still-runs`: the `Local Gateway` row is present at this
|
|
width, so the gateway was looked for and found. The narrow layout must not
|
|
skip that search.
|
|
|
|
### The tap-target size
|
|
|
|
44 by 44 CSS pixels is the figure in Apple's Human Interface Guidelines and in
|
|
WCAG 2.2 success criterion 2.5.5, "Target Size (Enhanced)". WCAG 2.2 criterion
|
|
2.5.8 sets a lower minimum of 24 by 24 pixels, with an exception that depends on
|
|
the space around a control. The pin buttons sit against the host name they
|
|
belong to, so the test keeps 44.
|
|
|
|
## The browser's network
|
|
|
|
The browser container runs on an internal Docker network with no route to the
|
|
internet, so the page's checks reach no real host. The test answers them itself
|
|
from a fixed table of delays: 2, 45, 123, 456 and 780 milliseconds. Which delay
|
|
a target gets depends on its URL, and about one target in seven fails outright.
|
|
So the rows show one-, two- and three-digit latencies and some unreachable rows,
|
|
and the layout is tested against that spread. The request to
|
|
`http://192.168.1.1` is answered after 5 milliseconds, so the `Local Gateway`
|
|
row appears.
|
|
|
|
At each width, the test sets the interval to 1 second and waits until at least 5
|
|
rows show a latency before it measures. The page discards its first round, so
|
|
this reaches a filled page without waiting out two rounds at 3 seconds.
|
|
|
|
## What the test cannot check
|
|
|
|
- Other browser engines. The test runs Chrome. Safari uses WebKit, and bugs that
|
|
only WebKit has do not show up here.
|
|
- Real touch input. Touch emulation changes what the page is told, not how a
|
|
finger behaves. Gestures, scroll momentum, double-tap zoom and hover states on
|
|
touch are not tested.
|
|
- Physical pixels. The device pixel ratio is set, but antialiasing, colour
|
|
rendering and legibility at a given physical size are not measured.
|
|
- Fonts. The container has DejaVu, not a phone's own monospace font. Text is
|
|
about as wide as on a real device, but not exactly. A layout that fits here by
|
|
a few pixels might not fit there.
|
|
- Performance. The cost of redrawing the graphs, battery use and behaviour on a
|
|
slow connection are not measured.
|
|
- The browser's own frame. The address bar, safe-area insets and notch cutouts
|
|
are not simulated.
|
|
|
|
## The files
|
|
|
|
- `viewports.js` derives the widths from the sources.
|
|
- `facts.js` is run inside the page and measures the layout. It only measures;
|
|
it decides nothing.
|
|
- `checks.js` turns the measurements into the pass or fail of each check, with
|
|
the numbers that decided it.
|
|
- `harness.js` connects to the browser, runs each width, writes the screenshots
|
|
and `results.json`, and prints the results. `script/frontend-viewport-test`
|
|
starts it; every input it needs arrives in the environment.
|
|
|
|
## Relation to the unit tests
|
|
|
|
The unit tests in `test/unit/` call the functions that `web/src/main.js`
|
|
exports, with no browser. `make test` runs them with Node's built-in test
|
|
runner. This test runs the built page in a real browser engine and is the only
|
|
test here that can see a media query. Neither replaces the other. Checks on
|
|
computed styles and element geometry belong here; checks on functions belong in
|
|
`test/unit/`.
|