From d9415e8684b07d3647901174549e1a4830aa051a Mon Sep 17 00:00:00 2001 From: Philippos Savvides Date: Sat, 29 Aug 2026 19:37:01 -0700 Subject: [PATCH 1/4] fix: repair the landing-page guard and the mutation harness that hid its holes (v3.5.1.0) The mutation suite could not fail. fresh() copies the repo per mutation and omitted extension/, so test-extension.sh failed on every copy before any mutation was applied; since expect_fail only checks for a non-zero exit, all 36 mutations reported GUARDED regardless of whether their guard worked. Adds extension/ to the copy list and a null-mutation control that aborts the suite when an unmutated copy is already red. Fixes three defects on idstack.org, all phone-only: - .section had no safe-area inset, so every content region sat under the notch once viewport-fit=cover landed. The four containers now share one rule. - Three ad-hoc touch floors (36/40/42px), with the footer controls applying theirs only below 480px. One --tap-min token, 44px, at every width. - overflow-x: clip on html/body hid horizontal overflow instead of preventing it. Removed; the grid tracks that actually contain long install commands size with minmax(0, ...). Measured clean at 11 widths from 320px. Adds test/test-rendered-landing.js, which renders the page in headless Chrome and asserts the outcome rather than the CSS text. Six kinds of edit shipped a broken page past the text suite (a selector list, :is(), @container, a print-only media query, a media query nested in a desktop one, and a " +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, " @media (max-width: 480px) { :root { --tap-min: 30px; } }\n" + old, 1) +open(p, 'w').write(s) +PY +expect_fail "a later override shrinks the 44px touch target" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 25. The viewport clip returns. It hides horizontal overflow instead of +# preventing it, so a regression becomes unreachable content rather than a +# visible bug -- measured in Chrome as 305px of the install command unreachable +# when an element does overflow. +fresh +python3 - "$WORK/r/docs/index.html" <<'PY' +import sys +p = sys.argv[1]; s = open(p).read() +old = " html { scroll-behavior: smooth; }" +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, " html { scroll-behavior: smooth; overflow-x: clip; }", 1) +open(p, 'w').write(s) +PY +expect_fail "root overflow-x clip masks page overflow" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 26. The 44px floor is hoisted out of the unconditional cascade into a +# min-width query, so phones get no floor at all while the token and the +# declaration both still exist. This is the original scoping bug in mirror +# image, and an assertion that scans the whole sheet cannot see it. +fresh +python3 - "$WORK/r/docs/index.html" <<'PY' +import sys +p = sys.argv[1]; s = open(p).read() +old = " min-height: var(--tap-min);\n color: var(--ink-soft);" +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, " color: var(--ink-soft);", 1) +s = s.replace(" ", + " @media (min-width: 900px) { .copy-btn { min-height: var(--tap-min); } }\n ", 1) +open(p, 'w').write(s) +PY +expect_fail "touch-target floor hoisted into a desktop-only query" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 27. A second \n", 1) +open(p, 'w').write(s) +PY +expect_fail "a second " +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, " .copy-btn { min-height: 30px; }\n" + old, 1) +open(p, 'w').write(s) +PY +expect_fail "a later base rule outranks the touch-target token" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 30. The viewport clip returns as the `overflow` shorthand rather than the +# `overflow-x` longhand. Same rendered effect, different spelling. +fresh +python3 - "$WORK/r/docs/index.html" <<'PY' +import sys +p = sys.argv[1]; s = open(p).read() +old = " html { scroll-behavior: smooth; }" +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, " html { scroll-behavior: smooth; overflow: clip; }", 1) +open(p, 'w').write(s) +PY +expect_fail "root overflow shorthand masks page overflow" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 31. The mobile breakpoint stops collapsing to one column. `1fr 1fr` still +# contains `1fr`, so an unanchored match treats the two-column regression as +# satisfying the single-column assertion. +fresh +python3 - "$WORK/r/docs/index.html" <<'PY' +import sys +p = sys.argv[1]; s = open(p).read() +old = " .pipeline-flow { grid-template-columns: 1fr; }" +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, " .pipeline-flow { grid-template-columns: 1fr 1fr; }", 1) +open(p, 'w').write(s) +PY +expect_fail "pipeline stays two-column at the mobile breakpoint" "$WORK/r/test/smoke-test.sh" "$WORK/r" + +# 32. A selector LIST shrinks the touch target below 480px. The text suite anchors every +# touch-target guard on the single-selector spelling `.copy-btn {`, so `.copy-btn, .btn-badge {` +# is invisible to all three of its checks at once and it exits 0. Only the rendered suite +# catches this, by measuring the button at 375px. This mutation is the reason +# test-rendered-landing.js exists: it forbids the outcome, not the spelling. +fresh +python3 - "$WORK/r/docs/index.html" <<'PY' +import sys +p = sys.argv[1]; s = open(p).read() +old = " " +assert s.count(old) == 1, 'anchor not unique: %d' % s.count(old) +s = s.replace(old, " @media (max-width: 480px) { .copy-btn, .btn-badge { min-height: 30px; } }\n" + old, 1) +open(p, 'w').write(s) +PY +expect_fail "a selector list shrinks the touch target past the text guard" "$WORK/r/test/smoke-test.sh" "$WORK/r" + echo "" echo "guarded: $pass NOT guarded: $fail skipped: $skip" [ "$fail" -eq 0 ] diff --git a/test/smoke-test.sh b/test/smoke-test.sh index 490cb9f..1caa0ca 100755 --- a/test/smoke-test.sh +++ b/test/smoke-test.sh @@ -312,14 +312,20 @@ if [ -x "$IDSTACK_DIR/test/test-setup.sh" ]; then check "setup behavioral tests pass" "'$IDSTACK_DIR/test/test-setup.sh' '$IDSTACK_DIR'" fi -# Chrome extension unit and integration tests -if [ -x "$IDSTACK_DIR/test/test-extension.sh" ]; then +# The two node-dependent suites. Guarded on node, not on the files' exec bits — those are tracked +# at 100755 so they never vary, while node is the part that can actually be absent. CI installs it +# (see .github/workflows/test.yml); the guard is for a bare machine. The SKIP lines matter: without +# them a node-less run just reports a smaller total than CLAUDE.md documents, with nothing saying why. +if command -v node &>/dev/null; then check "chrome extension tests pass" "'$IDSTACK_DIR/test/test-extension.sh'" -fi - -# Responsive landing page test -if [ -x "$IDSTACK_DIR/test/test-responsive-landing.js" ]; then check "responsive landing page tests pass" "node '$IDSTACK_DIR/test/test-responsive-landing.js'" + # Renders the page in headless Chrome and asserts the OUTCOME (no sideways scroll, touch + # targets, column counts) rather than the CSS text. Skips loudly without a browser; the + # text suite above still runs. See test/test-rendered-landing.js for why both exist. + check "rendered landing page tests pass" "node '$IDSTACK_DIR/test/test-rendered-landing.js'" +else + echo " SKIP: chrome extension tests (node not installed)" + echo " SKIP: responsive landing page tests (node not installed)" fi # Check generated files have auto-generated header diff --git a/test/test-rendered-landing.js b/test/test-rendered-landing.js new file mode 100755 index 0000000..26910a4 --- /dev/null +++ b/test/test-rendered-landing.js @@ -0,0 +1,216 @@ +#!/usr/bin/env node +/** + * test-rendered-landing.js + * Renders docs/index.html in headless Chrome and asserts what the page actually DOES, not what + * its stylesheet says. + * + * test-responsive-landing.js scans CSS as text. That catches a rule being deleted, but six review + * rounds found ways to ship a broken page past it — a selector list, an @container wrapper, an + * @import, a