Repository navigation
fix(public): net the live site against WCAG 2.2 tap-target drift without blocking PRs #107
Changes from 5 commits
8090425
4d7b5b3
4123145
9aa4d45
ea5a636
e3a187b
228ce4a
5120396
552c25a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| // Guard the LIVE public site against WCAG 2.2 (SC 2.5.8) tap-target drift: | ||
| // the stylesheet tinystudio.in actually serves must keep every in-content | ||
| // and footer link rule at the 24px minimum. | ||
| // | ||
| // The static test (test-public-link-targets.mjs) only proves | ||
| // public/styles.css in the repo; it cannot catch a stale or misconfigured | ||
| // deployment. This check fetches the deployed stylesheet and re-asserts the | ||
| // same five link rules the local suite requires (.top-nav a, .plain-list a, | ||
| // .product-links a, .rail-item strong a, .footer-links a: block-level box, | ||
| // min-height 24px, >= 4px vertical padding), so a deployment that still | ||
| // serves pre-fix CSS (the June-20 bundle) fails loudly instead of silently | ||
| // re-opening the tap-target backlog item. It runs as part of | ||
| // `npm run site:check-live` and from the nightly live-site-check workflow. | ||
| // | ||
| // It is deliberately NOT part of `npm run test` / `npm run ci`: those | ||
| // blocking chains must stay green on repo state alone (see PR #84), while | ||
| // the live site is deployed by an external mechanism (Cloudflare Pages). | ||
| // Blocking CI on the live site would keep every pull request red whenever | ||
| // the deployment is stale. | ||
| // | ||
| // Failure semantics: a non-2xx HTTP response and a stylesheet that misses | ||
| // any required rule both FAIL this check (a deployment serving no rules is | ||
| // a failed deployment, not an unknown). Only a genuine network-level | ||
| // failure (site unreachable) skips with a notice, so offline machines do | ||
| // not go red; the nightly live-site-check workflow still catches the | ||
| // same outage via check-public-live-soft-404.mjs. | ||
| // | ||
| // Escape hatch for machines without network access: | ||
| // SKIP_LIVE_CHECKS=1 npm run site:check-live | ||
|
|
||
| import { readFileSync } from "node:fs" | ||
| import { fileURLToPath } from "node:url" | ||
| import { dirname, join } from "node:path" | ||
|
|
||
| if (process.env.SKIP_LIVE_CHECKS === "1") { | ||
| console.log("check-public-live-tap-targets: SKIP_LIVE_CHECKS=1, skipping live site checks") | ||
| process.exit(0) | ||
| } | ||
|
|
||
| const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..") | ||
| const read = (p) => readFileSync(join(ROOT, p), "utf8") | ||
|
|
||
| const LIVE_CSS_URL = "https://tinystudio.in/styles.css" | ||
| const FETCH_TIMEOUT_MS = 10_000 | ||
| const FETCH_ATTEMPTS = 2 | ||
|
|
||
| let failures = 0 | ||
| let checks = 0 | ||
| const ok = (cond, msg) => { | ||
| checks++ | ||
| if (cond) console.log(` ok ${msg}`) | ||
| else { | ||
| failures++ | ||
| console.error(` FAIL ${msg}`) | ||
| } | ||
| } | ||
|
|
||
| // Same selectors test-public-link-targets.mjs requires of the local sheet. | ||
| const TAP_TARGET_SELECTORS = [ | ||
| ".top-nav a", | ||
| ".plain-list a", | ||
| ".product-links a", | ||
| ".rail-item strong a", | ||
| ".footer-links a" | ||
| ] | ||
|
|
||
| const targetRuleOf = (css, selector) => { | ||
| const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") | ||
| const pattern = selector | ||
| .split(",") | ||
| .map((s) => esc(s.trim())) | ||
| .join("\\s*,\\s*") | ||
| const match = css.match(new RegExp(`${pattern}\\s*\\{([^}]*)\\}`)) | ||
| return match ? match[1] : null | ||
|
Comment on lines
+73
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a later or more-specific rule overrides one of these selectors—for example, a mobile media query sets Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| const assert24pxRule = (selector, rule) => { | ||
| ok(rule !== null, `live styles.css has a ${selector} rule`) | ||
| if (rule) { | ||
| ok(/display:\s*(inline-block|inline-flex|block)/.test(rule), `live ${selector} links are block-level boxes (hit area covers the line box)`) | ||
| ok(!/display:\s*inline\s*;/.test(rule), `live ${selector} links are not plain inline boxes`) | ||
| ok(/min-height:\s*24px/.test(rule), `live ${selector} links declare min-height: 24px`) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a width assertion as well as Useful? React with 👍 / 👎. |
||
| const paddingMatch = rule.match(/padding:\s*([^;]+)/) | ||
| ok(paddingMatch !== null, `live ${selector} links declare vertical padding`) | ||
| if (paddingMatch) { | ||
| const vertical = parseFloat(paddingMatch[1].trim().split(/\s+/)[0]) | ||
| ok(vertical >= 4, `live ${selector} vertical padding is at least 4px (${vertical}px), so 16px text + padding >= 24px`) | ||
|
Comment on lines
+67
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Evaluate CSS rules after selector grouping and source-order overrides. Line 73 only matches a selector directly before Parse stylesheet rules, collect every matching selector, and evaluate the final effective declarations. Add cases for grouped selectors and later overrides. 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
|
|
||
| console.log("check-public-live-tap-targets: the deployed tinystudio.in stylesheet keeps every link rule at the WCAG 2.2 24px minimum") | ||
|
|
||
| let css = null | ||
| try { | ||
| for (let attempt = 1; attempt <= FETCH_ATTEMPTS; attempt++) { | ||
| try { | ||
| const res = await fetch(LIVE_CSS_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }) | ||
| if (!res.ok) { | ||
| failures++ | ||
| checks++ | ||
| console.error(` FAIL ${LIVE_CSS_URL} answered HTTP ${res.status}: the deployed stylesheet is missing or inaccessible, so none of the tap-target rules are being served`) | ||
| process.exit(1) | ||
| } | ||
| css = await res.text() | ||
| break | ||
| } catch (err) { | ||
| if (attempt === FETCH_ATTEMPTS) throw err | ||
| await new Promise((r) => setTimeout(r, 1000)) | ||
| } | ||
| } | ||
| } catch (err) { | ||
| console.log(` ok skipped: ${LIVE_CSS_URL} unreachable (${err?.cause?.code ?? err?.name ?? "network error"}) - no tap-target assertions run`) | ||
| } | ||
|
Comment on lines
+112
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
|
|
||
| if (css !== null) { | ||
| console.log("A. every link rule in the deployed stylesheet enforces the 24px minimum") | ||
| for (const selector of TAP_TARGET_SELECTORS) { | ||
| assert24pxRule(selector, targetRuleOf(css, selector)) | ||
| } | ||
| if (failures === 0) { | ||
| console.log(" the deployed stylesheet carries every tap-target rule from public/styles.css") | ||
| } else { | ||
| console.log(" the deployed stylesheet is stale: it misses tap-target rules that public/styles.css already has (see FAIL lines above). Refresh the live deployment from origin/main.") | ||
| } | ||
| } | ||
|
|
||
| console.log("B. wiring") | ||
| const pkg = JSON.parse(read("package.json")) | ||
| ok( | ||
| (pkg.scripts["site:check-live"] ?? "").includes("check-public-live-soft-404.mjs") && | ||
| (pkg.scripts["site:check-live"] ?? "").includes("check-public-live-tap-targets.mjs"), | ||
| "npm run site:check-live runs the soft-404 and tap-target live checks" | ||
| ) | ||
| const workflow = read(".github/workflows/live-site-check.yml") | ||
| ok(workflow.includes("check-public-live-tap-targets.mjs"), "live-site-check.yml runs the live tap-target check (nightly + manual dispatch)") | ||
|
|
||
| console.log(`\n${checks} checks, ${failures} failures`) | ||
| if (failures > 0) { | ||
| console.error("\nThe deployed stylesheet drifted below the WCAG 2.2 24px tap-target minimum. Re-deploy the public site from origin/main and re-run this check.") | ||
| } | ||
| process.exit(failures === 0 ? 0 : 1) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: nish3451/tinystudio-in
Length of output: 796
🏁 Script executed:
Repository: nish3451/tinystudio-in
Length of output: 4485
🏁 Script executed:
Repository: nish3451/tinystudio-in
Length of output: 3367
Disable checkout credential persistence.
This self-hosted job performs no Git operations after checkout. Set
persist-credentials: falseso theGITHUB_TOKENis not retained in.git/configduring later steps.🧰 Tools
🪛 zizmor (1.29.0)
[warning] 43-43: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 Prompt for AI Agents
Source: Linters/SAST tools