@riddledc/riddle-proof 0.7.34 → 0.7.36
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 +18 -1
- package/dist/{chunk-PWGJQLOS.js → chunk-VEK6QHPE.js} +182 -1
- package/dist/cli.cjs +182 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +182 -1
- package/dist/index.js +1 -1
- package/dist/profile.cjs +182 -1
- package/dist/profile.d.cts +4 -1
- package/dist/profile.d.ts +4 -1
- package/dist/profile.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -202,6 +202,21 @@ clears `local`, `session`, or `both` browser storage scopes, defaults to
|
|
|
202
202
|
profile carries its own hosted Riddle worker budget; an explicit CLI `--timeout`
|
|
203
203
|
still overrides the profile value for one-off runs.
|
|
204
204
|
|
|
205
|
+
Use `selector_text_order` when a table, list, or card group must show visible
|
|
206
|
+
items in a specific order after setup actions such as sorting or filtering:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"type": "selector_text_order",
|
|
211
|
+
"selector": ".game-table tbody tr",
|
|
212
|
+
"expected_texts": ["AAA Alpha", "MMM Middle", "ZZZ Omega"]
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The check records the visible text sequence for the selector and passes when
|
|
217
|
+
the expected texts appear in that order as a subsequence. This is less brittle
|
|
218
|
+
than matching one large body-text regex when only row or card order matters.
|
|
219
|
+
|
|
205
220
|
Use the `route_inventory` check for source-page route coverage audits where a
|
|
206
221
|
navigation surface must expose a known set of routes and each route must load
|
|
207
222
|
both directly and through real link clicks:
|
|
@@ -225,7 +240,9 @@ source-link counts, missing/unexpected routes, direct route health, real
|
|
|
225
240
|
clickthrough health, wrong-path failures, and stale source-surface failures. It
|
|
226
241
|
runs direct/clickthrough sweeps on the first viewport by default and leaves
|
|
227
242
|
ordinary profile overflow checks to cover the source page across all configured
|
|
228
|
-
viewports. Set `
|
|
243
|
+
viewports. Set `run_all_viewports: true` when desktop and mobile navigation
|
|
244
|
+
surfaces both need direct/clickthrough inventory evidence in one profile result.
|
|
245
|
+
Set `run_direct_routes: false`, `run_clickthroughs: false`,
|
|
229
246
|
`allow_unexpected_routes: true`, `require_unique_routes: false`, or
|
|
230
247
|
`save_route_screenshots: true` when a profile needs a narrower or more
|
|
231
248
|
artifact-heavy audit. `require_unique_routes: false` is useful when a navigation
|
|
@@ -14,6 +14,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
|
|
|
14
14
|
"route_loaded",
|
|
15
15
|
"selector_visible",
|
|
16
16
|
"selector_count_at_least",
|
|
17
|
+
"selector_text_order",
|
|
17
18
|
"text_visible",
|
|
18
19
|
"text_absent",
|
|
19
20
|
"route_inventory",
|
|
@@ -293,6 +294,15 @@ function normalizeRouteInventoryRoutes(value, index) {
|
|
|
293
294
|
}
|
|
294
295
|
return routes;
|
|
295
296
|
}
|
|
297
|
+
function normalizeExpectedTexts(value, index) {
|
|
298
|
+
if (value === void 0) return void 0;
|
|
299
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
300
|
+
throw new Error(`checks[${index}] selector_text_order expected_texts must be a non-empty array.`);
|
|
301
|
+
}
|
|
302
|
+
const texts = value.map((item) => String(item).replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
303
|
+
if (!texts.length) throw new Error(`checks[${index}] selector_text_order expected_texts must contain non-empty strings.`);
|
|
304
|
+
return texts;
|
|
305
|
+
}
|
|
296
306
|
function normalizeCheck(input, index) {
|
|
297
307
|
if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
|
|
298
308
|
const type = stringValue(input.type);
|
|
@@ -309,6 +319,11 @@ function normalizeCheck(input, index) {
|
|
|
309
319
|
if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
|
|
310
320
|
throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
|
|
311
321
|
}
|
|
322
|
+
const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
|
|
323
|
+
if (type === "selector_text_order") {
|
|
324
|
+
if (!stringValue(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
|
|
325
|
+
if (!expectedTexts?.length) throw new Error(`checks[${index}] selector_text_order requires expected_texts.`);
|
|
326
|
+
}
|
|
312
327
|
const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
|
|
313
328
|
if (type === "route_inventory" && !expectedRoutes?.length) {
|
|
314
329
|
throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
|
|
@@ -319,6 +334,7 @@ function normalizeCheck(input, index) {
|
|
|
319
334
|
expected_path: stringValue(input.expected_path),
|
|
320
335
|
expected_routes: expectedRoutes,
|
|
321
336
|
selector: stringValue(input.selector),
|
|
337
|
+
expected_texts: expectedTexts,
|
|
322
338
|
link_selector: stringValue(input.link_selector) || stringValue(input.linkSelector),
|
|
323
339
|
source_selector: stringValue(input.source_selector) || stringValue(input.sourceSelector),
|
|
324
340
|
route_path_prefix: stringValue(input.route_path_prefix) || stringValue(input.routePathPrefix),
|
|
@@ -334,6 +350,7 @@ function normalizeCheck(input, index) {
|
|
|
334
350
|
timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
|
|
335
351
|
run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
|
|
336
352
|
run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
|
|
353
|
+
run_all_viewports: input.run_all_viewports === true || input.runAllViewports === true,
|
|
337
354
|
require_unique_routes: input.require_unique_routes === false || input.requireUniqueRoutes === false ? false : true,
|
|
338
355
|
allow_unexpected_routes: input.allow_unexpected_routes === true || input.allowUnexpectedRoutes === true,
|
|
339
356
|
save_route_screenshots: input.save_route_screenshots === true || input.saveRouteScreenshots === true
|
|
@@ -424,6 +441,47 @@ function selectorKey(check) {
|
|
|
424
441
|
function textKey(check) {
|
|
425
442
|
return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
|
|
426
443
|
}
|
|
444
|
+
function textSequenceForCheck(viewport, check) {
|
|
445
|
+
const key = selectorKey(check);
|
|
446
|
+
const sequence = viewport.text_sequences?.[key];
|
|
447
|
+
if (isRecord(sequence)) {
|
|
448
|
+
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
449
|
+
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
450
|
+
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
451
|
+
return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
452
|
+
}
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
function textOrderMatch(texts, expectedTexts) {
|
|
456
|
+
const positions = [];
|
|
457
|
+
let startAt = 0;
|
|
458
|
+
for (const expected of expectedTexts) {
|
|
459
|
+
const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
|
|
460
|
+
if (index < 0) return { matched: false, positions };
|
|
461
|
+
positions.push(index);
|
|
462
|
+
startAt = index + 1;
|
|
463
|
+
}
|
|
464
|
+
return { matched: true, positions };
|
|
465
|
+
}
|
|
466
|
+
function summarizeRouteInventory(viewport, inventory) {
|
|
467
|
+
const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
|
|
468
|
+
const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
|
|
469
|
+
const sourceLinkCount = numberValue(inventory.source_link_count) ?? numberValue(inventory.home_game_link_count) ?? null;
|
|
470
|
+
const sourceUniqueLinkCount = numberValue(inventory.source_unique_link_count) ?? numberValue(inventory.home_unique_game_link_count) ?? null;
|
|
471
|
+
const duplicateSourceLinks = Array.isArray(inventory.duplicate_source_link_paths) ? inventory.duplicate_source_link_paths.map((path) => String(path)) : [];
|
|
472
|
+
const duplicateSourceLinkCount = numberValue(inventory.duplicate_source_link_count) ?? (sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null);
|
|
473
|
+
const failures = Array.isArray(inventory.failures) ? inventory.failures : [];
|
|
474
|
+
return {
|
|
475
|
+
viewport,
|
|
476
|
+
source_link_count: sourceLinkCount,
|
|
477
|
+
source_unique_link_count: sourceUniqueLinkCount,
|
|
478
|
+
duplicate_source_link_count: duplicateSourceLinkCount,
|
|
479
|
+
duplicate_source_links: duplicateSourceLinks,
|
|
480
|
+
direct_route_count: directRoutes.length,
|
|
481
|
+
clickthrough_count: clickthroughs.length,
|
|
482
|
+
failure_count: failures.length
|
|
483
|
+
};
|
|
484
|
+
}
|
|
427
485
|
function matchText(sample, check) {
|
|
428
486
|
if (check.pattern) {
|
|
429
487
|
try {
|
|
@@ -543,6 +601,32 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
543
601
|
message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
|
|
544
602
|
};
|
|
545
603
|
}
|
|
604
|
+
if (check.type === "selector_text_order") {
|
|
605
|
+
const key = selectorKey(check);
|
|
606
|
+
const expectedTexts = check.expected_texts || [];
|
|
607
|
+
const results = viewports.map((viewport) => {
|
|
608
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
609
|
+
const match = textOrderMatch(texts, expectedTexts);
|
|
610
|
+
return {
|
|
611
|
+
viewport: viewport.name,
|
|
612
|
+
matched: match.matched,
|
|
613
|
+
matched_positions: match.positions,
|
|
614
|
+
visible_texts: texts.slice(0, 20)
|
|
615
|
+
};
|
|
616
|
+
});
|
|
617
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
618
|
+
return {
|
|
619
|
+
type: check.type,
|
|
620
|
+
label: checkLabel(check),
|
|
621
|
+
status: failed ? "failed" : "passed",
|
|
622
|
+
evidence: {
|
|
623
|
+
selector: key,
|
|
624
|
+
expected_texts: expectedTexts,
|
|
625
|
+
viewports: results.map((result) => toJsonValue(result))
|
|
626
|
+
},
|
|
627
|
+
message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
|
|
628
|
+
};
|
|
629
|
+
}
|
|
546
630
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
547
631
|
const key = textKey(check);
|
|
548
632
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -574,6 +658,7 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
574
658
|
message: "No route inventory evidence was captured."
|
|
575
659
|
};
|
|
576
660
|
}
|
|
661
|
+
const viewportSummaries = inventories.map((item) => summarizeRouteInventory(item.viewport, item.inventory));
|
|
577
662
|
const failures = inventories.flatMap((item) => Array.isArray(item.inventory?.failures) ? item.inventory.failures.map((failure) => toJsonValue({ viewport: item.viewport, failure })) : []);
|
|
578
663
|
const first = inventories[0]?.inventory;
|
|
579
664
|
const directRoutes = Array.isArray(first?.direct_routes) ? first.direct_routes : [];
|
|
@@ -597,6 +682,8 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
597
682
|
homepage_unique_link_count: numberValue(first?.home_unique_game_link_count) ?? sourceUniqueLinkCount,
|
|
598
683
|
direct_route_count: directRoutes.length,
|
|
599
684
|
clickthrough_count: clickthroughs.length,
|
|
685
|
+
viewport_count: inventories.length,
|
|
686
|
+
viewports: viewportSummaries.map((summary) => toJsonValue(summary)),
|
|
600
687
|
failures
|
|
601
688
|
},
|
|
602
689
|
message: failures.length ? `Route inventory failed with ${failures.length} issue(s).` : void 0
|
|
@@ -893,6 +980,47 @@ function textMatches(sample, check) {
|
|
|
893
980
|
}
|
|
894
981
|
return String(sample || "").includes(check.text || "");
|
|
895
982
|
}
|
|
983
|
+
function textSequenceForCheck(viewport, check) {
|
|
984
|
+
const key = check.selector || "";
|
|
985
|
+
const sequence = viewport.text_sequences && viewport.text_sequences[key];
|
|
986
|
+
if (sequence && typeof sequence === "object") {
|
|
987
|
+
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
988
|
+
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
989
|
+
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
990
|
+
return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
991
|
+
}
|
|
992
|
+
return [];
|
|
993
|
+
}
|
|
994
|
+
function textOrderMatch(texts, expectedTexts) {
|
|
995
|
+
const positions = [];
|
|
996
|
+
let startAt = 0;
|
|
997
|
+
for (const expected of expectedTexts || []) {
|
|
998
|
+
const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
|
|
999
|
+
if (index < 0) return { matched: false, positions };
|
|
1000
|
+
positions.push(index);
|
|
1001
|
+
startAt = index + 1;
|
|
1002
|
+
}
|
|
1003
|
+
return { matched: true, positions };
|
|
1004
|
+
}
|
|
1005
|
+
function summarizeRouteInventory(viewport, inventory) {
|
|
1006
|
+
const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
|
|
1007
|
+
const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
|
|
1008
|
+
const sourceLinkCount = typeof inventory.source_link_count === "number" ? inventory.source_link_count : typeof inventory.home_game_link_count === "number" ? inventory.home_game_link_count : null;
|
|
1009
|
+
const sourceUniqueLinkCount = typeof inventory.source_unique_link_count === "number" ? inventory.source_unique_link_count : typeof inventory.home_unique_game_link_count === "number" ? inventory.home_unique_game_link_count : null;
|
|
1010
|
+
const duplicateSourceLinks = Array.isArray(inventory.duplicate_source_link_paths) ? inventory.duplicate_source_link_paths.map((path) => String(path)) : [];
|
|
1011
|
+
const duplicateSourceLinkCount = typeof inventory.duplicate_source_link_count === "number" ? inventory.duplicate_source_link_count : sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null;
|
|
1012
|
+
const failures = Array.isArray(inventory.failures) ? inventory.failures : [];
|
|
1013
|
+
return {
|
|
1014
|
+
viewport,
|
|
1015
|
+
source_link_count: sourceLinkCount,
|
|
1016
|
+
source_unique_link_count: sourceUniqueLinkCount,
|
|
1017
|
+
duplicate_source_link_count: duplicateSourceLinkCount,
|
|
1018
|
+
duplicate_source_links: duplicateSourceLinks,
|
|
1019
|
+
direct_route_count: directRoutes.length,
|
|
1020
|
+
clickthrough_count: clickthroughs.length,
|
|
1021
|
+
failure_count: failures.length,
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
896
1024
|
function numberValue(value) {
|
|
897
1025
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
898
1026
|
}
|
|
@@ -1097,6 +1225,29 @@ function assessProfile(profile, evidence) {
|
|
|
1097
1225
|
});
|
|
1098
1226
|
continue;
|
|
1099
1227
|
}
|
|
1228
|
+
if (check.type === "selector_text_order") {
|
|
1229
|
+
const selector = check.selector || "";
|
|
1230
|
+
const expectedTexts = check.expected_texts || [];
|
|
1231
|
+
const results = viewports.map((viewport) => {
|
|
1232
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
1233
|
+
const match = textOrderMatch(texts, expectedTexts);
|
|
1234
|
+
return {
|
|
1235
|
+
viewport: viewport.name,
|
|
1236
|
+
matched: match.matched,
|
|
1237
|
+
matched_positions: match.positions,
|
|
1238
|
+
visible_texts: texts.slice(0, 20),
|
|
1239
|
+
};
|
|
1240
|
+
});
|
|
1241
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
1242
|
+
checks.push({
|
|
1243
|
+
type: check.type,
|
|
1244
|
+
label: check.label || check.type,
|
|
1245
|
+
status: failed ? "failed" : "passed",
|
|
1246
|
+
evidence: { selector, expected_texts: expectedTexts, viewports: results },
|
|
1247
|
+
message: failed ? "Selector " + selector + " text order failed in " + failed + " viewport(s)." : undefined,
|
|
1248
|
+
});
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1100
1251
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
1101
1252
|
const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
|
|
1102
1253
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -1125,6 +1276,7 @@ function assessProfile(profile, evidence) {
|
|
|
1125
1276
|
});
|
|
1126
1277
|
continue;
|
|
1127
1278
|
}
|
|
1279
|
+
const viewportSummaries = inventories.map((item) => summarizeRouteInventory(item.viewport, item.inventory || {}));
|
|
1128
1280
|
const failures = [];
|
|
1129
1281
|
for (const item of inventories) {
|
|
1130
1282
|
for (const failure of Array.isArray(item.inventory.failures) ? item.inventory.failures : []) {
|
|
@@ -1153,6 +1305,8 @@ function assessProfile(profile, evidence) {
|
|
|
1153
1305
|
homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : sourceUniqueLinkCount,
|
|
1154
1306
|
direct_route_count: directRoutes.length,
|
|
1155
1307
|
clickthrough_count: clickthroughs.length,
|
|
1308
|
+
viewport_count: inventories.length,
|
|
1309
|
+
viewports: viewportSummaries,
|
|
1156
1310
|
failures,
|
|
1157
1311
|
},
|
|
1158
1312
|
message: failures.length ? "Route inventory failed with " + failures.length + " issue(s)." : undefined,
|
|
@@ -1470,6 +1624,27 @@ async function selectorStats(selector) {
|
|
|
1470
1624
|
return { count: elements.length, visible_count: elements.filter(isVisible).length };
|
|
1471
1625
|
}).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
1472
1626
|
}
|
|
1627
|
+
async function selectorTextSequence(selector) {
|
|
1628
|
+
return page.locator(selector).evaluateAll((elements) => {
|
|
1629
|
+
const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
1630
|
+
const isVisible = (element) => {
|
|
1631
|
+
const style = window.getComputedStyle(element);
|
|
1632
|
+
const rect = element.getBoundingClientRect();
|
|
1633
|
+
return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
|
|
1634
|
+
};
|
|
1635
|
+
const rows = elements.map((element, index) => ({
|
|
1636
|
+
index,
|
|
1637
|
+
text: compact(element.innerText || element.textContent || ""),
|
|
1638
|
+
visible: isVisible(element),
|
|
1639
|
+
}));
|
|
1640
|
+
return {
|
|
1641
|
+
count: rows.length,
|
|
1642
|
+
visible_count: rows.filter((row) => row.visible).length,
|
|
1643
|
+
texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
1644
|
+
visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
1645
|
+
};
|
|
1646
|
+
}).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
1647
|
+
}
|
|
1473
1648
|
function inventoryAppPathFromUrl(urlLike) {
|
|
1474
1649
|
const url = new URL(String(urlLike), targetUrl);
|
|
1475
1650
|
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
@@ -1867,11 +2042,16 @@ async function captureViewport(viewport) {
|
|
|
1867
2042
|
evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
|
|
1868
2043
|
}));
|
|
1869
2044
|
const selectors = {};
|
|
2045
|
+
const text_sequences = {};
|
|
1870
2046
|
const text_matches = {};
|
|
1871
2047
|
for (const check of profile.checks || []) {
|
|
1872
2048
|
if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
|
|
1873
2049
|
selectors[check.selector] = await selectorStats(check.selector);
|
|
1874
2050
|
}
|
|
2051
|
+
if (check.type === "selector_text_order" && check.selector) {
|
|
2052
|
+
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
2053
|
+
text_sequences[check.selector] = await selectorTextSequence(check.selector);
|
|
2054
|
+
}
|
|
1875
2055
|
if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
|
|
1876
2056
|
text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
|
|
1877
2057
|
}
|
|
@@ -1885,7 +2065,7 @@ async function captureViewport(viewport) {
|
|
|
1885
2065
|
let routeInventory;
|
|
1886
2066
|
const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
|
|
1887
2067
|
const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
|
|
1888
|
-
if (routeInventoryCheck && (!firstViewportName || viewport.name === firstViewportName)) {
|
|
2068
|
+
if (routeInventoryCheck && (routeInventoryCheck.run_all_viewports || !firstViewportName || viewport.name === firstViewportName)) {
|
|
1889
2069
|
try {
|
|
1890
2070
|
routeInventory = await collectRouteInventory(routeInventoryCheck, viewport);
|
|
1891
2071
|
} catch (error) {
|
|
@@ -1935,6 +2115,7 @@ async function captureViewport(viewport) {
|
|
|
1935
2115
|
bounds_overflow_px: dom.bounds_overflow_px,
|
|
1936
2116
|
overflow_offenders: dom.overflow_offenders || [],
|
|
1937
2117
|
selectors,
|
|
2118
|
+
text_sequences,
|
|
1938
2119
|
text_matches,
|
|
1939
2120
|
route_inventory: routeInventory,
|
|
1940
2121
|
setup_action_results: setupActionResults,
|
package/dist/cli.cjs
CHANGED
|
@@ -6887,6 +6887,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
|
|
|
6887
6887
|
"route_loaded",
|
|
6888
6888
|
"selector_visible",
|
|
6889
6889
|
"selector_count_at_least",
|
|
6890
|
+
"selector_text_order",
|
|
6890
6891
|
"text_visible",
|
|
6891
6892
|
"text_absent",
|
|
6892
6893
|
"route_inventory",
|
|
@@ -7166,6 +7167,15 @@ function normalizeRouteInventoryRoutes(value, index) {
|
|
|
7166
7167
|
}
|
|
7167
7168
|
return routes;
|
|
7168
7169
|
}
|
|
7170
|
+
function normalizeExpectedTexts(value, index) {
|
|
7171
|
+
if (value === void 0) return void 0;
|
|
7172
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
7173
|
+
throw new Error(`checks[${index}] selector_text_order expected_texts must be a non-empty array.`);
|
|
7174
|
+
}
|
|
7175
|
+
const texts = value.map((item) => String(item).replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
7176
|
+
if (!texts.length) throw new Error(`checks[${index}] selector_text_order expected_texts must contain non-empty strings.`);
|
|
7177
|
+
return texts;
|
|
7178
|
+
}
|
|
7169
7179
|
function normalizeCheck(input, index) {
|
|
7170
7180
|
if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
|
|
7171
7181
|
const type = stringValue2(input.type);
|
|
@@ -7182,6 +7192,11 @@ function normalizeCheck(input, index) {
|
|
|
7182
7192
|
if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
|
|
7183
7193
|
throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
|
|
7184
7194
|
}
|
|
7195
|
+
const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
|
|
7196
|
+
if (type === "selector_text_order") {
|
|
7197
|
+
if (!stringValue2(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
|
|
7198
|
+
if (!expectedTexts?.length) throw new Error(`checks[${index}] selector_text_order requires expected_texts.`);
|
|
7199
|
+
}
|
|
7185
7200
|
const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
|
|
7186
7201
|
if (type === "route_inventory" && !expectedRoutes?.length) {
|
|
7187
7202
|
throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
|
|
@@ -7192,6 +7207,7 @@ function normalizeCheck(input, index) {
|
|
|
7192
7207
|
expected_path: stringValue2(input.expected_path),
|
|
7193
7208
|
expected_routes: expectedRoutes,
|
|
7194
7209
|
selector: stringValue2(input.selector),
|
|
7210
|
+
expected_texts: expectedTexts,
|
|
7195
7211
|
link_selector: stringValue2(input.link_selector) || stringValue2(input.linkSelector),
|
|
7196
7212
|
source_selector: stringValue2(input.source_selector) || stringValue2(input.sourceSelector),
|
|
7197
7213
|
route_path_prefix: stringValue2(input.route_path_prefix) || stringValue2(input.routePathPrefix),
|
|
@@ -7207,6 +7223,7 @@ function normalizeCheck(input, index) {
|
|
|
7207
7223
|
timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
|
|
7208
7224
|
run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
|
|
7209
7225
|
run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
|
|
7226
|
+
run_all_viewports: input.run_all_viewports === true || input.runAllViewports === true,
|
|
7210
7227
|
require_unique_routes: input.require_unique_routes === false || input.requireUniqueRoutes === false ? false : true,
|
|
7211
7228
|
allow_unexpected_routes: input.allow_unexpected_routes === true || input.allowUnexpectedRoutes === true,
|
|
7212
7229
|
save_route_screenshots: input.save_route_screenshots === true || input.saveRouteScreenshots === true
|
|
@@ -7297,6 +7314,47 @@ function selectorKey(check) {
|
|
|
7297
7314
|
function textKey(check) {
|
|
7298
7315
|
return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
|
|
7299
7316
|
}
|
|
7317
|
+
function textSequenceForCheck(viewport, check) {
|
|
7318
|
+
const key = selectorKey(check);
|
|
7319
|
+
const sequence = viewport.text_sequences?.[key];
|
|
7320
|
+
if (isRecord(sequence)) {
|
|
7321
|
+
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
7322
|
+
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
7323
|
+
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
7324
|
+
return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
7325
|
+
}
|
|
7326
|
+
return [];
|
|
7327
|
+
}
|
|
7328
|
+
function textOrderMatch(texts, expectedTexts) {
|
|
7329
|
+
const positions = [];
|
|
7330
|
+
let startAt = 0;
|
|
7331
|
+
for (const expected of expectedTexts) {
|
|
7332
|
+
const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
|
|
7333
|
+
if (index < 0) return { matched: false, positions };
|
|
7334
|
+
positions.push(index);
|
|
7335
|
+
startAt = index + 1;
|
|
7336
|
+
}
|
|
7337
|
+
return { matched: true, positions };
|
|
7338
|
+
}
|
|
7339
|
+
function summarizeRouteInventory(viewport, inventory) {
|
|
7340
|
+
const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
|
|
7341
|
+
const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
|
|
7342
|
+
const sourceLinkCount = numberValue(inventory.source_link_count) ?? numberValue(inventory.home_game_link_count) ?? null;
|
|
7343
|
+
const sourceUniqueLinkCount = numberValue(inventory.source_unique_link_count) ?? numberValue(inventory.home_unique_game_link_count) ?? null;
|
|
7344
|
+
const duplicateSourceLinks = Array.isArray(inventory.duplicate_source_link_paths) ? inventory.duplicate_source_link_paths.map((path7) => String(path7)) : [];
|
|
7345
|
+
const duplicateSourceLinkCount = numberValue(inventory.duplicate_source_link_count) ?? (sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null);
|
|
7346
|
+
const failures = Array.isArray(inventory.failures) ? inventory.failures : [];
|
|
7347
|
+
return {
|
|
7348
|
+
viewport,
|
|
7349
|
+
source_link_count: sourceLinkCount,
|
|
7350
|
+
source_unique_link_count: sourceUniqueLinkCount,
|
|
7351
|
+
duplicate_source_link_count: duplicateSourceLinkCount,
|
|
7352
|
+
duplicate_source_links: duplicateSourceLinks,
|
|
7353
|
+
direct_route_count: directRoutes.length,
|
|
7354
|
+
clickthrough_count: clickthroughs.length,
|
|
7355
|
+
failure_count: failures.length
|
|
7356
|
+
};
|
|
7357
|
+
}
|
|
7300
7358
|
function matchText(sample, check) {
|
|
7301
7359
|
if (check.pattern) {
|
|
7302
7360
|
try {
|
|
@@ -7416,6 +7474,32 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
7416
7474
|
message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
|
|
7417
7475
|
};
|
|
7418
7476
|
}
|
|
7477
|
+
if (check.type === "selector_text_order") {
|
|
7478
|
+
const key = selectorKey(check);
|
|
7479
|
+
const expectedTexts = check.expected_texts || [];
|
|
7480
|
+
const results = viewports.map((viewport) => {
|
|
7481
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
7482
|
+
const match = textOrderMatch(texts, expectedTexts);
|
|
7483
|
+
return {
|
|
7484
|
+
viewport: viewport.name,
|
|
7485
|
+
matched: match.matched,
|
|
7486
|
+
matched_positions: match.positions,
|
|
7487
|
+
visible_texts: texts.slice(0, 20)
|
|
7488
|
+
};
|
|
7489
|
+
});
|
|
7490
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
7491
|
+
return {
|
|
7492
|
+
type: check.type,
|
|
7493
|
+
label: checkLabel(check),
|
|
7494
|
+
status: failed ? "failed" : "passed",
|
|
7495
|
+
evidence: {
|
|
7496
|
+
selector: key,
|
|
7497
|
+
expected_texts: expectedTexts,
|
|
7498
|
+
viewports: results.map((result) => toJsonValue(result))
|
|
7499
|
+
},
|
|
7500
|
+
message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
|
|
7501
|
+
};
|
|
7502
|
+
}
|
|
7419
7503
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
7420
7504
|
const key = textKey(check);
|
|
7421
7505
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -7447,6 +7531,7 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
7447
7531
|
message: "No route inventory evidence was captured."
|
|
7448
7532
|
};
|
|
7449
7533
|
}
|
|
7534
|
+
const viewportSummaries = inventories.map((item) => summarizeRouteInventory(item.viewport, item.inventory));
|
|
7450
7535
|
const failures = inventories.flatMap((item) => Array.isArray(item.inventory?.failures) ? item.inventory.failures.map((failure) => toJsonValue({ viewport: item.viewport, failure })) : []);
|
|
7451
7536
|
const first = inventories[0]?.inventory;
|
|
7452
7537
|
const directRoutes = Array.isArray(first?.direct_routes) ? first.direct_routes : [];
|
|
@@ -7470,6 +7555,8 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
7470
7555
|
homepage_unique_link_count: numberValue(first?.home_unique_game_link_count) ?? sourceUniqueLinkCount,
|
|
7471
7556
|
direct_route_count: directRoutes.length,
|
|
7472
7557
|
clickthrough_count: clickthroughs.length,
|
|
7558
|
+
viewport_count: inventories.length,
|
|
7559
|
+
viewports: viewportSummaries.map((summary) => toJsonValue(summary)),
|
|
7473
7560
|
failures
|
|
7474
7561
|
},
|
|
7475
7562
|
message: failures.length ? `Route inventory failed with ${failures.length} issue(s).` : void 0
|
|
@@ -7750,6 +7837,47 @@ function textMatches(sample, check) {
|
|
|
7750
7837
|
}
|
|
7751
7838
|
return String(sample || "").includes(check.text || "");
|
|
7752
7839
|
}
|
|
7840
|
+
function textSequenceForCheck(viewport, check) {
|
|
7841
|
+
const key = check.selector || "";
|
|
7842
|
+
const sequence = viewport.text_sequences && viewport.text_sequences[key];
|
|
7843
|
+
if (sequence && typeof sequence === "object") {
|
|
7844
|
+
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
7845
|
+
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
7846
|
+
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
7847
|
+
return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
7848
|
+
}
|
|
7849
|
+
return [];
|
|
7850
|
+
}
|
|
7851
|
+
function textOrderMatch(texts, expectedTexts) {
|
|
7852
|
+
const positions = [];
|
|
7853
|
+
let startAt = 0;
|
|
7854
|
+
for (const expected of expectedTexts || []) {
|
|
7855
|
+
const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
|
|
7856
|
+
if (index < 0) return { matched: false, positions };
|
|
7857
|
+
positions.push(index);
|
|
7858
|
+
startAt = index + 1;
|
|
7859
|
+
}
|
|
7860
|
+
return { matched: true, positions };
|
|
7861
|
+
}
|
|
7862
|
+
function summarizeRouteInventory(viewport, inventory) {
|
|
7863
|
+
const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
|
|
7864
|
+
const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
|
|
7865
|
+
const sourceLinkCount = typeof inventory.source_link_count === "number" ? inventory.source_link_count : typeof inventory.home_game_link_count === "number" ? inventory.home_game_link_count : null;
|
|
7866
|
+
const sourceUniqueLinkCount = typeof inventory.source_unique_link_count === "number" ? inventory.source_unique_link_count : typeof inventory.home_unique_game_link_count === "number" ? inventory.home_unique_game_link_count : null;
|
|
7867
|
+
const duplicateSourceLinks = Array.isArray(inventory.duplicate_source_link_paths) ? inventory.duplicate_source_link_paths.map((path) => String(path)) : [];
|
|
7868
|
+
const duplicateSourceLinkCount = typeof inventory.duplicate_source_link_count === "number" ? inventory.duplicate_source_link_count : sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null;
|
|
7869
|
+
const failures = Array.isArray(inventory.failures) ? inventory.failures : [];
|
|
7870
|
+
return {
|
|
7871
|
+
viewport,
|
|
7872
|
+
source_link_count: sourceLinkCount,
|
|
7873
|
+
source_unique_link_count: sourceUniqueLinkCount,
|
|
7874
|
+
duplicate_source_link_count: duplicateSourceLinkCount,
|
|
7875
|
+
duplicate_source_links: duplicateSourceLinks,
|
|
7876
|
+
direct_route_count: directRoutes.length,
|
|
7877
|
+
clickthrough_count: clickthroughs.length,
|
|
7878
|
+
failure_count: failures.length,
|
|
7879
|
+
};
|
|
7880
|
+
}
|
|
7753
7881
|
function numberValue(value) {
|
|
7754
7882
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
7755
7883
|
}
|
|
@@ -7954,6 +8082,29 @@ function assessProfile(profile, evidence) {
|
|
|
7954
8082
|
});
|
|
7955
8083
|
continue;
|
|
7956
8084
|
}
|
|
8085
|
+
if (check.type === "selector_text_order") {
|
|
8086
|
+
const selector = check.selector || "";
|
|
8087
|
+
const expectedTexts = check.expected_texts || [];
|
|
8088
|
+
const results = viewports.map((viewport) => {
|
|
8089
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
8090
|
+
const match = textOrderMatch(texts, expectedTexts);
|
|
8091
|
+
return {
|
|
8092
|
+
viewport: viewport.name,
|
|
8093
|
+
matched: match.matched,
|
|
8094
|
+
matched_positions: match.positions,
|
|
8095
|
+
visible_texts: texts.slice(0, 20),
|
|
8096
|
+
};
|
|
8097
|
+
});
|
|
8098
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
8099
|
+
checks.push({
|
|
8100
|
+
type: check.type,
|
|
8101
|
+
label: check.label || check.type,
|
|
8102
|
+
status: failed ? "failed" : "passed",
|
|
8103
|
+
evidence: { selector, expected_texts: expectedTexts, viewports: results },
|
|
8104
|
+
message: failed ? "Selector " + selector + " text order failed in " + failed + " viewport(s)." : undefined,
|
|
8105
|
+
});
|
|
8106
|
+
continue;
|
|
8107
|
+
}
|
|
7957
8108
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
7958
8109
|
const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
|
|
7959
8110
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -7982,6 +8133,7 @@ function assessProfile(profile, evidence) {
|
|
|
7982
8133
|
});
|
|
7983
8134
|
continue;
|
|
7984
8135
|
}
|
|
8136
|
+
const viewportSummaries = inventories.map((item) => summarizeRouteInventory(item.viewport, item.inventory || {}));
|
|
7985
8137
|
const failures = [];
|
|
7986
8138
|
for (const item of inventories) {
|
|
7987
8139
|
for (const failure of Array.isArray(item.inventory.failures) ? item.inventory.failures : []) {
|
|
@@ -8010,6 +8162,8 @@ function assessProfile(profile, evidence) {
|
|
|
8010
8162
|
homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : sourceUniqueLinkCount,
|
|
8011
8163
|
direct_route_count: directRoutes.length,
|
|
8012
8164
|
clickthrough_count: clickthroughs.length,
|
|
8165
|
+
viewport_count: inventories.length,
|
|
8166
|
+
viewports: viewportSummaries,
|
|
8013
8167
|
failures,
|
|
8014
8168
|
},
|
|
8015
8169
|
message: failures.length ? "Route inventory failed with " + failures.length + " issue(s)." : undefined,
|
|
@@ -8327,6 +8481,27 @@ async function selectorStats(selector) {
|
|
|
8327
8481
|
return { count: elements.length, visible_count: elements.filter(isVisible).length };
|
|
8328
8482
|
}).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
8329
8483
|
}
|
|
8484
|
+
async function selectorTextSequence(selector) {
|
|
8485
|
+
return page.locator(selector).evaluateAll((elements) => {
|
|
8486
|
+
const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
8487
|
+
const isVisible = (element) => {
|
|
8488
|
+
const style = window.getComputedStyle(element);
|
|
8489
|
+
const rect = element.getBoundingClientRect();
|
|
8490
|
+
return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
|
|
8491
|
+
};
|
|
8492
|
+
const rows = elements.map((element, index) => ({
|
|
8493
|
+
index,
|
|
8494
|
+
text: compact(element.innerText || element.textContent || ""),
|
|
8495
|
+
visible: isVisible(element),
|
|
8496
|
+
}));
|
|
8497
|
+
return {
|
|
8498
|
+
count: rows.length,
|
|
8499
|
+
visible_count: rows.filter((row) => row.visible).length,
|
|
8500
|
+
texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
8501
|
+
visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
8502
|
+
};
|
|
8503
|
+
}).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
8504
|
+
}
|
|
8330
8505
|
function inventoryAppPathFromUrl(urlLike) {
|
|
8331
8506
|
const url = new URL(String(urlLike), targetUrl);
|
|
8332
8507
|
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
@@ -8724,11 +8899,16 @@ async function captureViewport(viewport) {
|
|
|
8724
8899
|
evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
|
|
8725
8900
|
}));
|
|
8726
8901
|
const selectors = {};
|
|
8902
|
+
const text_sequences = {};
|
|
8727
8903
|
const text_matches = {};
|
|
8728
8904
|
for (const check of profile.checks || []) {
|
|
8729
8905
|
if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
|
|
8730
8906
|
selectors[check.selector] = await selectorStats(check.selector);
|
|
8731
8907
|
}
|
|
8908
|
+
if (check.type === "selector_text_order" && check.selector) {
|
|
8909
|
+
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
8910
|
+
text_sequences[check.selector] = await selectorTextSequence(check.selector);
|
|
8911
|
+
}
|
|
8732
8912
|
if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
|
|
8733
8913
|
text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
|
|
8734
8914
|
}
|
|
@@ -8742,7 +8922,7 @@ async function captureViewport(viewport) {
|
|
|
8742
8922
|
let routeInventory;
|
|
8743
8923
|
const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
|
|
8744
8924
|
const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
|
|
8745
|
-
if (routeInventoryCheck && (!firstViewportName || viewport.name === firstViewportName)) {
|
|
8925
|
+
if (routeInventoryCheck && (routeInventoryCheck.run_all_viewports || !firstViewportName || viewport.name === firstViewportName)) {
|
|
8746
8926
|
try {
|
|
8747
8927
|
routeInventory = await collectRouteInventory(routeInventoryCheck, viewport);
|
|
8748
8928
|
} catch (error) {
|
|
@@ -8792,6 +8972,7 @@ async function captureViewport(viewport) {
|
|
|
8792
8972
|
bounds_overflow_px: dom.bounds_overflow_px,
|
|
8793
8973
|
overflow_offenders: dom.overflow_offenders || [],
|
|
8794
8974
|
selectors,
|
|
8975
|
+
text_sequences,
|
|
8795
8976
|
text_matches,
|
|
8796
8977
|
route_inventory: routeInventory,
|
|
8797
8978
|
setup_action_results: setupActionResults,
|
package/dist/cli.js
CHANGED