import assert from "node:assert/strict"; import test from "node:test"; import { buildPageSpeedRequestUrl, classifyPageSpeedError, fetchPageSpeedResult, normalizePageSpeedResult, type PageSpeedErrorType, } from "../lib/pagespeed-insights"; const MOBILE_RAW_FIXTURE = { analysisUTCTimestamp: "2026-06-01T08:00:00.000Z", lighthouseResult: { finalUrl: "https://example.com/mobil", categories: { performance: { score: 0.81 }, accessibility: { score: 0.96 }, "best-practices": { score: 0.77 }, seo: { score: 0.94 }, }, audits: { "first-contentful-paint": { title: "Erste Inhalte", numericValue: 2850, score: 0.76, }, "largest-contentful-paint": { title: "Größtes Inhaltselement", numericValue: 4300, score: 0.6, }, "cumulative-layout-shift": { title: "Layout-Verschiebung", numericValue: 0.14, }, "total-blocking-time": { title: "Blockierende Skripte", numericValue: 420, }, "speed-index": { title: "Speed Index", numericValue: 4800, }, "unused-css-rules": { title: "Nicht verwendetes CSS", score: 0.4, details: { type: "opportunity", overallSavingsMs: 380, }, }, "modern-image-formats": { title: "Moderne Bildformate", score: 0.55, details: { type: "opportunity", overallSavingsBytes: 52000, }, }, }, }, }; const DESKTOP_RAW_FIXTURE = { analysisUTCTimestamp: "2026-06-01T09:00:00.000Z", lighthouseResult: { finalUrl: "https://example.com/desktop", categories: { performance: { score: 0.93 }, accessibility: { score: 0.99 }, "best-practices": { score: 0.85 }, seo: { score: 0.97 }, }, audits: { "first-contentful-paint": { title: "Erste Inhalte", numericValue: 1800, score: 0.91, }, "largest-contentful-paint": { title: "Größtes Inhaltselement", numericValue: 3600, score: 0.73, }, "cumulative-layout-shift": { title: "Layout-Verschiebung", numericValue: 0.08, }, "total-blocking-time": { title: "Blockierende Skripte", numericValue: 310, }, "speed-index": { title: "Speed Index", numericValue: 3800, }, "offscreen-images": { title: "Außenseiten-Bilder", score: 0.9, details: { type: "opportunity", overallSavingsMs: 210, }, }, }, }, }; test("buildPageSpeedRequestUrl includes required query params and repeated categories", () => { const url = buildPageSpeedRequestUrl({ url: "https://example.com/landing?x=1&y=2", strategy: "mobile", locale: "de-DE", apiKey: "super-secret", }); const parsed = new URL(url); assert.equal( parsed.origin + parsed.pathname, "https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed", ); assert.equal(parsed.searchParams.get("url"), "https://example.com/landing?x=1&y=2"); assert.equal(parsed.searchParams.get("strategy"), "mobile"); assert.equal(parsed.searchParams.get("locale"), "de-DE"); assert.equal(parsed.searchParams.get("key"), "super-secret"); assert.deepEqual(parsed.searchParams.getAll("category"), [ "performance", "accessibility", "best-practices", "seo", ]); assert.equal( parsed.search.includes("url=https%3A%2F%2Fexample.com%2Flanding%3Fx%3D1%26y%3D2"), true, "URL input should be encoded", ); }); test("buildPageSpeedRequestUrl omits empty API keys", () => { const url = buildPageSpeedRequestUrl({ url: "https://example.com", strategy: "desktop", apiKey: "", locale: "de-DE", }); const parsed = new URL(url); assert.equal(parsed.searchParams.has("key"), false); }); test("normalizePageSpeedResult maps mobile scores, metrics, and implications", () => { const normalized = normalizePageSpeedResult({ strategy: "mobile", sourceUrl: "https://example.com", raw: MOBILE_RAW_FIXTURE, }); assert.equal(normalized.strategy, "mobile"); assert.equal(normalized.sourceUrl, "https://example.com"); assert.equal(normalized.finalUrl, "https://example.com/mobil"); assert.equal(normalized.analysisTimestamp, "2026-06-01T08:00:00.000Z"); assert.equal(normalized.scores?.performance, 0.81); assert.equal(normalized.scores?.accessibility, 0.96); assert.equal(normalized.scores?.bestPractices, 0.77); assert.equal(normalized.scores?.seo, 0.94); assert.equal(normalized.metrics.firstContentfulPaintMs, 2850); assert.equal(normalized.metrics.largestContentfulPaintMs, 4300); assert.equal(normalized.metrics.cumulativeLayoutShift, 0.14); assert.equal(normalized.metrics.totalBlockingTimeMs, 420); assert.equal(normalized.metrics.speedIndexMs, 4800); assert.equal(normalized.opportunities.length >= 1, true); assert.equal(normalized.implications.length >= 2, true); assert.equal(normalized.implications.some((text) => text.includes("Besucher")), true); for (const implication of normalized.implications) { assert.equal( /score\s*\d+/i.test(implication), false, `Implication should not contain raw score text: ${implication}`, ); assert.equal(implication.length > 0, true); } }); test("normalizePageSpeedResult maps desktop scores and metrics", () => { const normalized = normalizePageSpeedResult({ strategy: "desktop", sourceUrl: "https://example.com/landing", raw: DESKTOP_RAW_FIXTURE, }); assert.equal(normalized.strategy, "desktop"); assert.equal(normalized.sourceUrl, "https://example.com/landing"); assert.equal(normalized.finalUrl, "https://example.com/desktop"); assert.equal(normalized.analysisTimestamp, "2026-06-01T09:00:00.000Z"); assert.equal(normalized.scores?.performance, 0.93); assert.equal(normalized.scores?.bestPractices, 0.85); assert.equal(normalized.metrics.firstContentfulPaintMs, 1800); assert.equal(normalized.metrics.speedIndexMs, 3800); assert.equal(normalized.metrics.totalBlockingTimeMs, 310); assert.equal(normalized.opportunities.length >= 1, true); assert.equal(normalized.implications.length >= 2, true); }); test("classifyPageSpeedError maps status and body signals", () => { const quotaByStatus = classifyPageSpeedError({ status: 429 }); assert.equal(quotaByStatus.errorType, "quota"); const quotaByBody = classifyPageSpeedError({ status: 403, body: { error: { errors: [{ reason: "userRateLimitExceeded" }] } }, }); assert.equal(quotaByBody.errorType, "quota"); const timeoutError = classifyPageSpeedError({ error: new DOMException("timed out", "AbortError"), }); assert.equal(timeoutError.errorType, "timeout"); const unavailableByStatus = classifyPageSpeedError({ status: 404 }); assert.equal(unavailableByStatus.errorType, "unavailable"); const unavailableByBody = classifyPageSpeedError({ status: 500, body: { error: { message: "Failed to fetch document from given URL" } }, }); assert.equal(unavailableByBody.errorType, "unavailable"); const invalidUrl = classifyPageSpeedError({ status: 400, body: { error: { message: "Invalid URL: unsupported format" } }, }); assert.equal(invalidUrl.errorType, "invalid_url"); const apiError = classifyPageSpeedError({ status: 500, body: { error: { message: "backend down" } }, }); assert.equal(apiError.errorType, "api_error"); assert.match(apiError.message, /backend down/); }); test("classifyPageSpeedError returns unknown for non-classified cases", () => { const classified = classifyPageSpeedError({ error: new Error("something odd"), }); const errorType: PageSpeedErrorType = classified.errorType; assert.equal(errorType, "unknown"); assert.match(classified.message, /something odd/); }); test("fetchPageSpeedResult uses injected fetch and uses the built request URL", async () => { const calls: string[] = []; const fetchImpl = async (url: string) => { calls.push(url); return { ok: true, status: 200, async json() { return { ok: true }; }, } as Response; }; const actual = await fetchPageSpeedResult({ url: "https://example.com/test?tracking=true", strategy: "desktop", apiKey: "secret-key", fetchImpl, }); assert.deepEqual(actual, { ok: true }); assert.equal(calls.length, 1); const parsed = new URL(calls[0]); assert.equal(parsed.searchParams.get("strategy"), "desktop"); assert.equal(parsed.searchParams.get("locale"), "de-DE"); assert.deepEqual( parsed.searchParams.getAll("category"), ["performance", "accessibility", "best-practices", "seo"], ); assert.equal(parsed.searchParams.get("key"), "secret-key"); }); test( "fetchPageSpeedResult throws classified api_error when response.ok response has invalid JSON", async () => { const fetchImpl = async () => ({ ok: true, status: 200, async json() { throw new SyntaxError("Unexpected token <"); }, }) as unknown as Response; let caughtError: unknown; try { await fetchPageSpeedResult({ url: "https://example.com/broken-json", strategy: "mobile", fetchImpl, }); assert.fail("Expected fetchPageSpeedResult to throw"); } catch (error) { caughtError = error; } assert.match(String((caughtError as Error).message), /Unexpected token { const fetchImpl = async () => ({ ok: false, status: 403, async json() { return { error: { code: 403, message: "API key not valid. Please pass a valid API key.", status: "PERMISSION_DENIED", }, }; }, }) as unknown as Response; let caughtError: unknown; try { await fetchPageSpeedResult({ url: "https://example.com/key-error", strategy: "desktop", fetchImpl, }); assert.fail("Expected fetchPageSpeedResult to throw"); } catch (error) { caughtError = error; } assert.equal((caughtError as Error & { errorType?: string }).errorType, "api_error"); assert.match(String((caughtError as Error).message), /API key not valid/); });