Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions scripts/check-public-live-deploy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
// JSON-LD on only 4 of 12 pages).
// Proof 2b covers the 2026-08-08 dogfood finding page:
// 2b. /contact/ renders H2 after H1 (the heading-hierarchy repair, PR #18).
// 5. the deployed stylesheet keeps the WCAG 2.2 24px footer tap-target
// rule (PR #22) so mobile footer links stay >= 24px on every page.
import { join } from "node:path"
import { fileURLToPath } from "node:url"
import { dirname } from "node:path"
Expand Down Expand Up @@ -151,6 +153,26 @@ try {
const blocks = (body.match(/<script\s+type="application\/ld\+json"[^>]*>/gi) || []).length
ok(blocks === 1, `${path} carries exactly one application/ld+json block (got ${blocks})`)
}

console.log("G. the deployed stylesheet keeps the WCAG 2.2 24px footer tap-target rule (PR #22)")
{
const { status, body } = await get("/styles.css")
ok(status === 200, `GET /styles.css returns 200 (got ${status})`)
const footerRule = body.match(/\.footer-links\s*a\s*\{([^}]*)\}/)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject ancestor-scoped footer rules

If the deployed stylesheet scopes this declaration beneath another selector, such as .homepage .footer-links a { ... }, the unanchored regex starts matching at .footer-links and every assertion passes. Footer links on pages outside that ancestor remain undersized even though this acceptance proof claims coverage on every page; validate the complete selector list and require an unscoped .footer-links a selector.

Useful? React with 👍 / 👎.

