QA: test responsive mobile layout on real devices #13

Open
opened 2026-08-07 17:49:33 +02:00 by clawbot · 5 comments
Collaborator

Per the decision in #8, main keeps the responsive mobile layout from #5 (closes #2) rather than a "not available on mobile" message. This issue tracks manual QA of that layout on actual mobile hardware.

What to verify

  • Load the app on a phone (portrait and landscape) and confirm the host-row layout from the max-width: 768px media block in src/styles.css renders correctly
  • Check readability of host rows, status dots, and latency values at narrow widths
  • Rotate the device and confirm the layout reflows sanely across the 768px boundary
  • Confirm polling and gateway detection behave normally on mobile (nothing is skipped — the early-return mobile path from #8 was rejected)
  • Note any layout breakage or unusable interactions as follow-up issues

Ref #2, #5, #8

Per the decision in #8, `main` keeps the responsive mobile layout from #5 (closes #2) rather than a "not available on mobile" message. This issue tracks manual QA of that layout on actual mobile hardware. ### What to verify - Load the app on a phone (portrait and landscape) and confirm the host-row layout from the `max-width: 768px` media block in `src/styles.css` renders correctly - Check readability of host rows, status dots, and latency values at narrow widths - Rotate the device and confirm the layout reflows sanely across the 768px boundary - Confirm polling and gateway detection behave normally on mobile (nothing is skipped — the early-return mobile path from #8 was rejected) - Note any layout breakage or unusable interactions as follow-up issues Ref #2, #5, #8
sneak was assigned by clawbot 2026-08-07 17:49:33 +02:00
Author
Collaborator

Assigning to @sneak: this QA pass needs real physical devices (phones/tablets), which automation here cannot provide. The responsive layout itself landed via PR #5 and the CI gate via PR #12; what remains is hands-on verification on actual hardware. If you find rendering problems, please note device/browser/viewport details here and reassign to clawbot for fixes.

Assigning to @sneak: this QA pass needs real physical devices (phones/tablets), which automation here cannot provide. The responsive layout itself landed via PR #5 and the CI gate via PR #12; what remains is hands-on verification on actual hardware. If you find rendering problems, please note device/browser/viewport details here and reassign to clawbot for fixes.
Author
Collaborator

Reopening for automation — my earlier "needs physical devices" call was wrong

I parked this with @sneak on the grounds that "this QA pass needs real physical devices (phones/tablets), which automation here cannot provide." That was incorrect, and it parked a whole issue on the wrong premise. Correction relayed from sneak via the fleet manager: this host has full build tooling and Docker, so a scriptable headless browser with device emulation can verify essentially all of it.

Taking it back off sneak's queue and rewriting the definition of done accordingly.

The mistake is worth naming precisely, because it is a reasoning error rather than a missing fact: I conflated "testing a mobile layout" with "testing on mobile hardware." The overwhelming majority of what this issue actually cares about — does the layout reflow, does anything overflow horizontally, are tap targets big enough, is content clipped — is a function of viewport width and CSS, not of the physical device. Emulation covers it.

Revised definition of done

The harness

  • Headless Chrome in a container, driven over CDP (Playwright or Puppeteer are both fine; pick one and say why). The browser image is pinned by digest, like every other external dependency — no :latest, no version tag. Version+date comment above the reference.
  • Driven through a make target and a script/ entrypoint, consistent with everything else in this repo. Never a raw browser or node invocation.
  • The harness runs against the built artifact — serve dist/ (or the built image) rather than a dev server, so what is tested is what ships.

The viewports

  • Test the widths the CSS actually declares, not a generic "phone" size. Read the breakpoints out of src/styles.css and the Tailwind classes in src/main.js and enumerate them. Include one width just below and one just above each breakpoint — off-by-one at a media query boundary is the classic failure and a generic 375px test sails straight past it.
  • The issue must name the widths tested and what "correct" looks like at each, so the result is reproducible rather than a screenshot somebody eyeballed.

The assertions — these must be able to fail

  • No horizontal overflow: document.documentElement.scrollWidth <= innerWidth at every tested width.
  • No clipped content: no element whose scrollWidth materially exceeds its clientWidth in a way that hides text.
  • Interactive controls (the pin buttons, the interval dropdown, the play/pause and debug toggles) meet a minimum tap-target size. State the threshold you use and where it comes from.
  • The host rows actually reflow rather than merely shrinking — assert on computed layout, not just on a screenshot diff.
  • Screenshots are captured as artifacts alongside the assertions, not as the evidence itself. "It looked fine" is not a check.

Prove the harness can fail — non-negotiable

  • Plant a deliberate layout break (e.g. force a wide fixed-width element into a host row), confirm the harness fails, then revert. A check that has never been observed failing is not a check. This repo has now shipped three separate mechanisms that reported green while verifying nothing — an inert lint config (#14), a root gate that skipped the backend (#16), and a CI build served from cache (#37). Do not add a fourth.

Document the limits, so this does not get re-parked later

  • State plainly in the issue what the harness covers and what it genuinely cannot. My current understanding of the real gaps: device-specific browser engine quirks (iOS Safari in particular, which is not Chrome and cannot be emulated by it), true touch-hardware behaviour, physical pixel density and rendering differences, and on-device performance. Those are narrow and mostly not what this issue was about.
  • If any residual check genuinely requires hardware, list exactly that check rather than returning the whole issue to sneak.

Implementation requirements

  • Any new dependency gets checked against the org package defaults first. Playwright bundles its own browsers, which interacts with the digest-pinning requirement — resolve that deliberately and say how.
  • Keep the harness out of the default make check path unless it is fast and hermetic; if it is slow, give it its own target and wire it into CI separately. make test must stay under 20 seconds.
  • Note that #21 adds a frontend test framework (vitest). Coordinate — this harness is browser-level and complementary, not a replacement, but they should not end up as two unrelated test stacks. Say in the PR how they relate.
  • TODO.md updated in the same commit; commit title ends with (closes #13).

Note on provenance

This reassignment reached me via the fleet manager relaying sneak, not from sneak directly in the tracker. Flagging that so the record is accurate. It is a low-risk instruction — working a tracker issue is my normal job, and the technical premise is verifiably correct — so I am proceeding rather than round-tripping for confirmation. @sneak, if you did not intend this, say so and I will park it again.

If the harness turns out to be reusable, that is worth knowing beyond this repo — another repo on the roster reportedly wrote off much of its 1.0.0 milestone as unverifiable for exactly the reason I got wrong here.

## Reopening for automation — my earlier "needs physical devices" call was wrong I parked this with @sneak on the grounds that "this QA pass needs real physical devices (phones/tablets), which automation here cannot provide." That was incorrect, and it parked a whole issue on the wrong premise. Correction relayed from sneak via the fleet manager: this host has full build tooling and Docker, so a scriptable headless browser with device emulation can verify essentially all of it. Taking it back off sneak's queue and rewriting the definition of done accordingly. The mistake is worth naming precisely, because it is a reasoning error rather than a missing fact: I conflated *"testing a mobile layout"* with *"testing on mobile hardware."* The overwhelming majority of what this issue actually cares about — does the layout reflow, does anything overflow horizontally, are tap targets big enough, is content clipped — is a function of **viewport width and CSS**, not of the physical device. Emulation covers it. ## Revised definition of done ### The harness - [ ] Headless Chrome in a container, driven over CDP (Playwright or Puppeteer are both fine; pick one and say why). **The browser image is pinned by digest**, like every other external dependency — no `:latest`, no version tag. Version+date comment above the reference. - [ ] Driven through a `make` target and a `script/` entrypoint, consistent with everything else in this repo. Never a raw browser or node invocation. - [ ] The harness runs against the **built** artifact — serve `dist/` (or the built image) rather than a dev server, so what is tested is what ships. ### The viewports - [ ] Test the widths **the CSS actually declares**, not a generic "phone" size. Read the breakpoints out of `src/styles.css` and the Tailwind classes in `src/main.js` and enumerate them. Include one width just below and one just above each breakpoint — off-by-one at a media query boundary is the classic failure and a generic 375px test sails straight past it. - [ ] The issue must **name the widths tested and what "correct" looks like at each**, so the result is reproducible rather than a screenshot somebody eyeballed. ### The assertions — these must be able to fail - [ ] No horizontal overflow: `document.documentElement.scrollWidth <= innerWidth` at every tested width. - [ ] No clipped content: no element whose `scrollWidth` materially exceeds its `clientWidth` in a way that hides text. - [ ] Interactive controls (the pin buttons, the interval dropdown, the play/pause and debug toggles) meet a minimum tap-target size. State the threshold you use and where it comes from. - [ ] The host rows actually reflow rather than merely shrinking — assert on computed layout, not just on a screenshot diff. - [ ] Screenshots are captured as **artifacts alongside** the assertions, not as the evidence itself. "It looked fine" is not a check. ### Prove the harness can fail — non-negotiable - [ ] Plant a deliberate layout break (e.g. force a wide fixed-width element into a host row), confirm the harness **fails**, then revert. A check that has never been observed failing is not a check. This repo has now shipped three separate mechanisms that reported green while verifying nothing — an inert lint config (#14), a root gate that skipped the backend (#16), and a CI build served from cache (#37). Do not add a fourth. ### Document the limits, so this does not get re-parked later - [ ] State plainly in the issue what the harness covers and what it genuinely cannot. My current understanding of the real gaps: device-specific browser engine quirks (iOS Safari in particular, which is not Chrome and cannot be emulated by it), true touch-hardware behaviour, physical pixel density and rendering differences, and on-device performance. Those are narrow and mostly not what this issue was about. - [ ] If any residual check genuinely requires hardware, list exactly that check rather than returning the whole issue to sneak. ## Implementation requirements - Any new dependency gets checked against the org package defaults first. Playwright bundles its own browsers, which interacts with the digest-pinning requirement — resolve that deliberately and say how. - Keep the harness out of the default `make check` path unless it is fast and hermetic; if it is slow, give it its own target and wire it into CI separately. `make test` must stay under 20 seconds. - Note that #21 adds a frontend test framework (`vitest`). Coordinate — this harness is browser-level and complementary, not a replacement, but they should not end up as two unrelated test stacks. Say in the PR how they relate. - `TODO.md` updated in the same commit; commit title ends with ` (closes #13)`. ## Note on provenance This reassignment reached me via the fleet manager relaying sneak, not from sneak directly in the tracker. Flagging that so the record is accurate. It is a low-risk instruction — working a tracker issue is my normal job, and the technical premise is verifiably correct — so I am proceeding rather than round-tripping for confirmation. @sneak, if you did not intend this, say so and I will park it again. If the harness turns out to be reusable, that is worth knowing beyond this repo — another repo on the roster reportedly wrote off much of its 1.0.0 milestone as unverifiable for exactly the reason I got wrong here.
sneak was unassigned by clawbot 2026-08-09 16:03:55 +02:00
clawbot self-assigned this 2026-08-09 16:03:55 +02:00
clawbot added this to the 1.0.0 milestone 2026-08-09 16:03:55 +02:00
Author
Collaborator

Implementation plan

Driver: puppeteer-core (devDependency), not playwright. Reason, and the
resolution of the bundled-browser problem the DoD flags: puppeteer-core is the
one variant of either library that never downloads or bundles a browser — it
only speaks CDP to a browser you point it at. So the browser stays a
digest-pinned container image and the npm side is pinned by yarn.lock
integrity hashes. playwright expects its own version-matched browser download,
which would either fight the digest pin or force us onto the ~2 GB official
Playwright image.

Browser image: chromedp/headless-shell 151.0.7922.109, referenced by
@sha256: digest with a version+date comment.

Topology: script/frontend-viewport-test builds dist/ via the existing
build entrypoint, then brings up two --rm containers on a private
--internal docker network:

  • the repo's own pinned nginx digest (same one Dockerfile uses) serving
    dist/ with the repo's own nginx.conf — so the harness runs against the
    built artifact and the shipping server config, not a dev server;
  • the headless-shell container, CDP published on an ephemeral 127.0.0.1 port.

The network is --internal, so the browser physically cannot reach the real
internet; on top of that every off-origin request is intercepted and served a
deterministic canned response (a fixed delay table, plus a couple of forced
failures) so host rows render a realistic mix of numeric latencies, TIMEOUT and
offline states rather than 24 identical --- placeholders. Containers and
network are torn down in a trap.

Widths are derived, not hardcoded. The harness parses src/styles.css for
@media min-width/max-width values and scans src/main.js + index.html
for Tailwind responsive prefixes, mapping them to Tailwind v4 default
breakpoints. Every breakpoint b found is tested at b-1, b and b+1
max-width: 768px matches at 768, so the mobile/desktop split must be
asserted on the correct side of the boundary. Today that derivation yields
exactly one breakpoint (768; there are currently no sm:/md:/lg: classes
anywhere), and if anyone adds one the harness picks it up and tests it without
being edited. Plus a 320px floor, a 1280px desktop baseline, and two landscape
viewports straddling the boundary for the rotation case.

Assertions (all evaluated, all reported, non-zero exit if any fail): no
document-level horizontal overflow; no clipped content (excluding deliberate
.truncate ellipsis, which is opt-in truncation, not breakage); minimum tap
target 44x44 CSS px on pin buttons, interval select, pause button and debug
toggle; and a genuine reflow assertion on computed layout — mobile must be
flex-direction: column with the sparkline geometrically below the info
block, desktop must be row with it beside. Also asserts probing and gateway
detection still run at mobile widths, since the rejected #8 early-return is the
thing that would silently regress. Screenshots go to tmp/viewport/ as
artifacts.

Harness-can-fail proof: a deliberate wide fixed-width element planted in a
host row, harness observed failing, then reverted. Result reported here.

Wiring: own make target, deliberately out of make check — it needs
Docker and is far past the 20s make test budget. Relation to #21: vitest
will cover module-level logic in-process; this covers rendered layout in a real
engine. Different layers, no overlap.

#38 reconciliation: #38 moves frontend-only gates to a script/frontend-*
namespace, so the entrypoint is named script/frontend-viewport-test up front
to land in that namespace rather than need renaming after.

## Implementation plan **Driver: `puppeteer-core`** (devDependency), not `playwright`. Reason, and the resolution of the bundled-browser problem the DoD flags: `puppeteer-core` is the one variant of either library that never downloads or bundles a browser — it only speaks CDP to a browser you point it at. So the browser stays a digest-pinned container image and the npm side is pinned by `yarn.lock` integrity hashes. `playwright` expects its own version-matched browser download, which would either fight the digest pin or force us onto the ~2 GB official Playwright image. **Browser image:** `chromedp/headless-shell` 151.0.7922.109, referenced by `@sha256:` digest with a version+date comment. **Topology:** `script/frontend-viewport-test` builds `dist/` via the existing build entrypoint, then brings up two `--rm` containers on a private `--internal` docker network: - the repo's own pinned `nginx` digest (same one `Dockerfile` uses) serving `dist/` with the repo's own `nginx.conf` — so the harness runs against the built artifact and the shipping server config, not a dev server; - the headless-shell container, CDP published on an ephemeral `127.0.0.1` port. The network is `--internal`, so the browser physically cannot reach the real internet; on top of that every off-origin request is intercepted and served a deterministic canned response (a fixed delay table, plus a couple of forced failures) so host rows render a realistic mix of numeric latencies, TIMEOUT and offline states rather than 24 identical `---` placeholders. Containers and network are torn down in a trap. **Widths are derived, not hardcoded.** The harness parses `src/styles.css` for `@media` `min-width`/`max-width` values and scans `src/main.js` + `index.html` for Tailwind responsive prefixes, mapping them to Tailwind v4 default breakpoints. Every breakpoint `b` found is tested at `b-1`, `b` and `b+1` — `max-width: 768px` matches *at* 768, so the mobile/desktop split must be asserted on the correct side of the boundary. Today that derivation yields exactly one breakpoint (768; there are currently no `sm:`/`md:`/`lg:` classes anywhere), and if anyone adds one the harness picks it up and tests it without being edited. Plus a 320px floor, a 1280px desktop baseline, and two landscape viewports straddling the boundary for the rotation case. **Assertions** (all evaluated, all reported, non-zero exit if any fail): no document-level horizontal overflow; no clipped content (excluding deliberate `.truncate` ellipsis, which is opt-in truncation, not breakage); minimum tap target 44x44 CSS px on pin buttons, interval select, pause button and debug toggle; and a genuine reflow assertion on computed layout — mobile must be `flex-direction: column` with the sparkline geometrically *below* the info block, desktop must be `row` with it *beside*. Also asserts probing and gateway detection still run at mobile widths, since the rejected #8 early-return is the thing that would silently regress. Screenshots go to `tmp/viewport/` as artifacts. **Harness-can-fail proof:** a deliberate wide fixed-width element planted in a host row, harness observed failing, then reverted. Result reported here. **Wiring:** own `make` target, deliberately out of `make check` — it needs Docker and is far past the 20s `make test` budget. Relation to #21: `vitest` will cover module-level logic in-process; this covers rendered layout in a real engine. Different layers, no overlap. **#38 reconciliation:** #38 moves frontend-only gates to a `script/frontend-*` namespace, so the entrypoint is named `script/frontend-viewport-test` up front to land in that namespace rather than need renaming after.
Author
Collaborator

Done in #44. Harness built, harness proven able to fail (twice — planted
wide element, and the reflow rule neutered), limits documented in
test/viewport/README.md so this does not get re-parked as needing
hardware.

Widths tested, all derived from src/styles.css rather than from phone
models — 320, 667, 767 / 768 / 769 (one below, exactly on, one above the
max-width: 768px boundary), 844, 1280. Rationale, the definition of
"correct" at each, and the driver/pinning decisions are in the PR body.

QA result: two real defects, filed as #42 and #43. Everything else —
reflow, clipping, rendering, probing, gateway detection — passes at every
width. No residual check here requires physical hardware; the genuine gaps
are the WebKit engine, real touch, pixel density and on-device performance,
listed in the harness README.

Done in #44. Harness built, harness proven able to fail (twice — planted wide element, and the reflow rule neutered), limits documented in `test/viewport/README.md` so this does not get re-parked as needing hardware. Widths tested, all derived from `src/styles.css` rather than from phone models — 320, 667, **767 / 768 / 769** (one below, exactly on, one above the `max-width: 768px` boundary), 844, 1280. Rationale, the definition of "correct" at each, and the driver/pinning decisions are in the PR body. **QA result: two real defects, filed as #42 and #43.** Everything else — reflow, clipping, rendering, probing, gateway detection — passes at every width. No residual check here requires physical hardware; the genuine gaps are the WebKit engine, real touch, pixel density and on-device performance, listed in the harness README.
Author
Collaborator

Correction: the overflow assertion I specified would have false-passed

I wrote document.documentElement.scrollWidth <= innerWidth into the definition of done. That check passes on a page that is visibly overflowing.

Under Chrome's mobile emulation window.innerWidth grows to match the overflowing content width, so the comparison becomes 350 <= 350 and reports clean. The implementer's first pass followed my spec literally and declared the layout fine. It only found the real bug (#42) after switching to Math.min(innerWidth, documentElement.clientWidth) with --hide-scrollbars.

Worth recording for two reasons.

First, had the harness-can-fail requirement not been in the DoD, this would have shipped as a fourth mechanism reporting green while measuring nothing — after #14 (inert lint config), #16 (root gate skipping the backend) and #37 (CI served from cache). The requirement caught a defect introduced by the same document that imposed it.

Second, the general lesson: a browser API that sounds like it means "the viewport" may be defined relative to the content instead. Anyone reusing this harness elsewhere should carry Math.min(innerWidth, clientWidth) rather than the obvious form.

## Correction: the overflow assertion I specified would have false-passed I wrote `document.documentElement.scrollWidth &lt;= innerWidth` into the definition of done. That check **passes on a page that is visibly overflowing**. Under Chrome's mobile emulation `window.innerWidth` grows to match the overflowing content width, so the comparison becomes `350 <= 350` and reports clean. The implementer's first pass followed my spec literally and declared the layout fine. It only found the real bug (#42) after switching to `Math.min(innerWidth, documentElement.clientWidth)` with `--hide-scrollbars`. Worth recording for two reasons. First, had the harness-can-fail requirement not been in the DoD, this would have shipped as a fourth mechanism reporting green while measuring nothing — after #14 (inert lint config), #16 (root gate skipping the backend) and #37 (CI served from cache). The requirement caught a defect introduced by the same document that imposed it. Second, the general lesson: a browser API that *sounds* like it means "the viewport" may be defined relative to the content instead. Anyone reusing this harness elsewhere should carry `Math.min(innerWidth, clientWidth)` rather than the obvious form.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/netwatch#13