Integrate PageSpeed Insights audits
This commit is contained in:
226
tests/pagespeed-schema.test.ts
Normal file
226
tests/pagespeed-schema.test.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
const schemaSource = readFileSync(
|
||||
join(process.cwd(), "convex", "schema.ts"),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
type ExactSetEquality<A, B> = [
|
||||
Exclude<A, B>,
|
||||
] extends [never]
|
||||
? [Exclude<B, A>] extends [never]
|
||||
? true
|
||||
: false
|
||||
: false;
|
||||
|
||||
type AssertPageSpeedStrategy = "mobile" | "desktop";
|
||||
type AssertPageSpeedResultStatus = "succeeded" | "failed";
|
||||
type AssertPageSpeedErrorType =
|
||||
| "quota"
|
||||
| "timeout"
|
||||
| "unavailable"
|
||||
| "invalid_url"
|
||||
| "api_error"
|
||||
| "unknown";
|
||||
|
||||
type PageSpeedStrategyParity = ExactSetEquality<
|
||||
AssertPageSpeedStrategy,
|
||||
("mobile" | "desktop")
|
||||
>;
|
||||
type PageSpeedResultStatusParity = ExactSetEquality<
|
||||
AssertPageSpeedResultStatus,
|
||||
"succeeded" | "failed"
|
||||
>;
|
||||
type PageSpeedErrorTypeParity = ExactSetEquality<
|
||||
AssertPageSpeedErrorType,
|
||||
"quota" | "timeout" | "unavailable" | "invalid_url" | "api_error" | "unknown"
|
||||
>;
|
||||
|
||||
const _assertPageSpeedStrategyParity: PageSpeedStrategyParity = true;
|
||||
const _assertPageSpeedResultStatusParity: PageSpeedResultStatusParity = true;
|
||||
const _assertPageSpeedErrorTypeParity: PageSpeedErrorTypeParity = true;
|
||||
|
||||
function extractTableSection(tableName: string) {
|
||||
const marker = `${tableName}: defineTable({`;
|
||||
const markerIndex = schemaSource.indexOf(marker);
|
||||
assert.notEqual(
|
||||
markerIndex,
|
||||
-1,
|
||||
`Expected schema table definition for ${tableName}.`,
|
||||
);
|
||||
|
||||
const objectStart = schemaSource.indexOf("{", markerIndex);
|
||||
let depth = 0;
|
||||
let objectEnd = -1;
|
||||
|
||||
for (let i = objectStart; i < schemaSource.length; i += 1) {
|
||||
if (schemaSource[i] === "{") {
|
||||
depth += 1;
|
||||
} else if (schemaSource[i] === "}") {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
objectEnd = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert.notEqual(objectEnd, -1, `Could not parse schema object for ${tableName}.`);
|
||||
|
||||
const remainder = schemaSource.slice(objectEnd + 1);
|
||||
const nextTableMatch = remainder.match(/^\s*[a-zA-Z_][\w]*:\s*defineTable\(/m);
|
||||
|
||||
const sectionEnd =
|
||||
nextTableMatch === null ? schemaSource.length : objectEnd + 1 + nextTableMatch.index!;
|
||||
const section = schemaSource.slice(markerIndex, sectionEnd);
|
||||
const objectBlock = schemaSource.slice(markerIndex, objectEnd + 1);
|
||||
|
||||
return { section, objectBlock };
|
||||
}
|
||||
|
||||
function assertHas(pattern: RegExp, source: string, message: string) {
|
||||
assert.equal(pattern.test(source), true, message);
|
||||
}
|
||||
|
||||
test("PageSpeed validator unions are declared", () => {
|
||||
assert.equal(_assertPageSpeedStrategyParity, true);
|
||||
assert.equal(_assertPageSpeedResultStatusParity, true);
|
||||
assert.equal(_assertPageSpeedErrorTypeParity, true);
|
||||
|
||||
assertHas(
|
||||
/const\s+pageSpeedStrategy\s*=\s*v\.union\(\s*[\s\S]*v\.literal\(\s*["']mobile["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']desktop["']\s*\)[\s\S]*\)/,
|
||||
schemaSource,
|
||||
"Schema should define pageSpeedStrategy union with mobile and desktop.",
|
||||
);
|
||||
assertHas(
|
||||
/const\s+pageSpeedResultStatus\s*=\s*v\.union\(\s*[\s\S]*v\.literal\(\s*["']succeeded["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']failed["']\s*\)[\s\S]*\)/,
|
||||
schemaSource,
|
||||
"Schema should define pageSpeedResultStatus union with succeeded and failed.",
|
||||
);
|
||||
assertHas(
|
||||
/const\s+pageSpeedErrorType\s*=\s*v\.union\(\s*[\s\S]*v\.literal\(\s*["']quota["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']timeout["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']unavailable["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']invalid_url["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']api_error["']\s*\)\s*,\s*[\s\S]*v\.literal\(\s*["']unknown["']\s*\)[\s\S]*\)/,
|
||||
schemaSource,
|
||||
"Schema should define pageSpeedErrorType union with all declared values.",
|
||||
);
|
||||
});
|
||||
|
||||
test("pageSpeedResults table has contract fields and indexes", () => {
|
||||
const { section, objectBlock } = extractTableSection("pageSpeedResults");
|
||||
|
||||
assertHas(
|
||||
/leadId:\s*v\.id\(["']leads["']\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.leadId should be required lead id.",
|
||||
);
|
||||
assertHas(
|
||||
/auditId:\s*v\.optional\(\s*v\.id\(["']audits["']\)\s*\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.auditId should be optional audit id.",
|
||||
);
|
||||
assertHas(
|
||||
/runId:\s*v\.optional\(\s*v\.id\(["']agentRuns["']\)\s*\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.runId should be optional run id.",
|
||||
);
|
||||
assertHas(
|
||||
/strategy:\s*pageSpeedStrategy/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.strategy should use pageSpeedStrategy validator.",
|
||||
);
|
||||
assertHas(
|
||||
/status:\s*pageSpeedResultStatus/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.status should use pageSpeedResultStatus validator.",
|
||||
);
|
||||
assertHas(
|
||||
/sourceUrl:\s*v\.string\(\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.sourceUrl should be required.",
|
||||
);
|
||||
assertHas(
|
||||
/finalUrl:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.finalUrl should be optional string.",
|
||||
);
|
||||
assertHas(
|
||||
/rawStorageId:\s*v\.optional\(\s*v\.id\(["']_storage["']\)\s*\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.rawStorageId should be optional storage id.",
|
||||
);
|
||||
assertHas(
|
||||
/errorType:\s*v\.optional\(\s*pageSpeedErrorType\s*\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.errorType should be optional error type.",
|
||||
);
|
||||
assertHas(
|
||||
/errorSummary:\s*v\.optional\(\s*v\.string\(\)\s*\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.errorSummary should be optional.",
|
||||
);
|
||||
assertHas(
|
||||
/fetchedAt:\s*v\.number\(\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.fetchedAt should be required.",
|
||||
);
|
||||
assertHas(
|
||||
/createdAt:\s*v\.number\(\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.createdAt should be required.",
|
||||
);
|
||||
assertHas(
|
||||
/scores:\s*v\.optional\(\s*v\.object\([\s\S]*?performance:\s*v\.optional\(v\.number\(\)\)[\s\S]*?accessibility:\s*v\.optional\(v\.number\(\)\)[\s\S]*?bestPractices:\s*v\.optional\(v\.number\(\)\)[\s\S]*?seo:\s*v\.optional\(v\.number\(\)\)[\s\S]*?\)\s*\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.normalized.scores should include expected keys.",
|
||||
);
|
||||
assertHas(
|
||||
/metrics:\s*v\.optional\(\s*v\.object\([\s\S]*?firstContentfulPaintMs:\s*v\.optional\(v\.number\(\)\)[\s\S]*?largestContentfulPaintMs:\s*v\.optional\(v\.number\(\)\)[\s\S]*?cumulativeLayoutShift:\s*v\.optional\(v\.number\(\)\)[\s\S]*?totalBlockingTimeMs:\s*v\.optional\(v\.number\(\)\)[\s\S]*?speedIndexMs:\s*v\.optional\(v\.number\(\)\)[\s\S]*?\)\s*\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.normalized.metrics should include expected keys.",
|
||||
);
|
||||
assertHas(
|
||||
/opportunities:\s*v\.optional\(\s*v\.array\(v\.string\(\)\)\s*\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.normalized.opportunities should be optional string array.",
|
||||
);
|
||||
assertHas(
|
||||
/implications:\s*v\.optional\(\s*v\.array\(v\.string\(\)\)\s*\)/,
|
||||
objectBlock,
|
||||
"pageSpeedResults.normalized.implications should be optional string array.",
|
||||
);
|
||||
|
||||
assertHas(
|
||||
/index\("by_leadId",\s*\["leadId"\]\)/,
|
||||
section,
|
||||
"pageSpeedResults should have by_leadId index.",
|
||||
);
|
||||
assertHas(
|
||||
/index\("by_runId",\s*\["runId"\]\)/,
|
||||
section,
|
||||
"pageSpeedResults should have by_runId index.",
|
||||
);
|
||||
assertHas(
|
||||
/index\("by_auditId",\s*\["auditId"\]\)/,
|
||||
section,
|
||||
"pageSpeedResults should have by_auditId index.",
|
||||
);
|
||||
assertHas(
|
||||
/index\("by_leadId_and_strategy",\s*\["leadId",\s*"strategy"\]\)/,
|
||||
section,
|
||||
"pageSpeedResults should have by_leadId_and_strategy index.",
|
||||
);
|
||||
});
|
||||
|
||||
test("audits should not include public raw PageSpeed/Lighthouse JSON fields", () => {
|
||||
const { objectBlock } = extractTableSection("audits");
|
||||
const hasPublicRawJson = /raw.*pagespeed|pagespeed.*raw|raw.*lighthouse|lighthouse.*raw/i.test(
|
||||
objectBlock,
|
||||
);
|
||||
assert.equal(
|
||||
hasPublicRawJson,
|
||||
false,
|
||||
"audits should not expose raw PageSpeed/Lighthouse JSON fields.",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user