All checks were successful
check / check (push) Successful in 38s
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).
96 lines
3.7 KiB
Bash
Executable File
96 lines
3.7 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/frontend-viewport-test: verify the responsive layout of the built
|
|
# frontend in a real browser engine.
|
|
#
|
|
# Builds dist/, serves it with the same nginx image and the same nginx.conf
|
|
# the shipping container uses, points a containerised headless Chrome at it
|
|
# over CDP, and asserts on computed layout at every viewport width derived
|
|
# from the app's own CSS. See test/viewport/README.md for what this covers
|
|
# and what it cannot.
|
|
#
|
|
# Deliberately not part of script/check: it needs Docker and takes far
|
|
# longer than the 20s budget make test has to stay inside.
|
|
set -eu
|
|
|
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
|
|
# chromedp/headless-shell 151.0.7922.109, 2026-08-09
|
|
BROWSER_IMAGE="chromedp/headless-shell@sha256:2d349b544a1ea6b5b5fd7c0fe99215ff662339c57407ee2e8c0a11af93516b04"
|
|
# nginx:stable-alpine, 2026-02-22 (the digest Dockerfile ships)
|
|
SERVER_IMAGE="nginx@sha256:15e96e59aa3b0aada3a121296e3bce117721f42d88f5f64217ef4b18f458c6ab"
|
|
# node:22-alpine, 2026-02-22 (the digest Dockerfile builds with)
|
|
NODE_IMAGE="node@sha256:e4bf2a82ad0a4037d28035ae71529873c069b13eb0455466ae0bc13363826e34"
|
|
|
|
RUN_ID="$$-$(date +%s)"
|
|
NETWORK="netwatch-viewport-$RUN_ID"
|
|
SERVER="netwatch-viewport-server-$RUN_ID"
|
|
BROWSER="netwatch-viewport-browser-$RUN_ID"
|
|
ARTIFACT_DIR="$ROOT/tmp/viewport"
|
|
|
|
cleanup() {
|
|
docker rm -f "$BROWSER" > /dev/null 2>&1 || true
|
|
docker rm -f "$SERVER" > /dev/null 2>&1 || true
|
|
docker network rm "$NETWORK" > /dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
main() {
|
|
cd "$ROOT"
|
|
|
|
# Test what ships: the production build, not a dev server.
|
|
"$ROOT/script/test"
|
|
if [ ! -f "$ROOT/dist/index.html" ]; then
|
|
echo "frontend-viewport-test: dist/index.html missing after build" >&2
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$ARTIFACT_DIR"
|
|
|
|
# An --internal network has no route off the host, so the browser
|
|
# cannot reach the real internet no matter what the page asks for.
|
|
# Latency probes are answered by the harness instead. This also means
|
|
# no port can be published from it, which is why the harness itself
|
|
# runs as a third container on the same network rather than on the
|
|
# host.
|
|
docker network create --internal "$NETWORK" > /dev/null
|
|
|
|
docker run -d --rm --name "$SERVER" \
|
|
--network "$NETWORK" --network-alias netwatch \
|
|
-v "$ROOT/dist:/usr/share/nginx/html:ro" \
|
|
-v "$ROOT/nginx.conf:/etc/nginx/conf.d/default.conf:ro" \
|
|
"$SERVER_IMAGE" > /dev/null
|
|
|
|
# The image's own entrypoint already exposes CDP on 9222 and passes
|
|
# --no-sandbox, so only extra flags belong here; re-specifying the
|
|
# debugging port collides with it and leaves the endpoint bound to
|
|
# loopback only. --hide-scrollbars keeps innerWidth equal to
|
|
# clientWidth, so the overflow assertion has no scrollbar-sized slack
|
|
# to hide behind, and matches the overlay scrollbars phones use.
|
|
docker run -d --rm --name "$BROWSER" --init --shm-size=1g \
|
|
--network "$NETWORK" \
|
|
"$BROWSER_IMAGE" \
|
|
--hide-scrollbars \
|
|
> /dev/null
|
|
|
|
# Chrome refuses DevTools requests whose Host header is neither
|
|
# localhost nor an IP address, so dial the container by address rather
|
|
# than by its network alias.
|
|
browser_ip="$(docker inspect \
|
|
-f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
|
|
"$BROWSER")"
|
|
|
|
timeout 900 docker run --rm --init \
|
|
--network "$NETWORK" \
|
|
--user "$(id -u):$(id -g)" \
|
|
-v "$ROOT:/app" \
|
|
-w /app \
|
|
-e NETWATCH_ROOT=/app \
|
|
-e NETWATCH_BASE_URL=http://netwatch:8080 \
|
|
-e "NETWATCH_CDP_URL=http://$browser_ip:9222" \
|
|
-e NETWATCH_ARTIFACT_DIR=/app/tmp/viewport \
|
|
"$NODE_IMAGE" \
|
|
node test/viewport/harness.js
|
|
}
|
|
|
|
main "$@"
|