Tap-target check expects a pin button per WAN host row (closes #46) #96

Merged
clawbot merged 1 commits from issue-46-pin-btn-floor into next 2026-10-04 01:53:03 +02:00
4 changed files with 32 additions and 21 deletions
+10 -3
View File
@@ -23,6 +23,11 @@ latest run passes.
# Completed Steps # Completed Steps
- 2026-10-03: the tap-target check in `make frontend-viewport-test` expects one
visible pin button per WAN host row (issue #46), where it expected at least 10
of the 26, so pin buttons missing from only some rows now fail it. The host
row count the harness gathers, which the `app-rendered` check also reads, now
counts only the WAN host rows: the local host rows have no pin button
- 2026-10-03: each target's row shows its result as soon as its check ends - 2026-10-03: each target's row shows its result as soon as its check ends
(issue #91), where every row waited for the round's slowest check, up to 24 (issue #91), where every row waited for the round's slowest check, up to 24
seconds at a 30-second interval. Every row is still redrawn, and sorting, the seconds at a 30-second interval. Every row is still redrawn, and sorting, the
@@ -237,9 +242,11 @@ latest run passes.
- 2026-08-09: automated responsive-layout harness - 2026-08-09: automated responsive-layout harness
(`make frontend-viewport-test`): digest-pinned headless Chrome driven over CDP (`make frontend-viewport-test`): digest-pinned headless Chrome driven over CDP
against the built `dist/`, viewport widths derived from the breakpoints in against the built `dist/`, viewport widths derived from the breakpoints in
`src/styles.css` ([#13](https://git.eeqj.de/sneak/netwatch/issues/13)). Every `src/styles.css` ([#13](https://git.eeqj.de/sneak/netwatch/issues/13)). The
check carries a presence guard so none of them can pass against a page it is tap-target and host-row checks each fail when they measured nothing; the
not actually measuring. Found two real layout defects, filed as overflow, viewport-edge and clipped-text checks have no such guard of their
own and rely on the `app-rendered` check, which fails the run when the app did
not render. Found two real layout defects, filed as
[#42](https://git.eeqj.de/sneak/netwatch/issues/42) and [#42](https://git.eeqj.de/sneak/netwatch/issues/42) and
[#43](https://git.eeqj.de/sneak/netwatch/issues/43) [#43](https://git.eeqj.de/sneak/netwatch/issues/43)
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile - 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile
+5 -4
View File
@@ -49,10 +49,11 @@ sizes straddling the breakpoint for the rotation case.
excluded: it is a design choice, not breakage. excluded: it is a design choice, not breakage.
- **tap-targets-44px** — every interactive control is at least 44x44 CSS px on - **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 touch viewports, _and_ each selector in the control list matched at least the
number of visible elements it declares. The second half is what stops the number of visible elements it declares: one of each single control, and one
check passing vacuously: with size alone, a renamed class would take its pin button per WAN host row. The second half is what stops the check passing
controls out of the measured set and the check would report "all 0 controls vacuously: with size alone, a renamed class would take its controls out of the
are at least 44x44" and pass. See below. 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. - **host-rows-stacked / host-rows-side-by-side** — the rows genuinely reflow.
Computed `flex-direction` _and_ the actual geometry are checked, and in the Computed `flex-direction` _and_ the actual geometry are checked, and in the
narrow layout the info block and the sparkline must each occupy essentially narrow layout the info block and the sparkline must each occupy essentially
+14 -13
View File
@@ -15,7 +15,8 @@ export const MIN_TAP_TARGET_PX = 44;
// The controls named in the definition of done, plus the pause button. // The controls named in the definition of done, plus the pause button.
// Each carries the smallest number of *visible* instances the page has to // Each carries the smallest number of *visible* instances the page has to
// contain for the tap-target oracle to be measuring anything at all. // contain, worked out from the facts gathered from that page, for the
// tap-target oracle to be measuring every control it should.
// //
// Without those floors the check is inert: `undersized` is empty both when // Without those floors the check is inert: `undersized` is empty both when
// every control is large enough and when the selectors have gone stale and // every control is large enough and when the selectors have gone stale and
@@ -24,13 +25,12 @@ export const MIN_TAP_TARGET_PX = 44;
// for all three singleton controls vanishing at once — so the floor is per // for all three singleton controls vanishing at once — so the floor is per
// selector, and one stale selector out of four fails the check. // selector, and one stale selector out of four fails the check.
export const INTERACTIVE_CONTROLS = [ export const INTERACTIVE_CONTROLS = [
{ selector: "#pause-btn", minCount: 1 }, { selector: "#pause-btn", minCount: () => 1 },
{ selector: "#interval-select", minCount: 1 }, { selector: "#interval-select", minCount: () => 1 },
// One per pinnable host row. `app-rendered` already requires at least // One per WAN host row, so pin buttons missing from even one row fail
// 10 host rows, so a count below that means the pin buttons stopped // the check rather than only a drop below some fixed number.
// being rendered per row rather than that there were fewer hosts. { selector: ".pin-btn", minCount: (facts) => facts.wanRowCount },
{ selector: ".pin-btn", minCount: 10 }, { selector: "#debug-toggle", minCount: () => 1 },
{ selector: "#debug-toggle", minCount: 1 },
]; ];
export const INTERACTIVE_SELECTORS = INTERACTIVE_CONTROLS.map( export const INTERACTIVE_SELECTORS = INTERACTIVE_CONTROLS.map(
@@ -105,8 +105,8 @@ export function evaluateChecks(facts, viewport, probes) {
// never rendered. Everything below is only meaningful if this holds. // never rendered. Everything below is only meaningful if this holds.
check( check(
"app-rendered", "app-rendered",
facts.rowCount >= 10 && facts.numericLatencies >= 5, facts.wanRowCount >= 10 && facts.numericLatencies >= 5,
`${facts.rowCount} host rows, ${facts.numericLatencies} showing a numeric latency`, `${facts.wanRowCount} WAN host rows, ${facts.numericLatencies} showing a numeric latency`,
); );
const viewportWidth = Math.min(facts.innerWidth, facts.documentClientWidth); const viewportWidth = Math.min(facts.innerWidth, facts.documentClientWidth);
@@ -162,7 +162,8 @@ export function evaluateChecks(facts, viewport, probes) {
seen.set(target.selector, (seen.get(target.selector) ?? 0) + 1); seen.set(target.selector, (seen.get(target.selector) ?? 0) + 1);
} }
const missing = INTERACTIVE_CONTROLS.filter( const missing = INTERACTIVE_CONTROLS.filter(
(control) => (seen.get(control.selector) ?? 0) < control.minCount, (control) =>
(seen.get(control.selector) ?? 0) < control.minCount(facts),
); );
const undersized = facts.tapTargets.filter( const undersized = facts.tapTargets.filter(
@@ -184,11 +185,11 @@ export function evaluateChecks(facts, viewport, probes) {
const detail = []; const detail = [];
if (missing.length > 0) { if (missing.length > 0) {
detail.push( detail.push(
"oracle is not measuring the page: " + "oracle is not measuring every control: " +
summarise( summarise(
missing, missing,
(c) => (c) =>
`${c.selector} matched ${seen.get(c.selector) ?? 0} visible element(s), expected at least ${c.minCount}`, `${c.selector} matched ${seen.get(c.selector) ?? 0} visible element(s), expected at least ${c.minCount(facts)}`,
4, 4,
), ),
); );
+3 -1
View File
@@ -173,7 +173,9 @@ export function collectLayoutFacts(options) {
clipped, clipped,
tapTargets, tapTargets,
rows, rows,
rowCount: document.querySelectorAll(".host-row").length, // WAN host rows only: each has a pin button, and the tap-target
// check expects one per row. The local host rows have none.
wanRowCount: document.querySelectorAll("#wan-hosts .host-row").length,
numericLatencies: Array.from( numericLatencies: Array.from(
document.querySelectorAll(".latency-value"), document.querySelectorAll(".latency-value"),
).filter((el) => /\d/.test(el.textContent)).length, ).filter((el) => /\d/.test(el.textContent)).length,