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,
);
});