@riddledc/riddle-proof 0.7.34 → 0.7.35
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 +15 -0
- package/dist/{chunk-PWGJQLOS.js → chunk-VCINQE5O.js} +136 -0
- package/dist/cli.cjs +136 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +136 -0
- package/dist/index.js +1 -1
- package/dist/profile.cjs +136 -0
- package/dist/profile.d.cts +3 -1
- package/dist/profile.d.ts +3 -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:
|
|
@@ -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),
|
|
@@ -424,6 +440,28 @@ function selectorKey(check) {
|
|
|
424
440
|
function textKey(check) {
|
|
425
441
|
return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
|
|
426
442
|
}
|
|
443
|
+
function textSequenceForCheck(viewport, check) {
|
|
444
|
+
const key = selectorKey(check);
|
|
445
|
+
const sequence = viewport.text_sequences?.[key];
|
|
446
|
+
if (isRecord(sequence)) {
|
|
447
|
+
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
448
|
+
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
449
|
+
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
450
|
+
return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
451
|
+
}
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
function textOrderMatch(texts, expectedTexts) {
|
|
455
|
+
const positions = [];
|
|
456
|
+
let startAt = 0;
|
|
457
|
+
for (const expected of expectedTexts) {
|
|
458
|
+
const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
|
|
459
|
+
if (index < 0) return { matched: false, positions };
|
|
460
|
+
positions.push(index);
|
|
461
|
+
startAt = index + 1;
|
|
462
|
+
}
|
|
463
|
+
return { matched: true, positions };
|
|
464
|
+
}
|
|
427
465
|
function matchText(sample, check) {
|
|
428
466
|
if (check.pattern) {
|
|
429
467
|
try {
|
|
@@ -543,6 +581,32 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
543
581
|
message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
|
|
544
582
|
};
|
|
545
583
|
}
|
|
584
|
+
if (check.type === "selector_text_order") {
|
|
585
|
+
const key = selectorKey(check);
|
|
586
|
+
const expectedTexts = check.expected_texts || [];
|
|
587
|
+
const results = viewports.map((viewport) => {
|
|
588
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
589
|
+
const match = textOrderMatch(texts, expectedTexts);
|
|
590
|
+
return {
|
|
591
|
+
viewport: viewport.name,
|
|
592
|
+
matched: match.matched,
|
|
593
|
+
matched_positions: match.positions,
|
|
594
|
+
visible_texts: texts.slice(0, 20)
|
|
595
|
+
};
|
|
596
|
+
});
|
|
597
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
598
|
+
return {
|
|
599
|
+
type: check.type,
|
|
600
|
+
label: checkLabel(check),
|
|
601
|
+
status: failed ? "failed" : "passed",
|
|
602
|
+
evidence: {
|
|
603
|
+
selector: key,
|
|
604
|
+
expected_texts: expectedTexts,
|
|
605
|
+
viewports: results.map((result) => toJsonValue(result))
|
|
606
|
+
},
|
|
607
|
+
message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
|
|
608
|
+
};
|
|
609
|
+
}
|
|
546
610
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
547
611
|
const key = textKey(check);
|
|
548
612
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -893,6 +957,28 @@ function textMatches(sample, check) {
|
|
|
893
957
|
}
|
|
894
958
|
return String(sample || "").includes(check.text || "");
|
|
895
959
|
}
|
|
960
|
+
function textSequenceForCheck(viewport, check) {
|
|
961
|
+
const key = check.selector || "";
|
|
962
|
+
const sequence = viewport.text_sequences && viewport.text_sequences[key];
|
|
963
|
+
if (sequence && typeof sequence === "object") {
|
|
964
|
+
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
965
|
+
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
966
|
+
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
967
|
+
return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
968
|
+
}
|
|
969
|
+
return [];
|
|
970
|
+
}
|
|
971
|
+
function textOrderMatch(texts, expectedTexts) {
|
|
972
|
+
const positions = [];
|
|
973
|
+
let startAt = 0;
|
|
974
|
+
for (const expected of expectedTexts || []) {
|
|
975
|
+
const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
|
|
976
|
+
if (index < 0) return { matched: false, positions };
|
|
977
|
+
positions.push(index);
|
|
978
|
+
startAt = index + 1;
|
|
979
|
+
}
|
|
980
|
+
return { matched: true, positions };
|
|
981
|
+
}
|
|
896
982
|
function numberValue(value) {
|
|
897
983
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
898
984
|
}
|
|
@@ -1097,6 +1183,29 @@ function assessProfile(profile, evidence) {
|
|
|
1097
1183
|
});
|
|
1098
1184
|
continue;
|
|
1099
1185
|
}
|
|
1186
|
+
if (check.type === "selector_text_order") {
|
|
1187
|
+
const selector = check.selector || "";
|
|
1188
|
+
const expectedTexts = check.expected_texts || [];
|
|
1189
|
+
const results = viewports.map((viewport) => {
|
|
1190
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
1191
|
+
const match = textOrderMatch(texts, expectedTexts);
|
|
1192
|
+
return {
|
|
1193
|
+
viewport: viewport.name,
|
|
1194
|
+
matched: match.matched,
|
|
1195
|
+
matched_positions: match.positions,
|
|
1196
|
+
visible_texts: texts.slice(0, 20),
|
|
1197
|
+
};
|
|
1198
|
+
});
|
|
1199
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
1200
|
+
checks.push({
|
|
1201
|
+
type: check.type,
|
|
1202
|
+
label: check.label || check.type,
|
|
1203
|
+
status: failed ? "failed" : "passed",
|
|
1204
|
+
evidence: { selector, expected_texts: expectedTexts, viewports: results },
|
|
1205
|
+
message: failed ? "Selector " + selector + " text order failed in " + failed + " viewport(s)." : undefined,
|
|
1206
|
+
});
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1100
1209
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
1101
1210
|
const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
|
|
1102
1211
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -1470,6 +1579,27 @@ async function selectorStats(selector) {
|
|
|
1470
1579
|
return { count: elements.length, visible_count: elements.filter(isVisible).length };
|
|
1471
1580
|
}).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
1472
1581
|
}
|
|
1582
|
+
async function selectorTextSequence(selector) {
|
|
1583
|
+
return page.locator(selector).evaluateAll((elements) => {
|
|
1584
|
+
const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
1585
|
+
const isVisible = (element) => {
|
|
1586
|
+
const style = window.getComputedStyle(element);
|
|
1587
|
+
const rect = element.getBoundingClientRect();
|
|
1588
|
+
return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
|
|
1589
|
+
};
|
|
1590
|
+
const rows = elements.map((element, index) => ({
|
|
1591
|
+
index,
|
|
1592
|
+
text: compact(element.innerText || element.textContent || ""),
|
|
1593
|
+
visible: isVisible(element),
|
|
1594
|
+
}));
|
|
1595
|
+
return {
|
|
1596
|
+
count: rows.length,
|
|
1597
|
+
visible_count: rows.filter((row) => row.visible).length,
|
|
1598
|
+
texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
1599
|
+
visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
1600
|
+
};
|
|
1601
|
+
}).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
1602
|
+
}
|
|
1473
1603
|
function inventoryAppPathFromUrl(urlLike) {
|
|
1474
1604
|
const url = new URL(String(urlLike), targetUrl);
|
|
1475
1605
|
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
@@ -1867,11 +1997,16 @@ async function captureViewport(viewport) {
|
|
|
1867
1997
|
evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
|
|
1868
1998
|
}));
|
|
1869
1999
|
const selectors = {};
|
|
2000
|
+
const text_sequences = {};
|
|
1870
2001
|
const text_matches = {};
|
|
1871
2002
|
for (const check of profile.checks || []) {
|
|
1872
2003
|
if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
|
|
1873
2004
|
selectors[check.selector] = await selectorStats(check.selector);
|
|
1874
2005
|
}
|
|
2006
|
+
if (check.type === "selector_text_order" && check.selector) {
|
|
2007
|
+
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
2008
|
+
text_sequences[check.selector] = await selectorTextSequence(check.selector);
|
|
2009
|
+
}
|
|
1875
2010
|
if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
|
|
1876
2011
|
text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
|
|
1877
2012
|
}
|
|
@@ -1935,6 +2070,7 @@ async function captureViewport(viewport) {
|
|
|
1935
2070
|
bounds_overflow_px: dom.bounds_overflow_px,
|
|
1936
2071
|
overflow_offenders: dom.overflow_offenders || [],
|
|
1937
2072
|
selectors,
|
|
2073
|
+
text_sequences,
|
|
1938
2074
|
text_matches,
|
|
1939
2075
|
route_inventory: routeInventory,
|
|
1940
2076
|
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),
|
|
@@ -7297,6 +7313,28 @@ function selectorKey(check) {
|
|
|
7297
7313
|
function textKey(check) {
|
|
7298
7314
|
return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
|
|
7299
7315
|
}
|
|
7316
|
+
function textSequenceForCheck(viewport, check) {
|
|
7317
|
+
const key = selectorKey(check);
|
|
7318
|
+
const sequence = viewport.text_sequences?.[key];
|
|
7319
|
+
if (isRecord(sequence)) {
|
|
7320
|
+
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
7321
|
+
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
7322
|
+
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
7323
|
+
return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
7324
|
+
}
|
|
7325
|
+
return [];
|
|
7326
|
+
}
|
|
7327
|
+
function textOrderMatch(texts, expectedTexts) {
|
|
7328
|
+
const positions = [];
|
|
7329
|
+
let startAt = 0;
|
|
7330
|
+
for (const expected of expectedTexts) {
|
|
7331
|
+
const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
|
|
7332
|
+
if (index < 0) return { matched: false, positions };
|
|
7333
|
+
positions.push(index);
|
|
7334
|
+
startAt = index + 1;
|
|
7335
|
+
}
|
|
7336
|
+
return { matched: true, positions };
|
|
7337
|
+
}
|
|
7300
7338
|
function matchText(sample, check) {
|
|
7301
7339
|
if (check.pattern) {
|
|
7302
7340
|
try {
|
|
@@ -7416,6 +7454,32 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
7416
7454
|
message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
|
|
7417
7455
|
};
|
|
7418
7456
|
}
|
|
7457
|
+
if (check.type === "selector_text_order") {
|
|
7458
|
+
const key = selectorKey(check);
|
|
7459
|
+
const expectedTexts = check.expected_texts || [];
|
|
7460
|
+
const results = viewports.map((viewport) => {
|
|
7461
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
7462
|
+
const match = textOrderMatch(texts, expectedTexts);
|
|
7463
|
+
return {
|
|
7464
|
+
viewport: viewport.name,
|
|
7465
|
+
matched: match.matched,
|
|
7466
|
+
matched_positions: match.positions,
|
|
7467
|
+
visible_texts: texts.slice(0, 20)
|
|
7468
|
+
};
|
|
7469
|
+
});
|
|
7470
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
7471
|
+
return {
|
|
7472
|
+
type: check.type,
|
|
7473
|
+
label: checkLabel(check),
|
|
7474
|
+
status: failed ? "failed" : "passed",
|
|
7475
|
+
evidence: {
|
|
7476
|
+
selector: key,
|
|
7477
|
+
expected_texts: expectedTexts,
|
|
7478
|
+
viewports: results.map((result) => toJsonValue(result))
|
|
7479
|
+
},
|
|
7480
|
+
message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
|
|
7481
|
+
};
|
|
7482
|
+
}
|
|
7419
7483
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
7420
7484
|
const key = textKey(check);
|
|
7421
7485
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -7750,6 +7814,28 @@ function textMatches(sample, check) {
|
|
|
7750
7814
|
}
|
|
7751
7815
|
return String(sample || "").includes(check.text || "");
|
|
7752
7816
|
}
|
|
7817
|
+
function textSequenceForCheck(viewport, check) {
|
|
7818
|
+
const key = check.selector || "";
|
|
7819
|
+
const sequence = viewport.text_sequences && viewport.text_sequences[key];
|
|
7820
|
+
if (sequence && typeof sequence === "object") {
|
|
7821
|
+
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
7822
|
+
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
7823
|
+
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
7824
|
+
return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
7825
|
+
}
|
|
7826
|
+
return [];
|
|
7827
|
+
}
|
|
7828
|
+
function textOrderMatch(texts, expectedTexts) {
|
|
7829
|
+
const positions = [];
|
|
7830
|
+
let startAt = 0;
|
|
7831
|
+
for (const expected of expectedTexts || []) {
|
|
7832
|
+
const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
|
|
7833
|
+
if (index < 0) return { matched: false, positions };
|
|
7834
|
+
positions.push(index);
|
|
7835
|
+
startAt = index + 1;
|
|
7836
|
+
}
|
|
7837
|
+
return { matched: true, positions };
|
|
7838
|
+
}
|
|
7753
7839
|
function numberValue(value) {
|
|
7754
7840
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
7755
7841
|
}
|
|
@@ -7954,6 +8040,29 @@ function assessProfile(profile, evidence) {
|
|
|
7954
8040
|
});
|
|
7955
8041
|
continue;
|
|
7956
8042
|
}
|
|
8043
|
+
if (check.type === "selector_text_order") {
|
|
8044
|
+
const selector = check.selector || "";
|
|
8045
|
+
const expectedTexts = check.expected_texts || [];
|
|
8046
|
+
const results = viewports.map((viewport) => {
|
|
8047
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
8048
|
+
const match = textOrderMatch(texts, expectedTexts);
|
|
8049
|
+
return {
|
|
8050
|
+
viewport: viewport.name,
|
|
8051
|
+
matched: match.matched,
|
|
8052
|
+
matched_positions: match.positions,
|
|
8053
|
+
visible_texts: texts.slice(0, 20),
|
|
8054
|
+
};
|
|
8055
|
+
});
|
|
8056
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
8057
|
+
checks.push({
|
|
8058
|
+
type: check.type,
|
|
8059
|
+
label: check.label || check.type,
|
|
8060
|
+
status: failed ? "failed" : "passed",
|
|
8061
|
+
evidence: { selector, expected_texts: expectedTexts, viewports: results },
|
|
8062
|
+
message: failed ? "Selector " + selector + " text order failed in " + failed + " viewport(s)." : undefined,
|
|
8063
|
+
});
|
|
8064
|
+
continue;
|
|
8065
|
+
}
|
|
7957
8066
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
7958
8067
|
const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
|
|
7959
8068
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -8327,6 +8436,27 @@ async function selectorStats(selector) {
|
|
|
8327
8436
|
return { count: elements.length, visible_count: elements.filter(isVisible).length };
|
|
8328
8437
|
}).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
8329
8438
|
}
|
|
8439
|
+
async function selectorTextSequence(selector) {
|
|
8440
|
+
return page.locator(selector).evaluateAll((elements) => {
|
|
8441
|
+
const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
8442
|
+
const isVisible = (element) => {
|
|
8443
|
+
const style = window.getComputedStyle(element);
|
|
8444
|
+
const rect = element.getBoundingClientRect();
|
|
8445
|
+
return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
|
|
8446
|
+
};
|
|
8447
|
+
const rows = elements.map((element, index) => ({
|
|
8448
|
+
index,
|
|
8449
|
+
text: compact(element.innerText || element.textContent || ""),
|
|
8450
|
+
visible: isVisible(element),
|
|
8451
|
+
}));
|
|
8452
|
+
return {
|
|
8453
|
+
count: rows.length,
|
|
8454
|
+
visible_count: rows.filter((row) => row.visible).length,
|
|
8455
|
+
texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
8456
|
+
visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
8457
|
+
};
|
|
8458
|
+
}).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
8459
|
+
}
|
|
8330
8460
|
function inventoryAppPathFromUrl(urlLike) {
|
|
8331
8461
|
const url = new URL(String(urlLike), targetUrl);
|
|
8332
8462
|
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
@@ -8724,11 +8854,16 @@ async function captureViewport(viewport) {
|
|
|
8724
8854
|
evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
|
|
8725
8855
|
}));
|
|
8726
8856
|
const selectors = {};
|
|
8857
|
+
const text_sequences = {};
|
|
8727
8858
|
const text_matches = {};
|
|
8728
8859
|
for (const check of profile.checks || []) {
|
|
8729
8860
|
if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
|
|
8730
8861
|
selectors[check.selector] = await selectorStats(check.selector);
|
|
8731
8862
|
}
|
|
8863
|
+
if (check.type === "selector_text_order" && check.selector) {
|
|
8864
|
+
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
8865
|
+
text_sequences[check.selector] = await selectorTextSequence(check.selector);
|
|
8866
|
+
}
|
|
8732
8867
|
if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
|
|
8733
8868
|
text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
|
|
8734
8869
|
}
|
|
@@ -8792,6 +8927,7 @@ async function captureViewport(viewport) {
|
|
|
8792
8927
|
bounds_overflow_px: dom.bounds_overflow_px,
|
|
8793
8928
|
overflow_offenders: dom.overflow_offenders || [],
|
|
8794
8929
|
selectors,
|
|
8930
|
+
text_sequences,
|
|
8795
8931
|
text_matches,
|
|
8796
8932
|
route_inventory: routeInventory,
|
|
8797
8933
|
setup_action_results: setupActionResults,
|
package/dist/cli.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -8728,6 +8728,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
|
|
|
8728
8728
|
"route_loaded",
|
|
8729
8729
|
"selector_visible",
|
|
8730
8730
|
"selector_count_at_least",
|
|
8731
|
+
"selector_text_order",
|
|
8731
8732
|
"text_visible",
|
|
8732
8733
|
"text_absent",
|
|
8733
8734
|
"route_inventory",
|
|
@@ -9007,6 +9008,15 @@ function normalizeRouteInventoryRoutes(value, index) {
|
|
|
9007
9008
|
}
|
|
9008
9009
|
return routes;
|
|
9009
9010
|
}
|
|
9011
|
+
function normalizeExpectedTexts(value, index) {
|
|
9012
|
+
if (value === void 0) return void 0;
|
|
9013
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
9014
|
+
throw new Error(`checks[${index}] selector_text_order expected_texts must be a non-empty array.`);
|
|
9015
|
+
}
|
|
9016
|
+
const texts = value.map((item) => String(item).replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
9017
|
+
if (!texts.length) throw new Error(`checks[${index}] selector_text_order expected_texts must contain non-empty strings.`);
|
|
9018
|
+
return texts;
|
|
9019
|
+
}
|
|
9010
9020
|
function normalizeCheck(input, index) {
|
|
9011
9021
|
if (!isRecord2(input)) throw new Error(`checks[${index}] must be an object.`);
|
|
9012
9022
|
const type = stringValue5(input.type);
|
|
@@ -9023,6 +9033,11 @@ function normalizeCheck(input, index) {
|
|
|
9023
9033
|
if (type === "selector_count_at_least" && numberValue3(input.min_count) === void 0) {
|
|
9024
9034
|
throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
|
|
9025
9035
|
}
|
|
9036
|
+
const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
|
|
9037
|
+
if (type === "selector_text_order") {
|
|
9038
|
+
if (!stringValue5(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
|
|
9039
|
+
if (!expectedTexts?.length) throw new Error(`checks[${index}] selector_text_order requires expected_texts.`);
|
|
9040
|
+
}
|
|
9026
9041
|
const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
|
|
9027
9042
|
if (type === "route_inventory" && !expectedRoutes?.length) {
|
|
9028
9043
|
throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
|
|
@@ -9033,6 +9048,7 @@ function normalizeCheck(input, index) {
|
|
|
9033
9048
|
expected_path: stringValue5(input.expected_path),
|
|
9034
9049
|
expected_routes: expectedRoutes,
|
|
9035
9050
|
selector: stringValue5(input.selector),
|
|
9051
|
+
expected_texts: expectedTexts,
|
|
9036
9052
|
link_selector: stringValue5(input.link_selector) || stringValue5(input.linkSelector),
|
|
9037
9053
|
source_selector: stringValue5(input.source_selector) || stringValue5(input.sourceSelector),
|
|
9038
9054
|
route_path_prefix: stringValue5(input.route_path_prefix) || stringValue5(input.routePathPrefix),
|
|
@@ -9138,6 +9154,28 @@ function selectorKey(check) {
|
|
|
9138
9154
|
function textKey(check) {
|
|
9139
9155
|
return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
|
|
9140
9156
|
}
|
|
9157
|
+
function textSequenceForCheck(viewport, check) {
|
|
9158
|
+
const key = selectorKey(check);
|
|
9159
|
+
const sequence = viewport.text_sequences?.[key];
|
|
9160
|
+
if (isRecord2(sequence)) {
|
|
9161
|
+
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
9162
|
+
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
9163
|
+
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
9164
|
+
return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
9165
|
+
}
|
|
9166
|
+
return [];
|
|
9167
|
+
}
|
|
9168
|
+
function textOrderMatch(texts, expectedTexts) {
|
|
9169
|
+
const positions = [];
|
|
9170
|
+
let startAt = 0;
|
|
9171
|
+
for (const expected of expectedTexts) {
|
|
9172
|
+
const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
|
|
9173
|
+
if (index < 0) return { matched: false, positions };
|
|
9174
|
+
positions.push(index);
|
|
9175
|
+
startAt = index + 1;
|
|
9176
|
+
}
|
|
9177
|
+
return { matched: true, positions };
|
|
9178
|
+
}
|
|
9141
9179
|
function matchText(sample, check) {
|
|
9142
9180
|
if (check.pattern) {
|
|
9143
9181
|
try {
|
|
@@ -9257,6 +9295,32 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
9257
9295
|
message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
|
|
9258
9296
|
};
|
|
9259
9297
|
}
|
|
9298
|
+
if (check.type === "selector_text_order") {
|
|
9299
|
+
const key = selectorKey(check);
|
|
9300
|
+
const expectedTexts = check.expected_texts || [];
|
|
9301
|
+
const results = viewports.map((viewport) => {
|
|
9302
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
9303
|
+
const match = textOrderMatch(texts, expectedTexts);
|
|
9304
|
+
return {
|
|
9305
|
+
viewport: viewport.name,
|
|
9306
|
+
matched: match.matched,
|
|
9307
|
+
matched_positions: match.positions,
|
|
9308
|
+
visible_texts: texts.slice(0, 20)
|
|
9309
|
+
};
|
|
9310
|
+
});
|
|
9311
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
9312
|
+
return {
|
|
9313
|
+
type: check.type,
|
|
9314
|
+
label: checkLabel(check),
|
|
9315
|
+
status: failed ? "failed" : "passed",
|
|
9316
|
+
evidence: {
|
|
9317
|
+
selector: key,
|
|
9318
|
+
expected_texts: expectedTexts,
|
|
9319
|
+
viewports: results.map((result) => toJsonValue(result))
|
|
9320
|
+
},
|
|
9321
|
+
message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
|
|
9322
|
+
};
|
|
9323
|
+
}
|
|
9260
9324
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
9261
9325
|
const key = textKey(check);
|
|
9262
9326
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -9607,6 +9671,28 @@ function textMatches(sample, check) {
|
|
|
9607
9671
|
}
|
|
9608
9672
|
return String(sample || "").includes(check.text || "");
|
|
9609
9673
|
}
|
|
9674
|
+
function textSequenceForCheck(viewport, check) {
|
|
9675
|
+
const key = check.selector || "";
|
|
9676
|
+
const sequence = viewport.text_sequences && viewport.text_sequences[key];
|
|
9677
|
+
if (sequence && typeof sequence === "object") {
|
|
9678
|
+
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
9679
|
+
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
9680
|
+
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
9681
|
+
return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
9682
|
+
}
|
|
9683
|
+
return [];
|
|
9684
|
+
}
|
|
9685
|
+
function textOrderMatch(texts, expectedTexts) {
|
|
9686
|
+
const positions = [];
|
|
9687
|
+
let startAt = 0;
|
|
9688
|
+
for (const expected of expectedTexts || []) {
|
|
9689
|
+
const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
|
|
9690
|
+
if (index < 0) return { matched: false, positions };
|
|
9691
|
+
positions.push(index);
|
|
9692
|
+
startAt = index + 1;
|
|
9693
|
+
}
|
|
9694
|
+
return { matched: true, positions };
|
|
9695
|
+
}
|
|
9610
9696
|
function numberValue(value) {
|
|
9611
9697
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
9612
9698
|
}
|
|
@@ -9811,6 +9897,29 @@ function assessProfile(profile, evidence) {
|
|
|
9811
9897
|
});
|
|
9812
9898
|
continue;
|
|
9813
9899
|
}
|
|
9900
|
+
if (check.type === "selector_text_order") {
|
|
9901
|
+
const selector = check.selector || "";
|
|
9902
|
+
const expectedTexts = check.expected_texts || [];
|
|
9903
|
+
const results = viewports.map((viewport) => {
|
|
9904
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
9905
|
+
const match = textOrderMatch(texts, expectedTexts);
|
|
9906
|
+
return {
|
|
9907
|
+
viewport: viewport.name,
|
|
9908
|
+
matched: match.matched,
|
|
9909
|
+
matched_positions: match.positions,
|
|
9910
|
+
visible_texts: texts.slice(0, 20),
|
|
9911
|
+
};
|
|
9912
|
+
});
|
|
9913
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
9914
|
+
checks.push({
|
|
9915
|
+
type: check.type,
|
|
9916
|
+
label: check.label || check.type,
|
|
9917
|
+
status: failed ? "failed" : "passed",
|
|
9918
|
+
evidence: { selector, expected_texts: expectedTexts, viewports: results },
|
|
9919
|
+
message: failed ? "Selector " + selector + " text order failed in " + failed + " viewport(s)." : undefined,
|
|
9920
|
+
});
|
|
9921
|
+
continue;
|
|
9922
|
+
}
|
|
9814
9923
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
9815
9924
|
const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
|
|
9816
9925
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -10184,6 +10293,27 @@ async function selectorStats(selector) {
|
|
|
10184
10293
|
return { count: elements.length, visible_count: elements.filter(isVisible).length };
|
|
10185
10294
|
}).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
10186
10295
|
}
|
|
10296
|
+
async function selectorTextSequence(selector) {
|
|
10297
|
+
return page.locator(selector).evaluateAll((elements) => {
|
|
10298
|
+
const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
10299
|
+
const isVisible = (element) => {
|
|
10300
|
+
const style = window.getComputedStyle(element);
|
|
10301
|
+
const rect = element.getBoundingClientRect();
|
|
10302
|
+
return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
|
|
10303
|
+
};
|
|
10304
|
+
const rows = elements.map((element, index) => ({
|
|
10305
|
+
index,
|
|
10306
|
+
text: compact(element.innerText || element.textContent || ""),
|
|
10307
|
+
visible: isVisible(element),
|
|
10308
|
+
}));
|
|
10309
|
+
return {
|
|
10310
|
+
count: rows.length,
|
|
10311
|
+
visible_count: rows.filter((row) => row.visible).length,
|
|
10312
|
+
texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
10313
|
+
visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
10314
|
+
};
|
|
10315
|
+
}).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
10316
|
+
}
|
|
10187
10317
|
function inventoryAppPathFromUrl(urlLike) {
|
|
10188
10318
|
const url = new URL(String(urlLike), targetUrl);
|
|
10189
10319
|
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
@@ -10581,11 +10711,16 @@ async function captureViewport(viewport) {
|
|
|
10581
10711
|
evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
|
|
10582
10712
|
}));
|
|
10583
10713
|
const selectors = {};
|
|
10714
|
+
const text_sequences = {};
|
|
10584
10715
|
const text_matches = {};
|
|
10585
10716
|
for (const check of profile.checks || []) {
|
|
10586
10717
|
if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
|
|
10587
10718
|
selectors[check.selector] = await selectorStats(check.selector);
|
|
10588
10719
|
}
|
|
10720
|
+
if (check.type === "selector_text_order" && check.selector) {
|
|
10721
|
+
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
10722
|
+
text_sequences[check.selector] = await selectorTextSequence(check.selector);
|
|
10723
|
+
}
|
|
10589
10724
|
if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
|
|
10590
10725
|
text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
|
|
10591
10726
|
}
|
|
@@ -10649,6 +10784,7 @@ async function captureViewport(viewport) {
|
|
|
10649
10784
|
bounds_overflow_px: dom.bounds_overflow_px,
|
|
10650
10785
|
overflow_offenders: dom.overflow_offenders || [],
|
|
10651
10786
|
selectors,
|
|
10787
|
+
text_sequences,
|
|
10652
10788
|
text_matches,
|
|
10653
10789
|
route_inventory: routeInventory,
|
|
10654
10790
|
setup_action_results: setupActionResults,
|
package/dist/index.js
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
resolveRiddleProofProfileTimeoutSec,
|
|
59
59
|
slugifyRiddleProofProfileName,
|
|
60
60
|
summarizeRiddleProofProfileResult
|
|
61
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-VCINQE5O.js";
|
|
62
62
|
import {
|
|
63
63
|
DEFAULT_RIDDLE_API_BASE_URL,
|
|
64
64
|
DEFAULT_RIDDLE_API_KEY_FILE,
|
package/dist/profile.cjs
CHANGED
|
@@ -57,6 +57,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
|
|
|
57
57
|
"route_loaded",
|
|
58
58
|
"selector_visible",
|
|
59
59
|
"selector_count_at_least",
|
|
60
|
+
"selector_text_order",
|
|
60
61
|
"text_visible",
|
|
61
62
|
"text_absent",
|
|
62
63
|
"route_inventory",
|
|
@@ -336,6 +337,15 @@ function normalizeRouteInventoryRoutes(value, index) {
|
|
|
336
337
|
}
|
|
337
338
|
return routes;
|
|
338
339
|
}
|
|
340
|
+
function normalizeExpectedTexts(value, index) {
|
|
341
|
+
if (value === void 0) return void 0;
|
|
342
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
343
|
+
throw new Error(`checks[${index}] selector_text_order expected_texts must be a non-empty array.`);
|
|
344
|
+
}
|
|
345
|
+
const texts = value.map((item) => String(item).replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
346
|
+
if (!texts.length) throw new Error(`checks[${index}] selector_text_order expected_texts must contain non-empty strings.`);
|
|
347
|
+
return texts;
|
|
348
|
+
}
|
|
339
349
|
function normalizeCheck(input, index) {
|
|
340
350
|
if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
|
|
341
351
|
const type = stringValue(input.type);
|
|
@@ -352,6 +362,11 @@ function normalizeCheck(input, index) {
|
|
|
352
362
|
if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
|
|
353
363
|
throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
|
|
354
364
|
}
|
|
365
|
+
const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
|
|
366
|
+
if (type === "selector_text_order") {
|
|
367
|
+
if (!stringValue(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
|
|
368
|
+
if (!expectedTexts?.length) throw new Error(`checks[${index}] selector_text_order requires expected_texts.`);
|
|
369
|
+
}
|
|
355
370
|
const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
|
|
356
371
|
if (type === "route_inventory" && !expectedRoutes?.length) {
|
|
357
372
|
throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
|
|
@@ -362,6 +377,7 @@ function normalizeCheck(input, index) {
|
|
|
362
377
|
expected_path: stringValue(input.expected_path),
|
|
363
378
|
expected_routes: expectedRoutes,
|
|
364
379
|
selector: stringValue(input.selector),
|
|
380
|
+
expected_texts: expectedTexts,
|
|
365
381
|
link_selector: stringValue(input.link_selector) || stringValue(input.linkSelector),
|
|
366
382
|
source_selector: stringValue(input.source_selector) || stringValue(input.sourceSelector),
|
|
367
383
|
route_path_prefix: stringValue(input.route_path_prefix) || stringValue(input.routePathPrefix),
|
|
@@ -467,6 +483,28 @@ function selectorKey(check) {
|
|
|
467
483
|
function textKey(check) {
|
|
468
484
|
return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
|
|
469
485
|
}
|
|
486
|
+
function textSequenceForCheck(viewport, check) {
|
|
487
|
+
const key = selectorKey(check);
|
|
488
|
+
const sequence = viewport.text_sequences?.[key];
|
|
489
|
+
if (isRecord(sequence)) {
|
|
490
|
+
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
491
|
+
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
492
|
+
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
493
|
+
return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
494
|
+
}
|
|
495
|
+
return [];
|
|
496
|
+
}
|
|
497
|
+
function textOrderMatch(texts, expectedTexts) {
|
|
498
|
+
const positions = [];
|
|
499
|
+
let startAt = 0;
|
|
500
|
+
for (const expected of expectedTexts) {
|
|
501
|
+
const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
|
|
502
|
+
if (index < 0) return { matched: false, positions };
|
|
503
|
+
positions.push(index);
|
|
504
|
+
startAt = index + 1;
|
|
505
|
+
}
|
|
506
|
+
return { matched: true, positions };
|
|
507
|
+
}
|
|
470
508
|
function matchText(sample, check) {
|
|
471
509
|
if (check.pattern) {
|
|
472
510
|
try {
|
|
@@ -586,6 +624,32 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
586
624
|
message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
|
|
587
625
|
};
|
|
588
626
|
}
|
|
627
|
+
if (check.type === "selector_text_order") {
|
|
628
|
+
const key = selectorKey(check);
|
|
629
|
+
const expectedTexts = check.expected_texts || [];
|
|
630
|
+
const results = viewports.map((viewport) => {
|
|
631
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
632
|
+
const match = textOrderMatch(texts, expectedTexts);
|
|
633
|
+
return {
|
|
634
|
+
viewport: viewport.name,
|
|
635
|
+
matched: match.matched,
|
|
636
|
+
matched_positions: match.positions,
|
|
637
|
+
visible_texts: texts.slice(0, 20)
|
|
638
|
+
};
|
|
639
|
+
});
|
|
640
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
641
|
+
return {
|
|
642
|
+
type: check.type,
|
|
643
|
+
label: checkLabel(check),
|
|
644
|
+
status: failed ? "failed" : "passed",
|
|
645
|
+
evidence: {
|
|
646
|
+
selector: key,
|
|
647
|
+
expected_texts: expectedTexts,
|
|
648
|
+
viewports: results.map((result) => toJsonValue(result))
|
|
649
|
+
},
|
|
650
|
+
message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
|
|
651
|
+
};
|
|
652
|
+
}
|
|
589
653
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
590
654
|
const key = textKey(check);
|
|
591
655
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -936,6 +1000,28 @@ function textMatches(sample, check) {
|
|
|
936
1000
|
}
|
|
937
1001
|
return String(sample || "").includes(check.text || "");
|
|
938
1002
|
}
|
|
1003
|
+
function textSequenceForCheck(viewport, check) {
|
|
1004
|
+
const key = check.selector || "";
|
|
1005
|
+
const sequence = viewport.text_sequences && viewport.text_sequences[key];
|
|
1006
|
+
if (sequence && typeof sequence === "object") {
|
|
1007
|
+
const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
|
|
1008
|
+
const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
|
|
1009
|
+
const candidates = visibleTexts.length ? visibleTexts : texts;
|
|
1010
|
+
return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
1011
|
+
}
|
|
1012
|
+
return [];
|
|
1013
|
+
}
|
|
1014
|
+
function textOrderMatch(texts, expectedTexts) {
|
|
1015
|
+
const positions = [];
|
|
1016
|
+
let startAt = 0;
|
|
1017
|
+
for (const expected of expectedTexts || []) {
|
|
1018
|
+
const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
|
|
1019
|
+
if (index < 0) return { matched: false, positions };
|
|
1020
|
+
positions.push(index);
|
|
1021
|
+
startAt = index + 1;
|
|
1022
|
+
}
|
|
1023
|
+
return { matched: true, positions };
|
|
1024
|
+
}
|
|
939
1025
|
function numberValue(value) {
|
|
940
1026
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
941
1027
|
}
|
|
@@ -1140,6 +1226,29 @@ function assessProfile(profile, evidence) {
|
|
|
1140
1226
|
});
|
|
1141
1227
|
continue;
|
|
1142
1228
|
}
|
|
1229
|
+
if (check.type === "selector_text_order") {
|
|
1230
|
+
const selector = check.selector || "";
|
|
1231
|
+
const expectedTexts = check.expected_texts || [];
|
|
1232
|
+
const results = viewports.map((viewport) => {
|
|
1233
|
+
const texts = textSequenceForCheck(viewport, check);
|
|
1234
|
+
const match = textOrderMatch(texts, expectedTexts);
|
|
1235
|
+
return {
|
|
1236
|
+
viewport: viewport.name,
|
|
1237
|
+
matched: match.matched,
|
|
1238
|
+
matched_positions: match.positions,
|
|
1239
|
+
visible_texts: texts.slice(0, 20),
|
|
1240
|
+
};
|
|
1241
|
+
});
|
|
1242
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
1243
|
+
checks.push({
|
|
1244
|
+
type: check.type,
|
|
1245
|
+
label: check.label || check.type,
|
|
1246
|
+
status: failed ? "failed" : "passed",
|
|
1247
|
+
evidence: { selector, expected_texts: expectedTexts, viewports: results },
|
|
1248
|
+
message: failed ? "Selector " + selector + " text order failed in " + failed + " viewport(s)." : undefined,
|
|
1249
|
+
});
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1143
1252
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
1144
1253
|
const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
|
|
1145
1254
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -1513,6 +1622,27 @@ async function selectorStats(selector) {
|
|
|
1513
1622
|
return { count: elements.length, visible_count: elements.filter(isVisible).length };
|
|
1514
1623
|
}).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
1515
1624
|
}
|
|
1625
|
+
async function selectorTextSequence(selector) {
|
|
1626
|
+
return page.locator(selector).evaluateAll((elements) => {
|
|
1627
|
+
const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
|
|
1628
|
+
const isVisible = (element) => {
|
|
1629
|
+
const style = window.getComputedStyle(element);
|
|
1630
|
+
const rect = element.getBoundingClientRect();
|
|
1631
|
+
return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
|
|
1632
|
+
};
|
|
1633
|
+
const rows = elements.map((element, index) => ({
|
|
1634
|
+
index,
|
|
1635
|
+
text: compact(element.innerText || element.textContent || ""),
|
|
1636
|
+
visible: isVisible(element),
|
|
1637
|
+
}));
|
|
1638
|
+
return {
|
|
1639
|
+
count: rows.length,
|
|
1640
|
+
visible_count: rows.filter((row) => row.visible).length,
|
|
1641
|
+
texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
1642
|
+
visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
|
|
1643
|
+
};
|
|
1644
|
+
}).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
1645
|
+
}
|
|
1516
1646
|
function inventoryAppPathFromUrl(urlLike) {
|
|
1517
1647
|
const url = new URL(String(urlLike), targetUrl);
|
|
1518
1648
|
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
@@ -1910,11 +2040,16 @@ async function captureViewport(viewport) {
|
|
|
1910
2040
|
evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
|
|
1911
2041
|
}));
|
|
1912
2042
|
const selectors = {};
|
|
2043
|
+
const text_sequences = {};
|
|
1913
2044
|
const text_matches = {};
|
|
1914
2045
|
for (const check of profile.checks || []) {
|
|
1915
2046
|
if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
|
|
1916
2047
|
selectors[check.selector] = await selectorStats(check.selector);
|
|
1917
2048
|
}
|
|
2049
|
+
if (check.type === "selector_text_order" && check.selector) {
|
|
2050
|
+
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
2051
|
+
text_sequences[check.selector] = await selectorTextSequence(check.selector);
|
|
2052
|
+
}
|
|
1918
2053
|
if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
|
|
1919
2054
|
text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
|
|
1920
2055
|
}
|
|
@@ -1978,6 +2113,7 @@ async function captureViewport(viewport) {
|
|
|
1978
2113
|
bounds_overflow_px: dom.bounds_overflow_px,
|
|
1979
2114
|
overflow_offenders: dom.overflow_offenders || [],
|
|
1980
2115
|
selectors,
|
|
2116
|
+
text_sequences,
|
|
1981
2117
|
text_matches,
|
|
1982
2118
|
route_inventory: routeInventory,
|
|
1983
2119
|
setup_action_results: setupActionResults,
|
package/dist/profile.d.cts
CHANGED
|
@@ -4,7 +4,7 @@ declare const RIDDLE_PROOF_PROFILE_VERSION: "riddle-proof.profile.v1";
|
|
|
4
4
|
declare const RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION: "riddle-proof.profile-evidence.v1";
|
|
5
5
|
declare const RIDDLE_PROOF_PROFILE_RESULT_VERSION: "riddle-proof.profile-result.v1";
|
|
6
6
|
declare const RIDDLE_PROOF_PROFILE_STATUSES: readonly ["passed", "product_regression", "proof_insufficient", "environment_blocked", "configuration_error", "needs_human_review"];
|
|
7
|
-
declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_count_at_least", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
|
|
7
|
+
declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_count_at_least", "selector_text_order", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
|
|
8
8
|
declare const RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES: readonly ["click", "fill", "set_input_value", "local_storage", "session_storage", "clear_storage", "wait", "wait_for_selector", "wait_for_text"];
|
|
9
9
|
type RiddleProofProfileStatus = typeof RIDDLE_PROOF_PROFILE_STATUSES[number];
|
|
10
10
|
type RiddleProofProfileCheckType = typeof RIDDLE_PROOF_PROFILE_CHECK_TYPES[number];
|
|
@@ -66,6 +66,7 @@ interface RiddleProofProfileCheck {
|
|
|
66
66
|
expected_path?: string;
|
|
67
67
|
expected_routes?: RiddleProofProfileRouteInventoryRoute[];
|
|
68
68
|
selector?: string;
|
|
69
|
+
expected_texts?: string[];
|
|
69
70
|
link_selector?: string;
|
|
70
71
|
source_selector?: string;
|
|
71
72
|
route_path_prefix?: string;
|
|
@@ -137,6 +138,7 @@ interface RiddleProofProfileViewportEvidence {
|
|
|
137
138
|
count: number;
|
|
138
139
|
visible_count: number;
|
|
139
140
|
}>;
|
|
141
|
+
text_sequences?: Record<string, Record<string, JsonValue>>;
|
|
140
142
|
text_matches?: Record<string, boolean>;
|
|
141
143
|
route_inventory?: Record<string, JsonValue>;
|
|
142
144
|
setup_action_results?: Array<Record<string, JsonValue>>;
|
package/dist/profile.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ declare const RIDDLE_PROOF_PROFILE_VERSION: "riddle-proof.profile.v1";
|
|
|
4
4
|
declare const RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION: "riddle-proof.profile-evidence.v1";
|
|
5
5
|
declare const RIDDLE_PROOF_PROFILE_RESULT_VERSION: "riddle-proof.profile-result.v1";
|
|
6
6
|
declare const RIDDLE_PROOF_PROFILE_STATUSES: readonly ["passed", "product_regression", "proof_insufficient", "environment_blocked", "configuration_error", "needs_human_review"];
|
|
7
|
-
declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_count_at_least", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
|
|
7
|
+
declare const RIDDLE_PROOF_PROFILE_CHECK_TYPES: readonly ["route_loaded", "selector_visible", "selector_count_at_least", "selector_text_order", "text_visible", "text_absent", "route_inventory", "no_horizontal_overflow", "no_mobile_horizontal_overflow", "no_fatal_console_errors"];
|
|
8
8
|
declare const RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES: readonly ["click", "fill", "set_input_value", "local_storage", "session_storage", "clear_storage", "wait", "wait_for_selector", "wait_for_text"];
|
|
9
9
|
type RiddleProofProfileStatus = typeof RIDDLE_PROOF_PROFILE_STATUSES[number];
|
|
10
10
|
type RiddleProofProfileCheckType = typeof RIDDLE_PROOF_PROFILE_CHECK_TYPES[number];
|
|
@@ -66,6 +66,7 @@ interface RiddleProofProfileCheck {
|
|
|
66
66
|
expected_path?: string;
|
|
67
67
|
expected_routes?: RiddleProofProfileRouteInventoryRoute[];
|
|
68
68
|
selector?: string;
|
|
69
|
+
expected_texts?: string[];
|
|
69
70
|
link_selector?: string;
|
|
70
71
|
source_selector?: string;
|
|
71
72
|
route_path_prefix?: string;
|
|
@@ -137,6 +138,7 @@ interface RiddleProofProfileViewportEvidence {
|
|
|
137
138
|
count: number;
|
|
138
139
|
visible_count: number;
|
|
139
140
|
}>;
|
|
141
|
+
text_sequences?: Record<string, Record<string, JsonValue>>;
|
|
140
142
|
text_matches?: Record<string, boolean>;
|
|
141
143
|
route_inventory?: Record<string, JsonValue>;
|
|
142
144
|
setup_action_results?: Array<Record<string, JsonValue>>;
|
package/dist/profile.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
resolveRiddleProofProfileTimeoutSec,
|
|
20
20
|
slugifyRiddleProofProfileName,
|
|
21
21
|
summarizeRiddleProofProfileResult
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-VCINQE5O.js";
|
|
23
23
|
export {
|
|
24
24
|
RIDDLE_PROOF_PROFILE_CHECK_TYPES,
|
|
25
25
|
RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION,
|