import assert from "node:assert/strict"; import test from "node:test"; import { assertNoPublicPageSpeedScores, buildPageSpeedAuditInputs, type PageSpeedMinimalAuditResult, } from "../lib/pagespeed-audit-input"; const MOBILE_AND_DESKTOP_FIXTURES: PageSpeedMinimalAuditResult[] = [ { strategy: "mobile", status: "succeeded", sourceUrl: "https://example.com", normalized: { metrics: { firstContentfulPaintMs: 3200, largestContentfulPaintMs: 5000, cumulativeLayoutShift: 0.2, }, implications: [ "Score 0.42: Der erste sichtbare Inhalt erscheint zu langsam.", "Die Seite zeigt das Hauptbild zu langsam.", "Die Inhalte verschieben sich beim Laden.", ], opportunities: [ "Nicht verwendetes CSS kann entfernt werden.", "Bilder ohne passende Komprimierung koennen verzichtet werden.", ], }, }, { strategy: "desktop", status: "succeeded", sourceUrl: "https://example.com", normalized: { metrics: { firstContentfulPaintMs: 1200, largestContentfulPaintMs: 2200, cumulativeLayoutShift: 0.04, }, implications: [ "Die Seite zeigt das Hauptbild zu langsam.", "Inhalte werden beim Laden sauber angezeigt.", ], opportunities: ["Serverantworten sind stabil.", "Inhalte werden gestaffelt geladen."], }, }, ]; test("buildPageSpeedAuditInputs converts normalized implications into German customer impact statements", () => { const actual = buildPageSpeedAuditInputs(MOBILE_AND_DESKTOP_FIXTURES); assert.equal(actual.customerImplications.length > 0, true); assert.equal( actual.customerImplications.includes( "Die Seite zeigt das Hauptbild zu langsam.", ), true, ); assert.equal( actual.customerImplications.includes("Die Inhalte verschieben sich beim Laden."), true, ); assert.equal( actual.customerImplications.some((line) => /mobile/i.test(line)), true, "Customer implications should include a mobile-centric statement.", ); assert.equal( assertNoPublicPageSpeedScores(actual.customerImplications), true, "Customer implications must not contain score-like values.", ); assert.equal( assertNoPublicPageSpeedScores(actual.technicalSignals), true, "Technical signals must not contain score-like values.", ); }); test("buildPageSpeedAuditInputs detects meaningful mobile performance gaps versus desktop", () => { const actual = buildPageSpeedAuditInputs(MOBILE_AND_DESKTOP_FIXTURES); assert.equal( actual.customerImplications.some((line) => /mobile/i.test(line) && /deutlich|spurbar|signifikant|langsamer/.test(line) && /desktop/i.test(line), ), true, ); }); test("buildPageSpeedAuditInputs keeps quota/api/unavailable failures in internal notes only", () => { const actual = buildPageSpeedAuditInputs([ { strategy: "mobile", status: "failed", sourceUrl: "https://bad.example", errorType: "quota", errorSummary: "API quota has been exceeded for this host.", }, { strategy: "desktop", status: "failed", sourceUrl: "https://bad2.example", errorType: "unavailable", errorSummary: "Page not reachable at the moment.", }, { strategy: "mobile", status: "failed", sourceUrl: "https://bad3.example", errorType: "api_error", errorSummary: "Lighthouse processing failed due to API timeout.", }, { strategy: "desktop", status: "succeeded", sourceUrl: "https://example.com", normalized: { implications: ["Die wichtigste Information wird zu langsam sichtbar."], }, }, ]); assert.equal( actual.customerImplications.some((line) => /quota|unavailable|timeout|api/i.test(line)), false, ); assert.equal( actual.technicalSignals.some((line) => /quota|unavailable|timeout|api/i.test(line)), false, ); assert.equal(actual.internalNotes.length >= 3, true); assert.equal(actual.internalNotes.some((line) => /quota/i.test(line)), true); assert.equal(actual.internalNotes.some((line) => /not reachable|unreachable|erreich|timeout/i.test(line)), true); assert.equal(actual.internalNotes.some((line) => /api/i.test(line)), true); }); test("buildPageSpeedAuditInputs strips score-like and raw strings from public outputs", () => { const actual = buildPageSpeedAuditInputs([ { strategy: "mobile", status: "succeeded", sourceUrl: "https://example.com", normalized: { implications: [ "Score 0.42: FCP is high.", "rawStorageId: file_123", "Lighthouse category performance is present.", "Die Seite laedt in 3.2 Sekunden.", ], opportunities: [ "Ein { \"score\": 0.91 } kann optimiert werden.", "redundante CSS Dateien.", ], }, }, ]); assert.equal(assertNoPublicPageSpeedScores(actual.customerImplications), true); assert.equal(assertNoPublicPageSpeedScores(actual.technicalSignals), true); assert.equal( actual.customerImplications.every((line) => !/\d/.test(line)), true, ); }); test("buildPageSpeedAuditInputs strips URLs, markup, JSON-like payloads, and machine-like words from public outputs", () => { const actual = buildPageSpeedAuditInputs([ { strategy: "mobile", status: "succeeded", sourceUrl: "https://example.com", normalized: { implications: [ "Weitere Infos findest du in https://example.com/details", "Das Element lädt stabil.", "{ \"pagespeed\": 0.84, \"lighthouseResult\": {} }", "[\"rawStorageId\":\"id-0123456789abcdef0123456789\"]", "rawStorageId: run_2026_0001", "lighthouseResult suggests a bad candidate.", "Die Seite laedt insgesamt spuertbar langsam.", ], opportunities: [ "Moeglichkeit: ", "Pagespeed Score should not appear.", "[{\"audit\":\"speed\"}]", "Reduziere ungenutzte JavaScript-Dateien.", "A longMachineToken_0123456789abcdef0123456789 to test filtering.", ], }, }, ]); assert.equal(assertNoPublicPageSpeedScores(actual.customerImplications), true); assert.equal(assertNoPublicPageSpeedScores(actual.technicalSignals), true); assert.equal(actual.customerImplications.includes("Die Seite laedt insgesamt spuertbar langsam."), true); assert.equal( actual.technicalSignals.some((line) => /unused|reduziere|javascript/i.test(line)), true, ); assert.equal( actual.customerImplications.every((line) => !/\bhttps?:\/\/|rawstorageid|lighthouseresult|pagespeed|score|<|>|\\{|\\}|\\[|\\]/i.test(line)), true, ); assert.equal( actual.technicalSignals.every((line) => !/\bhttps?:\/\/|rawstorageid|lighthouseresult|pagespeed|score|<|>|\\{|\\}|\\[|\\]/i.test(line)), true, ); }); test("buildPageSpeedAuditInputs keeps failure categories in internal notes while removing URLs and JSON fragments", () => { const actual = buildPageSpeedAuditInputs([ { strategy: "mobile", status: "failed", sourceUrl: "https://example.com/audit?x=1", errorType: "api_error", errorSummary: "PageSpeed API failed: { \"lighthouseResult\": {\"code\":\"timeout\"}, \"rawStorageId\": \"abc123\" }", }, { strategy: "desktop", status: "succeeded", sourceUrl: "https://example.com", normalized: { implications: [ "Die Seite laedt spuerbar schneller auf Desktop.", ], }, }, ]); assert.equal(actual.internalNotes.length >= 1, true); assert.equal( actual.internalNotes.every( (line) => !/https?:\/\//i.test(line) && !/\{|\}|\[|\]/i.test(line) && !/rawstorageid|lighthouseresult/i.test(line), ), true, ); assert.equal( actual.internalNotes.some((line) => /api|technisch/i.test(line)), true, ); }); test("buildPageSpeedAuditInputs deduplicates and caps output lists", () => { const manyImplications = Array.from({ length: 12 }, (_, index) => [ "Die Seite ist zu langsam.", "Die Seite ist zu langsam.", `Implication ${index}`, "Wichtige Inhalte sind nicht sofort sichtbar.", "Wichtige Inhalte sind nicht sofort sichtbar.", ]).flat(); const manyOpportunities = Array.from({ length: 12 }, (_, index) => [ "Komprimieren Sie Bilder.", `Opportunity ${index}`, "Komprimieren Sie Bilder.", "Inhalte werden nachgeladen.", ]).flat(); const actual = buildPageSpeedAuditInputs([ { strategy: "mobile", status: "succeeded", sourceUrl: "https://example.com", normalized: { implications: manyImplications, opportunities: manyOpportunities, }, }, ...Array.from({ length: 10 }, (_, index) => ({ strategy: "desktop" as const, status: "failed" as const, sourceUrl: `https://example.com/${index}`, errorType: "api_error" as const, errorSummary: `Run ${String.fromCharCode(97 + (index % 26))} had internal problem.`, })), ]); assert.equal(actual.customerImplications.length <= 8, true); assert.equal(actual.technicalSignals.length <= 8, true); assert.equal(actual.customerImplications.length > 0, true); assert.equal(actual.technicalSignals.length > 0, true); assert.equal(actual.internalNotes.length, 6); assert.equal( new Set(actual.customerImplications).size, actual.customerImplications.length, ); assert.equal( new Set(actual.technicalSignals).size, actual.technicalSignals.length, ); });