SPB Git

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>
Simon-Pierre Boucher committed (Nov 2, 2026) parent 5c3b8a5 Browse files

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
Menu

Theme