@riddledc/riddle-proof 0.7.31 → 0.7.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/dist/{chunk-2NWVXT4Q.js → chunk-COUZXKGM.js} +436 -1
- package/dist/cli.cjs +436 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +436 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/profile.cjs +436 -1
- package/dist/profile.d.cts +21 -2
- package/dist/profile.d.ts +21 -2
- package/dist/profile.js +1 -1
- package/examples/profiles/route-inventory-basic.json +29 -0
- package/package.json +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -6889,6 +6889,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
|
|
|
6889
6889
|
"selector_count_at_least",
|
|
6890
6890
|
"text_visible",
|
|
6891
6891
|
"text_absent",
|
|
6892
|
+
"route_inventory",
|
|
6892
6893
|
"no_horizontal_overflow",
|
|
6893
6894
|
"no_mobile_horizontal_overflow",
|
|
6894
6895
|
"no_fatal_console_errors"
|
|
@@ -7138,6 +7139,33 @@ function normalizeNetworkMocks(value) {
|
|
|
7138
7139
|
if (!Array.isArray(value)) throw new Error("target.network_mocks must be an array.");
|
|
7139
7140
|
return value.map(normalizeNetworkMock);
|
|
7140
7141
|
}
|
|
7142
|
+
function normalizeRouteInventoryPath(value, label) {
|
|
7143
|
+
const path7 = stringValue2(value);
|
|
7144
|
+
if (!path7) throw new Error(`${label} requires path.`);
|
|
7145
|
+
if (!path7.startsWith("/")) throw new Error(`${label}.path must start with /.`);
|
|
7146
|
+
return normalizeRoutePath(path7);
|
|
7147
|
+
}
|
|
7148
|
+
function normalizeRouteInventoryRoute(input, index) {
|
|
7149
|
+
if (typeof input === "string") return { path: normalizeRouteInventoryPath(input, `checks route_inventory expected_routes[${index}]`) };
|
|
7150
|
+
if (!isRecord(input)) throw new Error(`checks route_inventory expected_routes[${index}] must be a string or object.`);
|
|
7151
|
+
return {
|
|
7152
|
+
name: stringValue2(input.name),
|
|
7153
|
+
path: normalizeRouteInventoryPath(input.path, `checks route_inventory expected_routes[${index}]`)
|
|
7154
|
+
};
|
|
7155
|
+
}
|
|
7156
|
+
function normalizeRouteInventoryRoutes(value, index) {
|
|
7157
|
+
if (value === void 0) return void 0;
|
|
7158
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
7159
|
+
throw new Error(`checks[${index}] route_inventory expected_routes must be a non-empty array.`);
|
|
7160
|
+
}
|
|
7161
|
+
const routes = value.map(normalizeRouteInventoryRoute);
|
|
7162
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7163
|
+
for (const route of routes) {
|
|
7164
|
+
if (seen.has(route.path)) throw new Error(`checks[${index}] route_inventory expected_routes contains duplicate path ${route.path}.`);
|
|
7165
|
+
seen.add(route.path);
|
|
7166
|
+
}
|
|
7167
|
+
return routes;
|
|
7168
|
+
}
|
|
7141
7169
|
function normalizeCheck(input, index) {
|
|
7142
7170
|
if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
|
|
7143
7171
|
const type = stringValue2(input.type);
|
|
@@ -7154,16 +7182,34 @@ function normalizeCheck(input, index) {
|
|
|
7154
7182
|
if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
|
|
7155
7183
|
throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
|
|
7156
7184
|
}
|
|
7185
|
+
const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
|
|
7186
|
+
if (type === "route_inventory" && !expectedRoutes?.length) {
|
|
7187
|
+
throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
|
|
7188
|
+
}
|
|
7157
7189
|
return {
|
|
7158
7190
|
type,
|
|
7159
7191
|
label: stringValue2(input.label),
|
|
7160
7192
|
expected_path: stringValue2(input.expected_path),
|
|
7193
|
+
expected_routes: expectedRoutes,
|
|
7161
7194
|
selector: stringValue2(input.selector),
|
|
7195
|
+
link_selector: stringValue2(input.link_selector) || stringValue2(input.linkSelector),
|
|
7196
|
+
source_selector: stringValue2(input.source_selector) || stringValue2(input.sourceSelector),
|
|
7197
|
+
route_path_prefix: stringValue2(input.route_path_prefix) || stringValue2(input.routePathPrefix),
|
|
7198
|
+
route_ready_selector: stringValue2(input.route_ready_selector) || stringValue2(input.routeReadySelector),
|
|
7199
|
+
route_ready_text: stringValue2(input.route_ready_text) || stringValue2(input.routeReadyText),
|
|
7200
|
+
route_ready_pattern: stringValue2(input.route_ready_pattern) || stringValue2(input.routeReadyPattern),
|
|
7201
|
+
route_ready_flags: stringValue2(input.route_ready_flags) || stringValue2(input.routeReadyFlags),
|
|
7162
7202
|
text: stringValue2(input.text),
|
|
7163
7203
|
pattern: stringValue2(input.pattern),
|
|
7164
7204
|
flags: stringValue2(input.flags),
|
|
7165
7205
|
min_count: numberValue(input.min_count),
|
|
7166
|
-
max_overflow_px: numberValue(input.max_overflow_px)
|
|
7206
|
+
max_overflow_px: numberValue(input.max_overflow_px),
|
|
7207
|
+
timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
|
|
7208
|
+
run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
|
|
7209
|
+
run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
|
|
7210
|
+
require_unique_routes: input.require_unique_routes === false || input.requireUniqueRoutes === false ? false : true,
|
|
7211
|
+
allow_unexpected_routes: input.allow_unexpected_routes === true || input.allowUnexpectedRoutes === true,
|
|
7212
|
+
save_route_screenshots: input.save_route_screenshots === true || input.saveRouteScreenshots === true
|
|
7167
7213
|
};
|
|
7168
7214
|
}
|
|
7169
7215
|
function normalizeFailurePolicy(input) {
|
|
@@ -7390,6 +7436,36 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
7390
7436
|
message: failed ? `Text assertion failed in ${failed} viewport(s).` : void 0
|
|
7391
7437
|
};
|
|
7392
7438
|
}
|
|
7439
|
+
if (check.type === "route_inventory") {
|
|
7440
|
+
const inventories = viewports.map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory })).filter((item) => isRecord(item.inventory));
|
|
7441
|
+
if (!inventories.length) {
|
|
7442
|
+
return {
|
|
7443
|
+
type: check.type,
|
|
7444
|
+
label: checkLabel(check),
|
|
7445
|
+
status: "failed",
|
|
7446
|
+
evidence: { expected_count: check.expected_routes?.length || 0 },
|
|
7447
|
+
message: "No route inventory evidence was captured."
|
|
7448
|
+
};
|
|
7449
|
+
}
|
|
7450
|
+
const failures = inventories.flatMap((item) => Array.isArray(item.inventory?.failures) ? item.inventory.failures.map((failure) => toJsonValue({ viewport: item.viewport, failure })) : []);
|
|
7451
|
+
const first = inventories[0]?.inventory;
|
|
7452
|
+
const directRoutes = Array.isArray(first?.direct_routes) ? first.direct_routes : [];
|
|
7453
|
+
const clickthroughs = Array.isArray(first?.clickthroughs) ? first.clickthroughs : [];
|
|
7454
|
+
return {
|
|
7455
|
+
type: check.type,
|
|
7456
|
+
label: checkLabel(check),
|
|
7457
|
+
status: failures.length ? "failed" : "passed",
|
|
7458
|
+
evidence: {
|
|
7459
|
+
expected_count: check.expected_routes?.length || 0,
|
|
7460
|
+
homepage_link_count: numberValue(first?.home_game_link_count) ?? null,
|
|
7461
|
+
homepage_unique_link_count: numberValue(first?.home_unique_game_link_count) ?? null,
|
|
7462
|
+
direct_route_count: directRoutes.length,
|
|
7463
|
+
clickthrough_count: clickthroughs.length,
|
|
7464
|
+
failures
|
|
7465
|
+
},
|
|
7466
|
+
message: failures.length ? `Route inventory failed with ${failures.length} issue(s).` : void 0
|
|
7467
|
+
};
|
|
7468
|
+
}
|
|
7393
7469
|
if (check.type === "no_horizontal_overflow" || check.type === "no_mobile_horizontal_overflow") {
|
|
7394
7470
|
const maxOverflow = check.max_overflow_px ?? 4;
|
|
7395
7471
|
const applicable = check.type === "no_mobile_horizontal_overflow" ? viewports.filter((viewport) => viewport.width <= 820) : viewports;
|
|
@@ -7883,6 +7959,45 @@ function assessProfile(profile, evidence) {
|
|
|
7883
7959
|
});
|
|
7884
7960
|
continue;
|
|
7885
7961
|
}
|
|
7962
|
+
if (check.type === "route_inventory") {
|
|
7963
|
+
const inventories = viewports
|
|
7964
|
+
.map((viewport) => ({ viewport: viewport.name, inventory: viewport.route_inventory }))
|
|
7965
|
+
.filter((item) => item.inventory && typeof item.inventory === "object");
|
|
7966
|
+
if (!inventories.length) {
|
|
7967
|
+
checks.push({
|
|
7968
|
+
type: check.type,
|
|
7969
|
+
label: check.label || check.type,
|
|
7970
|
+
status: "failed",
|
|
7971
|
+
evidence: { expected_count: (check.expected_routes || []).length },
|
|
7972
|
+
message: "No route inventory evidence was captured.",
|
|
7973
|
+
});
|
|
7974
|
+
continue;
|
|
7975
|
+
}
|
|
7976
|
+
const failures = [];
|
|
7977
|
+
for (const item of inventories) {
|
|
7978
|
+
for (const failure of Array.isArray(item.inventory.failures) ? item.inventory.failures : []) {
|
|
7979
|
+
failures.push({ viewport: item.viewport, failure });
|
|
7980
|
+
}
|
|
7981
|
+
}
|
|
7982
|
+
const first = inventories[0].inventory || {};
|
|
7983
|
+
const directRoutes = Array.isArray(first.direct_routes) ? first.direct_routes : [];
|
|
7984
|
+
const clickthroughs = Array.isArray(first.clickthroughs) ? first.clickthroughs : [];
|
|
7985
|
+
checks.push({
|
|
7986
|
+
type: check.type,
|
|
7987
|
+
label: check.label || check.type,
|
|
7988
|
+
status: failures.length ? "failed" : "passed",
|
|
7989
|
+
evidence: {
|
|
7990
|
+
expected_count: (check.expected_routes || []).length,
|
|
7991
|
+
homepage_link_count: typeof first.home_game_link_count === "number" ? first.home_game_link_count : null,
|
|
7992
|
+
homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : null,
|
|
7993
|
+
direct_route_count: directRoutes.length,
|
|
7994
|
+
clickthrough_count: clickthroughs.length,
|
|
7995
|
+
failures,
|
|
7996
|
+
},
|
|
7997
|
+
message: failures.length ? "Route inventory failed with " + failures.length + " issue(s)." : undefined,
|
|
7998
|
+
});
|
|
7999
|
+
continue;
|
|
8000
|
+
}
|
|
7886
8001
|
if (check.type === "no_horizontal_overflow" || check.type === "no_mobile_horizontal_overflow") {
|
|
7887
8002
|
const maxOverflow = check.max_overflow_px == null ? 4 : check.max_overflow_px;
|
|
7888
8003
|
const applicable = check.type === "no_mobile_horizontal_overflow" ? viewports.filter((viewport) => viewport.width <= 820) : viewports;
|
|
@@ -8194,6 +8309,290 @@ async function selectorStats(selector) {
|
|
|
8194
8309
|
return { count: elements.length, visible_count: elements.filter(isVisible).length };
|
|
8195
8310
|
}).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
8196
8311
|
}
|
|
8312
|
+
function inventoryAppPathFromUrl(urlLike) {
|
|
8313
|
+
const url = new URL(String(urlLike), targetUrl);
|
|
8314
|
+
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
8315
|
+
let pathname = url.pathname || "/";
|
|
8316
|
+
if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
|
|
8317
|
+
pathname = pathname.slice(mountPrefix.length) || "/";
|
|
8318
|
+
}
|
|
8319
|
+
return normalizeRoutePath(pathname);
|
|
8320
|
+
}
|
|
8321
|
+
function inventoryRouteUrl(expectedPath) {
|
|
8322
|
+
const base = new URL(targetUrl);
|
|
8323
|
+
const route = new URL(expectedPath, base.origin);
|
|
8324
|
+
const mountPrefix = previewMountPrefix(base.pathname);
|
|
8325
|
+
base.pathname = mountPrefix ? joinMountedRoutePath(mountPrefix, route.pathname) : route.pathname;
|
|
8326
|
+
base.search = route.search;
|
|
8327
|
+
base.hash = route.hash;
|
|
8328
|
+
return base.href;
|
|
8329
|
+
}
|
|
8330
|
+
function inventorySlugFromPath(path) {
|
|
8331
|
+
return String(path || "route").split("/").filter(Boolean).pop()?.replace(/[^a-z0-9]+/gi, "-").toLowerCase() || "route";
|
|
8332
|
+
}
|
|
8333
|
+
function inventoryCheckTimeout(check) {
|
|
8334
|
+
const timeout = Number(check && check.timeout_ms);
|
|
8335
|
+
return Number.isFinite(timeout) && timeout > 0 ? timeout : 45000;
|
|
8336
|
+
}
|
|
8337
|
+
async function collectInventoryHomeLinks(check) {
|
|
8338
|
+
const linkSelector = check.link_selector || "a[href]";
|
|
8339
|
+
const expectedPaths = (check.expected_routes || []).map((route) => route.path);
|
|
8340
|
+
const routePathPrefix = check.route_path_prefix || "";
|
|
8341
|
+
return await page.evaluate(({ linkSelector, expectedPaths, routePathPrefix, targetUrl }) => {
|
|
8342
|
+
const previewMountPrefix = (pathname) => {
|
|
8343
|
+
const value = pathname || "/";
|
|
8344
|
+
const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
|
|
8345
|
+
if (apiPreview) return apiPreview[1];
|
|
8346
|
+
const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
|
|
8347
|
+
return internalPreview ? internalPreview[1] : "";
|
|
8348
|
+
};
|
|
8349
|
+
const normalizeRoutePath = (path) => {
|
|
8350
|
+
const value = path || "/";
|
|
8351
|
+
if (value === "/") return "/";
|
|
8352
|
+
return value.replace(/\/+$/, "") || "/";
|
|
8353
|
+
};
|
|
8354
|
+
const appPathFromHref = (href) => {
|
|
8355
|
+
const url = new URL(href, location.href);
|
|
8356
|
+
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
8357
|
+
let pathname = url.pathname || "/";
|
|
8358
|
+
if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
|
|
8359
|
+
pathname = pathname.slice(mountPrefix.length) || "/";
|
|
8360
|
+
}
|
|
8361
|
+
return normalizeRoutePath(pathname);
|
|
8362
|
+
};
|
|
8363
|
+
const expectedSet = new Set(expectedPaths);
|
|
8364
|
+
const links = Array.from(document.querySelectorAll(linkSelector)).map((anchor) => {
|
|
8365
|
+
const href = anchor.getAttribute("href") || "";
|
|
8366
|
+
const appPath = appPathFromHref(href || anchor.href || location.href);
|
|
8367
|
+
return {
|
|
8368
|
+
text: (anchor.textContent || "").replace(/\s+/g, " ").trim().slice(0, 240),
|
|
8369
|
+
href,
|
|
8370
|
+
pathname: new URL(href || anchor.href || location.href, location.href).pathname,
|
|
8371
|
+
app_path: appPath,
|
|
8372
|
+
};
|
|
8373
|
+
});
|
|
8374
|
+
return links.filter((link) => (
|
|
8375
|
+
routePathPrefix ? link.app_path.startsWith(routePathPrefix) : expectedSet.has(link.app_path)
|
|
8376
|
+
));
|
|
8377
|
+
}, { linkSelector, expectedPaths, routePathPrefix, targetUrl });
|
|
8378
|
+
}
|
|
8379
|
+
async function waitForInventoryRouteHealth(check, expectedPath) {
|
|
8380
|
+
const timeout = inventoryCheckTimeout(check);
|
|
8381
|
+
await page.waitForURL((url) => inventoryAppPathFromUrl(url.href) === normalizeRoutePath(expectedPath), { timeout: Math.min(timeout, 20000) });
|
|
8382
|
+
if (check.route_ready_selector) {
|
|
8383
|
+
await page.waitForSelector(check.route_ready_selector, { state: "visible", timeout });
|
|
8384
|
+
return;
|
|
8385
|
+
}
|
|
8386
|
+
await page.waitForFunction(({ expectedPath, sourceSelector, routeReadyText, routeReadyPattern, routeReadyFlags, targetUrl }) => {
|
|
8387
|
+
const previewMountPrefix = (pathname) => {
|
|
8388
|
+
const value = pathname || "/";
|
|
8389
|
+
const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
|
|
8390
|
+
if (apiPreview) return apiPreview[1];
|
|
8391
|
+
const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
|
|
8392
|
+
return internalPreview ? internalPreview[1] : "";
|
|
8393
|
+
};
|
|
8394
|
+
const normalizeRoutePath = (path) => {
|
|
8395
|
+
const value = path || "/";
|
|
8396
|
+
if (value === "/") return "/";
|
|
8397
|
+
return value.replace(/\/+$/, "") || "/";
|
|
8398
|
+
};
|
|
8399
|
+
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
8400
|
+
let pathname = location.pathname || "/";
|
|
8401
|
+
if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
|
|
8402
|
+
pathname = pathname.slice(mountPrefix.length) || "/";
|
|
8403
|
+
}
|
|
8404
|
+
if (normalizeRoutePath(pathname) !== normalizeRoutePath(expectedPath)) return false;
|
|
8405
|
+
if (sourceSelector && document.querySelector(sourceSelector)) return false;
|
|
8406
|
+
const text = (document.body?.innerText || "").replace(/\s+/g, " ").trim();
|
|
8407
|
+
if (routeReadyPattern) {
|
|
8408
|
+
try {
|
|
8409
|
+
if (!new RegExp(routeReadyPattern, routeReadyFlags || "").test(text)) return false;
|
|
8410
|
+
} catch {
|
|
8411
|
+
return false;
|
|
8412
|
+
}
|
|
8413
|
+
}
|
|
8414
|
+
if (routeReadyText && !text.includes(routeReadyText)) return false;
|
|
8415
|
+
const loadingOnly = /^loading\.?$/i.test(text) || (/^loading/i.test(text) && text.length < 40);
|
|
8416
|
+
if (document.querySelectorAll("canvas").length > 0) return true;
|
|
8417
|
+
if (document.querySelectorAll("button").length > 0 && !loadingOnly) return true;
|
|
8418
|
+
return text.length > 30 && !loadingOnly && !/not found|cannot get/i.test(text);
|
|
8419
|
+
}, {
|
|
8420
|
+
expectedPath,
|
|
8421
|
+
sourceSelector: check.source_selector || "",
|
|
8422
|
+
routeReadyText: check.route_ready_text || "",
|
|
8423
|
+
routeReadyPattern: check.route_ready_pattern || "",
|
|
8424
|
+
routeReadyFlags: check.route_ready_flags || "",
|
|
8425
|
+
targetUrl,
|
|
8426
|
+
}, { timeout });
|
|
8427
|
+
}
|
|
8428
|
+
async function collectInventoryRouteSnapshot(expectedRoute, phase, check, error) {
|
|
8429
|
+
return await page.evaluate(({ expectedRoute, phase, sourceSelector, error, targetUrl }) => {
|
|
8430
|
+
const previewMountPrefix = (pathname) => {
|
|
8431
|
+
const value = pathname || "/";
|
|
8432
|
+
const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
|
|
8433
|
+
if (apiPreview) return apiPreview[1];
|
|
8434
|
+
const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
|
|
8435
|
+
return internalPreview ? internalPreview[1] : "";
|
|
8436
|
+
};
|
|
8437
|
+
const normalizeRoutePath = (path) => {
|
|
8438
|
+
const value = path || "/";
|
|
8439
|
+
if (value === "/") return "/";
|
|
8440
|
+
return value.replace(/\/+$/, "") || "/";
|
|
8441
|
+
};
|
|
8442
|
+
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
8443
|
+
let pathname = location.pathname || "/";
|
|
8444
|
+
if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
|
|
8445
|
+
pathname = pathname.slice(mountPrefix.length) || "/";
|
|
8446
|
+
}
|
|
8447
|
+
const sourceCount = sourceSelector ? document.querySelectorAll(sourceSelector).length : 0;
|
|
8448
|
+
const text = (document.body?.innerText || "").replace(/\s+/g, " ").trim();
|
|
8449
|
+
return {
|
|
8450
|
+
phase,
|
|
8451
|
+
name: expectedRoute.name || null,
|
|
8452
|
+
path: expectedRoute.path,
|
|
8453
|
+
loaded: !error,
|
|
8454
|
+
error: error || null,
|
|
8455
|
+
actual_path: location.pathname,
|
|
8456
|
+
actual_app_path: normalizeRoutePath(pathname),
|
|
8457
|
+
source_selector: sourceSelector || null,
|
|
8458
|
+
source_count: sourceCount,
|
|
8459
|
+
source_visible: sourceCount > 0,
|
|
8460
|
+
title: document.title,
|
|
8461
|
+
body_text_sample: text.slice(0, 900),
|
|
8462
|
+
canvas_count: document.querySelectorAll("canvas").length,
|
|
8463
|
+
button_texts: Array.from(document.querySelectorAll("button")).slice(0, 12).map((button) => (
|
|
8464
|
+
(button.textContent || "").replace(/\s+/g, " ").trim()
|
|
8465
|
+
)),
|
|
8466
|
+
heading_texts: Array.from(document.querySelectorAll("h1,h2,h3")).slice(0, 8).map((heading) => (
|
|
8467
|
+
(heading.textContent || "").replace(/\s+/g, " ").trim()
|
|
8468
|
+
)),
|
|
8469
|
+
};
|
|
8470
|
+
}, { expectedRoute, phase, sourceSelector: check.source_selector || "", error: error || "", targetUrl });
|
|
8471
|
+
}
|
|
8472
|
+
async function findInventoryLinkIndex(check, expectedPath) {
|
|
8473
|
+
const locator = page.locator(check.link_selector || "a[href]");
|
|
8474
|
+
const count = await locator.count();
|
|
8475
|
+
for (let index = 0; index < count; index += 1) {
|
|
8476
|
+
const appPath = await locator.nth(index).evaluate((anchor, targetUrl) => {
|
|
8477
|
+
const previewMountPrefix = (pathname) => {
|
|
8478
|
+
const value = pathname || "/";
|
|
8479
|
+
const apiPreview = value.match(/^(\/s\/[^/]+)(?:\/|$)/);
|
|
8480
|
+
if (apiPreview) return apiPreview[1];
|
|
8481
|
+
const internalPreview = value.match(/^(\/preview\/[^/]+\/[^/]+)(?:\/|$)/);
|
|
8482
|
+
return internalPreview ? internalPreview[1] : "";
|
|
8483
|
+
};
|
|
8484
|
+
const normalizeRoutePath = (path) => {
|
|
8485
|
+
const value = path || "/";
|
|
8486
|
+
if (value === "/") return "/";
|
|
8487
|
+
return value.replace(/\/+$/, "") || "/";
|
|
8488
|
+
};
|
|
8489
|
+
const href = anchor.getAttribute("href") || anchor.href || location.href;
|
|
8490
|
+
const url = new URL(href, location.href);
|
|
8491
|
+
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
8492
|
+
let pathname = url.pathname || "/";
|
|
8493
|
+
if (mountPrefix && (pathname === mountPrefix || pathname.startsWith(mountPrefix + "/"))) {
|
|
8494
|
+
pathname = pathname.slice(mountPrefix.length) || "/";
|
|
8495
|
+
}
|
|
8496
|
+
return normalizeRoutePath(pathname);
|
|
8497
|
+
}, targetUrl).catch(() => "");
|
|
8498
|
+
if (appPath === normalizeRoutePath(expectedPath)) return index;
|
|
8499
|
+
}
|
|
8500
|
+
return -1;
|
|
8501
|
+
}
|
|
8502
|
+
async function collectRouteInventory(check, viewport) {
|
|
8503
|
+
const expectedRoutes = check.expected_routes || [];
|
|
8504
|
+
const expectedPaths = expectedRoutes.map((route) => normalizeRoutePath(route.path));
|
|
8505
|
+
const expectedSet = new Set(expectedPaths);
|
|
8506
|
+
const failures = [];
|
|
8507
|
+
const homeLinks = await collectInventoryHomeLinks(check);
|
|
8508
|
+
const homeLinkPaths = homeLinks.map((link) => link.app_path);
|
|
8509
|
+
const uniqueHomeLinkPaths = Array.from(new Set(homeLinkPaths));
|
|
8510
|
+
const duplicateHomeLinkPaths = homeLinkPaths.filter((path, index) => homeLinkPaths.indexOf(path) !== index);
|
|
8511
|
+
if (check.require_unique_routes !== false && duplicateHomeLinkPaths.length) {
|
|
8512
|
+
failures.push({ code: "duplicate_source_links", paths: Array.from(new Set(duplicateHomeLinkPaths)) });
|
|
8513
|
+
}
|
|
8514
|
+
for (const route of expectedRoutes) {
|
|
8515
|
+
if (!homeLinkPaths.includes(normalizeRoutePath(route.path))) {
|
|
8516
|
+
failures.push({ code: "expected_route_missing_from_source", name: route.name || null, path: route.path });
|
|
8517
|
+
}
|
|
8518
|
+
}
|
|
8519
|
+
const unexpectedRoutes = uniqueHomeLinkPaths.filter((path) => !expectedSet.has(path));
|
|
8520
|
+
if (!check.allow_unexpected_routes && unexpectedRoutes.length) {
|
|
8521
|
+
failures.push({ code: "unexpected_source_links", paths: unexpectedRoutes });
|
|
8522
|
+
}
|
|
8523
|
+
if (!check.allow_unexpected_routes && uniqueHomeLinkPaths.length !== expectedRoutes.length) {
|
|
8524
|
+
failures.push({ code: "source_link_count_mismatch", expected: expectedRoutes.length, actual_unique: uniqueHomeLinkPaths.length, actual_total: homeLinkPaths.length });
|
|
8525
|
+
}
|
|
8526
|
+
|
|
8527
|
+
const directRoutes = [];
|
|
8528
|
+
if (check.run_direct_routes !== false) {
|
|
8529
|
+
for (const expectedRoute of expectedRoutes) {
|
|
8530
|
+
let error = "";
|
|
8531
|
+
try {
|
|
8532
|
+
await page.goto(inventoryRouteUrl(expectedRoute.path), { waitUntil: "domcontentloaded", timeout: 90000 });
|
|
8533
|
+
await waitForInventoryRouteHealth(check, expectedRoute.path);
|
|
8534
|
+
} catch (caught) {
|
|
8535
|
+
error = String(caught && caught.message ? caught.message : caught).slice(0, 1000);
|
|
8536
|
+
}
|
|
8537
|
+
const snapshot = await collectInventoryRouteSnapshot(expectedRoute, "direct", check, error);
|
|
8538
|
+
directRoutes.push(snapshot);
|
|
8539
|
+
if (check.save_route_screenshots) {
|
|
8540
|
+
await saveScreenshot(profileSlug + "-direct-" + inventorySlugFromPath(expectedRoute.path)).catch(() => {});
|
|
8541
|
+
}
|
|
8542
|
+
if (error) failures.push({ code: "direct_route_unhealthy", name: expectedRoute.name || null, path: expectedRoute.path, error });
|
|
8543
|
+
else if (snapshot.actual_app_path !== normalizeRoutePath(expectedRoute.path)) failures.push({ code: "direct_route_wrong_path", name: expectedRoute.name || null, path: expectedRoute.path, actual_app_path: snapshot.actual_app_path });
|
|
8544
|
+
else if (check.source_selector && snapshot.source_visible) failures.push({ code: "direct_route_kept_source_surface", name: expectedRoute.name || null, path: expectedRoute.path, source_selector: check.source_selector });
|
|
8545
|
+
}
|
|
8546
|
+
}
|
|
8547
|
+
|
|
8548
|
+
const clickthroughs = [];
|
|
8549
|
+
if (check.run_clickthroughs !== false) {
|
|
8550
|
+
for (const expectedRoute of expectedRoutes) {
|
|
8551
|
+
const result = { name: expectedRoute.name || null, path: expectedRoute.path, clicked: false, snapshot: null, error: null };
|
|
8552
|
+
try {
|
|
8553
|
+
await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 90000 });
|
|
8554
|
+
if (profile.target.wait_for_selector) {
|
|
8555
|
+
await page.waitForSelector(profile.target.wait_for_selector, { state: "visible", timeout: 15000 }).catch(() => {});
|
|
8556
|
+
}
|
|
8557
|
+
const index = await findInventoryLinkIndex(check, expectedRoute.path);
|
|
8558
|
+
if (index < 0) {
|
|
8559
|
+
result.error = "source_link_not_found";
|
|
8560
|
+
failures.push({ code: "source_link_clickthrough_missing", name: expectedRoute.name || null, path: expectedRoute.path });
|
|
8561
|
+
} else {
|
|
8562
|
+
const locator = page.locator(check.link_selector || "a[href]").nth(index);
|
|
8563
|
+
await locator.scrollIntoViewIfNeeded();
|
|
8564
|
+
await locator.click({ timeout: inventoryCheckTimeout(check), noWaitAfter: true });
|
|
8565
|
+
result.clicked = true;
|
|
8566
|
+
await waitForInventoryRouteHealth(check, expectedRoute.path);
|
|
8567
|
+
}
|
|
8568
|
+
} catch (caught) {
|
|
8569
|
+
result.error = String(caught && caught.message ? caught.message : caught).slice(0, 1000);
|
|
8570
|
+
}
|
|
8571
|
+
result.snapshot = await collectInventoryRouteSnapshot(expectedRoute, "clickthrough", check, result.error || "");
|
|
8572
|
+
clickthroughs.push(result);
|
|
8573
|
+
if (check.save_route_screenshots) {
|
|
8574
|
+
await saveScreenshot(profileSlug + "-click-" + inventorySlugFromPath(expectedRoute.path)).catch(() => {});
|
|
8575
|
+
}
|
|
8576
|
+
if (result.error && result.error !== "source_link_not_found") failures.push({ code: "source_link_clickthrough_unhealthy", name: expectedRoute.name || null, path: expectedRoute.path, error: result.error });
|
|
8577
|
+
else if (result.snapshot.actual_app_path !== normalizeRoutePath(expectedRoute.path)) failures.push({ code: "source_link_clickthrough_wrong_path", name: expectedRoute.name || null, path: expectedRoute.path, actual_app_path: result.snapshot.actual_app_path });
|
|
8578
|
+
else if (check.source_selector && result.snapshot.source_visible) failures.push({ code: "source_link_clickthrough_kept_source_surface", name: expectedRoute.name || null, path: expectedRoute.path, source_selector: check.source_selector });
|
|
8579
|
+
}
|
|
8580
|
+
}
|
|
8581
|
+
|
|
8582
|
+
return {
|
|
8583
|
+
version: "riddle-proof.route-inventory.v1",
|
|
8584
|
+
viewport: viewport.name,
|
|
8585
|
+
expected_routes: expectedRoutes,
|
|
8586
|
+
link_selector: check.link_selector || "a[href]",
|
|
8587
|
+
source_selector: check.source_selector || null,
|
|
8588
|
+
home_game_link_count: homeLinkPaths.length,
|
|
8589
|
+
home_unique_game_link_count: uniqueHomeLinkPaths.length,
|
|
8590
|
+
home_links: homeLinks,
|
|
8591
|
+
direct_routes: directRoutes,
|
|
8592
|
+
clickthroughs,
|
|
8593
|
+
failures,
|
|
8594
|
+
};
|
|
8595
|
+
}
|
|
8197
8596
|
async function captureViewport(viewport) {
|
|
8198
8597
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
8199
8598
|
let httpStatus = null;
|
|
@@ -8316,6 +8715,31 @@ async function captureViewport(viewport) {
|
|
|
8316
8715
|
} catch (error) {
|
|
8317
8716
|
pageErrors.push({ message: "saveScreenshot failed: " + String(error && error.message ? error.message : error).slice(0, 500) });
|
|
8318
8717
|
}
|
|
8718
|
+
let routeInventory;
|
|
8719
|
+
const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
|
|
8720
|
+
const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
|
|
8721
|
+
if (routeInventoryCheck && (!firstViewportName || viewport.name === firstViewportName)) {
|
|
8722
|
+
try {
|
|
8723
|
+
routeInventory = await collectRouteInventory(routeInventoryCheck, viewport);
|
|
8724
|
+
} catch (error) {
|
|
8725
|
+
routeInventory = {
|
|
8726
|
+
version: "riddle-proof.route-inventory.v1",
|
|
8727
|
+
viewport: viewport.name,
|
|
8728
|
+
expected_routes: routeInventoryCheck.expected_routes || [],
|
|
8729
|
+
link_selector: routeInventoryCheck.link_selector || "a[href]",
|
|
8730
|
+
source_selector: routeInventoryCheck.source_selector || null,
|
|
8731
|
+
home_game_link_count: 0,
|
|
8732
|
+
home_unique_game_link_count: 0,
|
|
8733
|
+
home_links: [],
|
|
8734
|
+
direct_routes: [],
|
|
8735
|
+
clickthroughs: [],
|
|
8736
|
+
failures: [{
|
|
8737
|
+
code: "route_inventory_capture_failed",
|
|
8738
|
+
error: String(error && error.message ? error.message : error).slice(0, 1000),
|
|
8739
|
+
}],
|
|
8740
|
+
};
|
|
8741
|
+
}
|
|
8742
|
+
}
|
|
8319
8743
|
const expectedPath = profile.checks.find((check) => check.type === "route_loaded" && check.expected_path)?.expected_path || new URL(targetUrl).pathname || "/";
|
|
8320
8744
|
return {
|
|
8321
8745
|
name: viewport.name,
|
|
@@ -8340,6 +8764,7 @@ async function captureViewport(viewport) {
|
|
|
8340
8764
|
overflow_offenders: dom.overflow_offenders || [],
|
|
8341
8765
|
selectors,
|
|
8342
8766
|
text_matches,
|
|
8767
|
+
route_inventory: routeInventory,
|
|
8343
8768
|
setup_action_results: setupActionResults,
|
|
8344
8769
|
screenshot_label: screenshotLabel,
|
|
8345
8770
|
navigation_error: navigationError,
|
|
@@ -8372,6 +8797,16 @@ function buildProfileEvidence(currentViewports) {
|
|
|
8372
8797
|
overflow_px: currentViewports.map((viewport) => viewport.overflow_px),
|
|
8373
8798
|
bounds_overflow_px: currentViewports.map((viewport) => viewport.bounds_overflow_px),
|
|
8374
8799
|
overflow_offender_counts: currentViewports.map((viewport) => (viewport.overflow_offenders || []).length),
|
|
8800
|
+
route_inventory: currentViewports
|
|
8801
|
+
.filter((viewport) => viewport.route_inventory)
|
|
8802
|
+
.map((viewport) => ({
|
|
8803
|
+
viewport: viewport.name,
|
|
8804
|
+
expected_count: (viewport.route_inventory.expected_routes || []).length,
|
|
8805
|
+
home_unique_game_link_count: viewport.route_inventory.home_unique_game_link_count,
|
|
8806
|
+
direct_route_count: (viewport.route_inventory.direct_routes || []).length,
|
|
8807
|
+
clickthrough_count: (viewport.route_inventory.clickthroughs || []).length,
|
|
8808
|
+
failure_count: (viewport.route_inventory.failures || []).length,
|
|
8809
|
+
})),
|
|
8375
8810
|
network_mock_count: (profile.target.network_mocks || []).length,
|
|
8376
8811
|
network_mock_hit_count: networkMockEvents.filter((event) => event.ok !== false).length,
|
|
8377
8812
|
},
|
package/dist/cli.js
CHANGED