link checker: record the underlying network error code; a11y audit waits for DOM instead of network idle; live screenshot helper
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3 changed files +8 −2
modified
scripts/a11y-audit.ts
+1 −1
@@ -8,7 +8,7 @@ const browser = await chromium.launch(); const results: any[] = []; let serious | ||
| 8 | 8 | for (const device of [null, devices["iPhone 15"]]) { |
| 9 | 9 | const ctx = await browser.newContext(device ? { ...device } : { viewport: { width: 1440, height: 900 } }); |
| 10 | 10 | for (const p of pages) { |
| 11 | − const page = await ctx.newPage(); await page.goto(base + p, { waitUntil: "networkidle" }); | |
| 11 | + const page = await ctx.newPage(); await page.goto(base + p, { waitUntil: "domcontentloaded", timeout: 45_000 }); await page.waitForTimeout(800); | |
| 12 | 12 | const r = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"]).analyze(); |
| 13 | 13 | const v = r.violations.map((x) => ({ id: x.id, impact: x.impact, help: x.help, nodes: x.nodes.length, targets: x.nodes.slice(0, 3).map((n) => n.target.join(" ")) })); |
| 14 | 14 | serious += v.filter((x) => x.impact === "serious" || x.impact === "critical").length; |
added
scripts/dev/prod-shots.mjs
+4 −0
@@ -0,0 +1,4 @@ | ||
| 1 | +import { chromium, devices } from "@playwright/test"; | |
| 2 | +const b = await chromium.launch(); const base = "https://tools.spboucher.ai"; | |
| 3 | +const d = await b.newContext({ viewport: { width: 1440, height: 1000 } }); const p = await d.newPage(); await p.goto(base + "/", { waitUntil: "networkidle" }); await p.screenshot({ path: "/tmp/prod-home.png" }); await p.goto(base + "/tools?q=econometrics", { waitUntil: "networkidle" }); await p.screenshot({ path: "/tmp/prod-dir.png" }); await d.close(); | |
| 4 | +const m = await b.newContext({ ...devices["iPhone 15"], browserName: "chromium" }); const mp = await m.newPage(); await mp.emulateMedia({ colorScheme: "dark" }); await mp.goto(base + "/tools/claude-code", { waitUntil: "networkidle" }); await mp.screenshot({ path: "/tmp/prod-mobile-dark.png" }); await mp.goto(base + "/tools?q=sec", { waitUntil: "networkidle" }); await mp.screenshot({ path: "/tmp/prod-mobile-dir.png" }); await m.close(); await b.close(); console.log("shots ok"); | |
modified
src/lib/url.ts
+3 −1
@@ -87,7 +87,9 @@ export async function safeFetch(url: string, opts: { method?: "GET" | "HEAD"; ma | ||
| 87 | 87 | } |
| 88 | 88 | throw new Error("too many redirects"); |
| 89 | 89 | } catch (e) { |
| 90 | − return { ok: false, status: 0, finalUrl: current, redirected, hops, contentType: null, body: null, error: (e as Error).name === "AbortError" ? "timeout" : (e as Error).message, durationMs: Date.now() - started }; | |
| 90 | + const err = e as Error & { cause?: { code?: string; message?: string; errors?: { code?: string }[] } }; | |
| 91 | + const cause = err.cause ? (err.cause.code ?? err.cause.errors?.map((x) => x.code).join("|") ?? err.cause.message) : null; | |
| 92 | + return { ok: false, status: 0, finalUrl: current, redirected, hops, contentType: null, body: null, error: err.name === "AbortError" ? "timeout" : cause ? `${err.message} (${String(cause).slice(0, 80)})` : err.message, durationMs: Date.now() - started }; | |
| 91 | 93 | } |
| 92 | 94 | } |
| 93 | 95 | |
| 94 | 96 | |