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 | for (const device of [null, devices["iPhone 15"]]) { | 8 | for (const device of [null, devices["iPhone 15"]]) { | ||
| 9 | const ctx = await browser.newContext(device ? { ...device } : { viewport: { width: 1440, height: 900 } }); | 9 | const ctx = await browser.newContext(device ? { ...device } : { viewport: { width: 1440, height: 900 } }); | ||
| 10 | for (const p of pages) { | 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 | const r = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"]).analyze(); | 12 | const r = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22aa", "best-practice"]).analyze(); | ||
| 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(" ")) })); | 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 | serious += v.filter((x) => x.impact === "serious" || x.impact === "critical").length; | 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 | throw new Error("too many redirects"); | 88 | throw new Error("too many redirects"); | ||
| 89 | } catch (e) { | 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 | ||||