ok(footerRule !== null, "deployed stylesheet has a .footer-links a rule")
if (footerRule) {
const rule = footerRule[1]
ok(/display:\s*(inline-block|inline-flex|block)/.test(rule), ".footer-links a is a block-level box (hit area covers the line box)")
ok(!/display:\s*inline\s*;/.test(rule), ".footer-links a is not a plain inline box")
ok(/min-height:\s*24px/.test(rule), ".footer-links a declares min-height: 24px")
const padding = rule.match(/padding:\s*([^;]+)/)
ok(padding !== null, ".footer-links a declares vertical padding")
if (padding) {
const vertical = parseFloat(padding[1].trim().split(/\s+/)[0])
ok(vertical >= 4, `.footer-links a vertical padding is at least 4px (${vertical}px), so 16px text + padding >= 24px`)
Comment on lines +161 to +172

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Use one cascade-aware validator for both live checks. Both implementations inspect only the first matching text block, so later declarations or conditional rules can invalidate the effective footer styles without failing the checks.

  • scripts/check-public-live-deploy.mjs#L100-L111: replace the first-match regex validation with cascade-aware CSS validation.
  • scripts/check-public-live-soft-404.mjs#L113-L124: reuse the same validator and evaluate the effective deployed rule.
📍 Affects 2 files
  • scripts/check-public-live-deploy.mjs#L100-L111 (this comment)
  • scripts/check-public-live-soft-404.mjs#L113-L124
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/check-public-live-deploy.mjs` around lines 100 - 111, Replace the
first-match footerRule checks in the live validation flow of
scripts/check-public-live-deploy.mjs (lines 100-111) with a shared cascade-aware
CSS validator that computes the effective .footer-links a styles across later
declarations and conditional rules. Update
scripts/check-public-live-soft-404.mjs (lines 113-124) to reuse this same
validator and evaluate the effective deployed rule, preserving all existing
display, min-height, and padding requirements.

}
}
}
} catch (error) {
failures++
console.error(` FAIL live request error: ${error.message}`)
Expand Down
39 changes: 31 additions & 8 deletions scripts/check-public-live-soft-404.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
// Guard the LIVE public site against soft-404s: an unknown URL on
// tinystudio.in must return HTTP 404 with the real 404 page, never HTTP 200
// with the homepage.
// Guard the LIVE public site against soft-404s and a stale a11y bundle: an
// unknown URL on tinystudio.in must return HTTP 404 with the real 404 page,
// never HTTP 200 with the homepage, and the deployed stylesheet must keep the
// WCAG 2.2 24px footer tap-target rule (PR #22) so mobile footer links are
// tappable on every live page.
//
// The static test (test-public-soft-404.mjs) only proves public/404.html
// exists in the repo; it cannot catch a stale or misconfigured deployment.
// This check hits the deployed site so a regression is detected the moment it
// ships. It runs as `npm run site:check-live`, from the nightly
// The static tests (test-public-soft-404.mjs, test-public-link-targets.mjs)
// only prove the repo state; they cannot catch a stale or misconfigured
// deployment. This check hits the deployed site so a regression is detected
// the moment it ships. It runs as `npm run site:check-live`, from the nightly
// live-site-check workflow, and as part of the deploy lane's post-deploy
// verification.
//
Expand Down Expand Up @@ -73,6 +75,7 @@ try {
unknown: await fetchWithRetry(`${SITE}${UNKNOWN_PATH}`),
notFoundAsset: await fetchWithRetry(`${SITE}/404.html`),
realPage: await fetchWithRetry(`${SITE}/promptly/`),
css: await fetchWithRetry(`${SITE}/styles.css`),
}
} catch (err) {
console.error(` FAIL could not reach ${SITE}: ${err.message}`)
Expand All @@ -84,6 +87,7 @@ const { res: homeRes, body: homeBody } = results.home
const { res: unknownRes, body: unknownBody } = results.unknown
const { res: notFoundRes, body: notFoundBody } = results.notFoundAsset
const { res: realRes } = results.realPage
const { res: cssRes, body: cssBody } = results.css

console.log("A. the homepage is reachable and intact")
ok(homeRes.status === 200, `GET / returns HTTP ${homeRes.status}`)
Expand All @@ -102,8 +106,27 @@ ok(!notFoundBody.includes(HOME_TITLE), "/404.html body is not the homepage")
console.log("D. a real page still serves")
ok(realRes.status === 200, `GET /promptly/ returns HTTP ${realRes.status}`)

// Mirrors section B of test-public-link-targets.mjs against the DEPLOYED
// stylesheet, so a stale bundle that loses the PR #22 rule fails loudly.
console.log("E. the deployed stylesheet keeps the WCAG 2.2 24px footer tap-target rule (PR #22)")
ok(cssRes.status === 200, `GET /styles.css returns HTTP ${cssRes.status}`)
const footerRule = cssBody.match(/\.footer-links\s*a\s*\{([^}]*)\}/)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Inspect all applicable footer rules

Both new live checks use match(), so they validate only the first .footer-links a declaration. If a later rule—especially a mobile @media override—sets display: inline, min-height: 0, or removes the padding, the CSS cascade makes the deployed mobile tap target undersized while every assertion here and in check-public-live-deploy.mjs still passes. Check the effective/final declarations or reject conflicting later rules.

Useful? React with 👍 / 👎.

ok(footerRule !== null, "deployed stylesheet has a .footer-links a rule")
if (footerRule) {
const rule = footerRule[1]
ok(/display:\s*(inline-block|inline-flex|block)/.test(rule), ".footer-links a is a block-level box (hit area covers the line box)")
ok(!/display:\s*inline\s*;/.test(rule), ".footer-links a is not a plain inline box")
ok(/min-height:\s*24px/.test(rule), ".footer-links a declares min-height: 24px")
Comment on lines +116 to +119

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Strip CSS comments before validating declarations

If the footer declarations are commented out—for example, /* display: inline-block; min-height: 24px; padding: 4px 0; */—all of these positive checks still match the comment while the negative inline check also passes, even though the browser applies none of the declarations and the link remains inline. Strip comments or parse the declaration block as CSS before running these assertions; the duplicated deploy check has the same issue.

Useful? React with 👍 / 👎.

const padding = rule.match(/padding:\s*([^;]+)/)
ok(padding !== null, ".footer-links a declares vertical padding")
if (padding) {
const vertical = parseFloat(padding[1].trim().split(/\s+/)[0])
Comment on lines +119 to +123

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require exact CSS units before passing the live check

If a deployed rule contains malformed values such as min-height: 24pxfoo; padding: 4, the browser discards both declarations, but the prefix regex and parseFloat still accept them. When the inherited line height is below 24px, both this live check and its duplicated deploy/static logic therefore report success while the effective tap target remains undersized; validate complete CSS tokens and require valid pixel units.

Useful? React with 👍 / 👎.

ok(vertical >= 4, `.footer-links a vertical padding is at least 4px (${vertical}px), so 16px text + padding >= 24px`)
}
}

console.log(`\n${checks} checks, ${failures} failures`)
if (failures > 0) {
console.error("\nThe live site is soft-404ing or serving a stale bundle. Re-deploy the public site from origin/main and re-run this check.")
console.error("\nThe live site is soft-404ing, serving a stale bundle, or missing the footer tap-target rule. Re-deploy the public site from origin/main and re-run this check.")
}
process.exit(failures === 0 ? 0 : 1)