-
Notifications
You must be signed in to change notification settings - Fork 0
fix(public): net the live site against WCAG 2.2 tap-target drift without blocking PRs #107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all 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