@riddledc/riddle-proof 0.7.37 → 0.7.39
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 +42 -0
- package/dist/{chunk-KZRDAMI5.js → chunk-L4FLSU7L.js} +347 -5
- package/dist/cli.cjs +347 -5
- package/dist/cli.js +1 -1
- package/dist/index.cjs +347 -5
- package/dist/index.js +1 -1
- package/dist/profile.cjs +347 -5
- package/dist/profile.d.cts +6 -1
- package/dist/profile.d.ts +6 -1
- package/dist/profile.js +1 -1
- package/dist/proof-run-engine.d.cts +3 -3
- package/dist/proof-run-engine.d.ts +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -202,6 +202,25 @@ 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 `allowed_console_patterns` / `allowed_console_texts` on
|
|
206
|
+
`no_fatal_console_errors` when a negative-path profile intentionally triggers a
|
|
207
|
+
known browser console error, such as a mocked `503` that the app recovers from:
|
|
208
|
+
|
|
209
|
+
```json
|
|
210
|
+
{
|
|
211
|
+
"type": "no_fatal_console_errors",
|
|
212
|
+
"allowed_console_patterns": [
|
|
213
|
+
"Failed to load resource: the server responded with a status of 503",
|
|
214
|
+
"Build failed: Error: Synthetic build outage"
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Allowed console events and page errors are still counted in check evidence, but
|
|
220
|
+
only unallowed `error` / `assert` console events and page errors fail the check.
|
|
221
|
+
Use `allowed_page_error_patterns`, `allowed_console_texts`, or
|
|
222
|
+
`allowed_page_error_texts` for narrower matching when needed.
|
|
223
|
+
|
|
205
224
|
Use `selector_text_order` when a table, list, or card group must show visible
|
|
206
225
|
items in a specific order after setup actions such as sorting or filtering:
|
|
207
226
|
|
|
@@ -217,6 +236,29 @@ The check records the visible text sequence for the selector and passes when
|
|
|
217
236
|
the expected texts appear in that order as a subsequence. This is less brittle
|
|
218
237
|
than matching one large body-text regex when only row or card order matters.
|
|
219
238
|
|
|
239
|
+
Use `frame_text_visible` and `frame_no_horizontal_overflow` for embedded app,
|
|
240
|
+
game, or preview surfaces that render inside iframes:
|
|
241
|
+
|
|
242
|
+
```json
|
|
243
|
+
[
|
|
244
|
+
{
|
|
245
|
+
"type": "frame_text_visible",
|
|
246
|
+
"selector": ".game-player-root iframe",
|
|
247
|
+
"text": "Start Game"
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"type": "frame_no_horizontal_overflow",
|
|
251
|
+
"selector": ".game-player-root iframe",
|
|
252
|
+
"max_overflow_px": 1
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Frame checks capture each matching iframe's URL, title, compact text sample,
|
|
258
|
+
scroll width, client width, measured horizontal overflow, and top visible
|
|
259
|
+
overflow offenders. This keeps embedded-player audits in profile mode instead
|
|
260
|
+
of requiring bespoke iframe inspection scripts.
|
|
261
|
+
|
|
220
262
|
Use the `route_inventory` check for source-page route coverage audits where a
|
|
221
263
|
navigation surface must expose a known set of routes and each route must load
|
|
222
264
|
both directly and through real link clicks:
|
|
@@ -15,6 +15,8 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
|
|
|
15
15
|
"selector_visible",
|
|
16
16
|
"selector_count_at_least",
|
|
17
17
|
"selector_text_order",
|
|
18
|
+
"frame_text_visible",
|
|
19
|
+
"frame_no_horizontal_overflow",
|
|
18
20
|
"text_visible",
|
|
19
21
|
"text_absent",
|
|
20
22
|
"route_inventory",
|
|
@@ -303,6 +305,13 @@ function normalizeExpectedTexts(value, index) {
|
|
|
303
305
|
if (!texts.length) throw new Error(`checks[${index}] selector_text_order expected_texts must contain non-empty strings.`);
|
|
304
306
|
return texts;
|
|
305
307
|
}
|
|
308
|
+
function normalizeStringList(value, label) {
|
|
309
|
+
if (value === void 0) return void 0;
|
|
310
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
|
|
311
|
+
const values = value.map((item) => String(item).replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
312
|
+
if (!values.length) throw new Error(`${label} must contain non-empty strings.`);
|
|
313
|
+
return values;
|
|
314
|
+
}
|
|
306
315
|
function normalizeCheck(input, index) {
|
|
307
316
|
if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
|
|
308
317
|
const type = stringValue(input.type);
|
|
@@ -313,6 +322,12 @@ function normalizeCheck(input, index) {
|
|
|
313
322
|
if ((type === "selector_visible" || type === "selector_count_at_least") && !stringValue(input.selector)) {
|
|
314
323
|
throw new Error(`checks[${index}] ${type} requires selector.`);
|
|
315
324
|
}
|
|
325
|
+
if ((type === "frame_text_visible" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
|
|
326
|
+
throw new Error(`checks[${index}] ${type} requires selector.`);
|
|
327
|
+
}
|
|
328
|
+
if (type === "frame_text_visible" && !stringValue(input.text) && !stringValue(input.pattern)) {
|
|
329
|
+
throw new Error(`checks[${index}] frame_text_visible requires text or pattern.`);
|
|
330
|
+
}
|
|
316
331
|
if ((type === "text_visible" || type === "text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
|
|
317
332
|
throw new Error(`checks[${index}] ${type} requires text or pattern.`);
|
|
318
333
|
}
|
|
@@ -345,6 +360,10 @@ function normalizeCheck(input, index) {
|
|
|
345
360
|
text: stringValue(input.text),
|
|
346
361
|
pattern: stringValue(input.pattern),
|
|
347
362
|
flags: stringValue(input.flags),
|
|
363
|
+
allowed_console_texts: normalizeStringList(input.allowed_console_texts ?? input.allowedConsoleTexts ?? input.allow_console_texts ?? input.allowConsoleTexts, `checks[${index}] allowed_console_texts`),
|
|
364
|
+
allowed_console_patterns: normalizeStringList(input.allowed_console_patterns ?? input.allowedConsolePatterns ?? input.allow_console_patterns ?? input.allowConsolePatterns, `checks[${index}] allowed_console_patterns`),
|
|
365
|
+
allowed_page_error_texts: normalizeStringList(input.allowed_page_error_texts ?? input.allowedPageErrorTexts ?? input.allow_page_error_texts ?? input.allowPageErrorTexts, `checks[${index}] allowed_page_error_texts`),
|
|
366
|
+
allowed_page_error_patterns: normalizeStringList(input.allowed_page_error_patterns ?? input.allowedPageErrorPatterns ?? input.allow_page_error_patterns ?? input.allowPageErrorPatterns, `checks[${index}] allowed_page_error_patterns`),
|
|
348
367
|
min_count: numberValue(input.min_count),
|
|
349
368
|
max_overflow_px: numberValue(input.max_overflow_px),
|
|
350
369
|
timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
|
|
@@ -463,6 +482,22 @@ function textOrderMatch(texts, expectedTexts) {
|
|
|
463
482
|
}
|
|
464
483
|
return { matched: true, positions };
|
|
465
484
|
}
|
|
485
|
+
function frameEvidenceForSelector(viewport, selector) {
|
|
486
|
+
const container = viewport.frames?.[selector];
|
|
487
|
+
if (!isRecord(container)) return [];
|
|
488
|
+
const frames = Array.isArray(container.frames) ? container.frames : [];
|
|
489
|
+
return frames.filter((frame) => isRecord(frame));
|
|
490
|
+
}
|
|
491
|
+
function frameTextSample(frame) {
|
|
492
|
+
const parts = [
|
|
493
|
+
frame.title,
|
|
494
|
+
frame.text_sample,
|
|
495
|
+
frame.body_text_sample,
|
|
496
|
+
frame.body_text,
|
|
497
|
+
frame.text
|
|
498
|
+
];
|
|
499
|
+
return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
|
|
500
|
+
}
|
|
466
501
|
function summarizeRouteInventory(viewport, inventory) {
|
|
467
502
|
const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
|
|
468
503
|
const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
|
|
@@ -492,6 +527,18 @@ function matchText(sample, check) {
|
|
|
492
527
|
}
|
|
493
528
|
return sample.includes(check.text || "");
|
|
494
529
|
}
|
|
530
|
+
function matchesAllowedMessage(message, texts, patterns) {
|
|
531
|
+
const sample = String(message || "");
|
|
532
|
+
if (texts?.some((text) => sample.includes(text))) return true;
|
|
533
|
+
for (const pattern of patterns || []) {
|
|
534
|
+
try {
|
|
535
|
+
if (new RegExp(pattern).test(sample)) return true;
|
|
536
|
+
} catch {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
495
542
|
function normalizeRoutePath(path) {
|
|
496
543
|
const value = path || "/";
|
|
497
544
|
if (value === "/") return "/";
|
|
@@ -627,6 +674,67 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
627
674
|
message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
|
|
628
675
|
};
|
|
629
676
|
}
|
|
677
|
+
if (check.type === "frame_text_visible") {
|
|
678
|
+
const key = selectorKey(check);
|
|
679
|
+
const results = viewports.map((viewport) => {
|
|
680
|
+
const frames = frameEvidenceForSelector(viewport, key);
|
|
681
|
+
const matches = frames.map((frame, index) => ({ index, frame, matched: matchText(frameTextSample(frame), check) })).filter((item) => item.matched);
|
|
682
|
+
return {
|
|
683
|
+
viewport: viewport.name,
|
|
684
|
+
frame_count: frames.length,
|
|
685
|
+
matched_count: matches.length,
|
|
686
|
+
matched: matches.length > 0,
|
|
687
|
+
urls: frames.map((frame) => String(frame.url || "")).filter(Boolean).slice(0, 10),
|
|
688
|
+
samples: matches.map((item) => frameTextSample(item.frame).replace(/\s+/g, " ").trim().slice(0, 240)).slice(0, 3)
|
|
689
|
+
};
|
|
690
|
+
});
|
|
691
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
692
|
+
return {
|
|
693
|
+
type: check.type,
|
|
694
|
+
label: checkLabel(check),
|
|
695
|
+
status: failed ? "failed" : "passed",
|
|
696
|
+
evidence: {
|
|
697
|
+
selector: key,
|
|
698
|
+
text: check.text || null,
|
|
699
|
+
pattern: check.pattern || null,
|
|
700
|
+
viewports: results.map((result) => toJsonValue(result))
|
|
701
|
+
},
|
|
702
|
+
message: failed ? `Frame selector ${key} did not contain expected text in ${failed} viewport(s).` : void 0
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
if (check.type === "frame_no_horizontal_overflow") {
|
|
706
|
+
const key = selectorKey(check);
|
|
707
|
+
const maxOverflow = check.max_overflow_px ?? 4;
|
|
708
|
+
const results = viewports.map((viewport) => {
|
|
709
|
+
const frames = frameEvidenceForSelector(viewport, key);
|
|
710
|
+
const frameOverflows = frames.map((frame, index) => ({
|
|
711
|
+
index,
|
|
712
|
+
url: String(frame.url || ""),
|
|
713
|
+
overflow_px: horizontalBoundsOverflowPx(frame),
|
|
714
|
+
offender_count: boundsOffendersForEvidence(frame).length
|
|
715
|
+
}));
|
|
716
|
+
const maxFrameOverflow = frameOverflows.reduce((max, frame) => Math.max(max, frame.overflow_px), 0);
|
|
717
|
+
return {
|
|
718
|
+
viewport: viewport.name,
|
|
719
|
+
frame_count: frames.length,
|
|
720
|
+
max_overflow_px: roundPixels(maxFrameOverflow),
|
|
721
|
+
failed_frame_count: frameOverflows.filter((frame) => frame.overflow_px > maxOverflow).length,
|
|
722
|
+
frames: frameOverflows.map((frame) => toJsonValue(frame))
|
|
723
|
+
};
|
|
724
|
+
});
|
|
725
|
+
const failed = results.filter((result) => result.frame_count < 1 || result.failed_frame_count > 0).length;
|
|
726
|
+
return {
|
|
727
|
+
type: check.type,
|
|
728
|
+
label: checkLabel(check),
|
|
729
|
+
status: failed ? "failed" : "passed",
|
|
730
|
+
evidence: {
|
|
731
|
+
selector: key,
|
|
732
|
+
max_overflow_px: maxOverflow,
|
|
733
|
+
viewports: results.map((result) => toJsonValue(result))
|
|
734
|
+
},
|
|
735
|
+
message: failed ? `Frame selector ${key} overflow exceeded ${maxOverflow}px or was missing in ${failed} viewport(s).` : void 0
|
|
736
|
+
};
|
|
737
|
+
}
|
|
630
738
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
631
739
|
const key = textKey(check);
|
|
632
740
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -717,14 +825,28 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
717
825
|
};
|
|
718
826
|
}
|
|
719
827
|
if (check.type === "no_fatal_console_errors") {
|
|
720
|
-
const
|
|
828
|
+
const fatalConsoleEvents = (evidence.console?.events || []).filter((event) => event.type === "error" || event.type === "assert");
|
|
829
|
+
const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
|
|
830
|
+
const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
|
|
831
|
+
const pageErrors = evidence.page_errors || [];
|
|
832
|
+
const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
|
|
833
|
+
const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
|
|
834
|
+
const fatalCount = unallowedConsoleEvents.length + unallowedPageErrors.length;
|
|
721
835
|
return {
|
|
722
836
|
type: check.type,
|
|
723
837
|
label: checkLabel(check),
|
|
724
838
|
status: fatalCount ? "failed" : "passed",
|
|
725
839
|
evidence: {
|
|
726
|
-
console_fatal_count:
|
|
727
|
-
page_error_count:
|
|
840
|
+
console_fatal_count: unallowedConsoleEvents.length,
|
|
841
|
+
page_error_count: unallowedPageErrors.length,
|
|
842
|
+
total_console_fatal_count: fatalConsoleEvents.length,
|
|
843
|
+
total_page_error_count: pageErrors.length,
|
|
844
|
+
allowed_console_fatal_count: allowedConsoleEvents.length,
|
|
845
|
+
allowed_page_error_count: allowedPageErrors.length,
|
|
846
|
+
allowed_console_texts: check.allowed_console_texts || [],
|
|
847
|
+
allowed_console_patterns: check.allowed_console_patterns || [],
|
|
848
|
+
allowed_page_error_texts: check.allowed_page_error_texts || [],
|
|
849
|
+
allowed_page_error_patterns: check.allowed_page_error_patterns || []
|
|
728
850
|
},
|
|
729
851
|
message: fatalCount ? `${fatalCount} fatal browser error(s) were captured.` : void 0
|
|
730
852
|
};
|
|
@@ -980,6 +1102,16 @@ function textMatches(sample, check) {
|
|
|
980
1102
|
}
|
|
981
1103
|
return String(sample || "").includes(check.text || "");
|
|
982
1104
|
}
|
|
1105
|
+
function matchesAllowedMessage(message, texts, patterns) {
|
|
1106
|
+
const sample = String(message || "");
|
|
1107
|
+
if ((texts || []).some((text) => sample.includes(text))) return true;
|
|
1108
|
+
for (const pattern of patterns || []) {
|
|
1109
|
+
try {
|
|
1110
|
+
if (new RegExp(pattern).test(sample)) return true;
|
|
1111
|
+
} catch {}
|
|
1112
|
+
}
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
983
1115
|
function textSequenceForCheck(viewport, check) {
|
|
984
1116
|
const key = check.selector || "";
|
|
985
1117
|
const sequence = viewport.text_sequences && viewport.text_sequences[key];
|
|
@@ -1002,6 +1134,21 @@ function textOrderMatch(texts, expectedTexts) {
|
|
|
1002
1134
|
}
|
|
1003
1135
|
return { matched: true, positions };
|
|
1004
1136
|
}
|
|
1137
|
+
function frameEvidenceForSelector(viewport, selector) {
|
|
1138
|
+
const container = viewport.frames && viewport.frames[selector || ""];
|
|
1139
|
+
if (!container || typeof container !== "object" || Array.isArray(container)) return [];
|
|
1140
|
+
const frames = Array.isArray(container.frames) ? container.frames : [];
|
|
1141
|
+
return frames.filter((frame) => frame && typeof frame === "object" && !Array.isArray(frame));
|
|
1142
|
+
}
|
|
1143
|
+
function frameTextSample(frame) {
|
|
1144
|
+
return [
|
|
1145
|
+
frame && frame.title,
|
|
1146
|
+
frame && frame.text_sample,
|
|
1147
|
+
frame && frame.body_text_sample,
|
|
1148
|
+
frame && frame.body_text,
|
|
1149
|
+
frame && frame.text,
|
|
1150
|
+
].map((part) => String(part || "")).filter(Boolean).join(" ");
|
|
1151
|
+
}
|
|
1005
1152
|
function summarizeRouteInventory(viewport, inventory) {
|
|
1006
1153
|
const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
|
|
1007
1154
|
const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
|
|
@@ -1248,6 +1395,62 @@ function assessProfile(profile, evidence) {
|
|
|
1248
1395
|
});
|
|
1249
1396
|
continue;
|
|
1250
1397
|
}
|
|
1398
|
+
if (check.type === "frame_text_visible") {
|
|
1399
|
+
const selector = check.selector || "";
|
|
1400
|
+
const results = viewports.map((viewport) => {
|
|
1401
|
+
const frames = frameEvidenceForSelector(viewport, selector);
|
|
1402
|
+
const matches = frames
|
|
1403
|
+
.map((frame, index) => ({ index, frame, matched: textMatches(frameTextSample(frame), check) }))
|
|
1404
|
+
.filter((item) => item.matched);
|
|
1405
|
+
return {
|
|
1406
|
+
viewport: viewport.name,
|
|
1407
|
+
frame_count: frames.length,
|
|
1408
|
+
matched_count: matches.length,
|
|
1409
|
+
matched: matches.length > 0,
|
|
1410
|
+
urls: frames.map((frame) => String(frame.url || "")).filter(Boolean).slice(0, 10),
|
|
1411
|
+
samples: matches.map((item) => frameTextSample(item.frame).replace(/\s+/g, " ").trim().slice(0, 240)).slice(0, 3),
|
|
1412
|
+
};
|
|
1413
|
+
});
|
|
1414
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
1415
|
+
checks.push({
|
|
1416
|
+
type: check.type,
|
|
1417
|
+
label: check.label || check.type,
|
|
1418
|
+
status: failed ? "failed" : "passed",
|
|
1419
|
+
evidence: { selector, text: check.text || null, pattern: check.pattern || null, viewports: results },
|
|
1420
|
+
message: failed ? "Frame selector " + selector + " did not contain expected text in " + failed + " viewport(s)." : undefined,
|
|
1421
|
+
});
|
|
1422
|
+
continue;
|
|
1423
|
+
}
|
|
1424
|
+
if (check.type === "frame_no_horizontal_overflow") {
|
|
1425
|
+
const selector = check.selector || "";
|
|
1426
|
+
const maxOverflow = check.max_overflow_px == null ? 4 : check.max_overflow_px;
|
|
1427
|
+
const results = viewports.map((viewport) => {
|
|
1428
|
+
const frames = frameEvidenceForSelector(viewport, selector);
|
|
1429
|
+
const frameOverflows = frames.map((frame, index) => ({
|
|
1430
|
+
index,
|
|
1431
|
+
url: String(frame.url || ""),
|
|
1432
|
+
overflow_px: horizontalBoundsOverflowPx(frame),
|
|
1433
|
+
offender_count: boundsOffendersForEvidence(frame).length,
|
|
1434
|
+
}));
|
|
1435
|
+
const maxFrameOverflow = frameOverflows.reduce((max, frame) => Math.max(max, frame.overflow_px), 0);
|
|
1436
|
+
return {
|
|
1437
|
+
viewport: viewport.name,
|
|
1438
|
+
frame_count: frames.length,
|
|
1439
|
+
max_overflow_px: roundPixels(maxFrameOverflow),
|
|
1440
|
+
failed_frame_count: frameOverflows.filter((frame) => frame.overflow_px > maxOverflow).length,
|
|
1441
|
+
frames: frameOverflows,
|
|
1442
|
+
};
|
|
1443
|
+
});
|
|
1444
|
+
const failed = results.filter((result) => result.frame_count < 1 || result.failed_frame_count > 0).length;
|
|
1445
|
+
checks.push({
|
|
1446
|
+
type: check.type,
|
|
1447
|
+
label: check.label || check.type,
|
|
1448
|
+
status: failed ? "failed" : "passed",
|
|
1449
|
+
evidence: { selector, max_overflow_px: maxOverflow, viewports: results },
|
|
1450
|
+
message: failed ? "Frame selector " + selector + " overflow exceeded " + maxOverflow + "px or was missing in " + failed + " viewport(s)." : undefined,
|
|
1451
|
+
});
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1251
1454
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
1252
1455
|
const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
|
|
1253
1456
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -1343,12 +1546,29 @@ function assessProfile(profile, evidence) {
|
|
|
1343
1546
|
continue;
|
|
1344
1547
|
}
|
|
1345
1548
|
if (check.type === "no_fatal_console_errors") {
|
|
1346
|
-
const
|
|
1549
|
+
const fatalConsoleEvents = ((evidence.console && evidence.console.events) || []).filter((event) => event && (event.type === "error" || event.type === "assert"));
|
|
1550
|
+
const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
|
|
1551
|
+
const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
|
|
1552
|
+
const pageErrors = evidence.page_errors || [];
|
|
1553
|
+
const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error && error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
|
|
1554
|
+
const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error && error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
|
|
1555
|
+
const fatalCount = unallowedConsoleEvents.length + unallowedPageErrors.length;
|
|
1347
1556
|
checks.push({
|
|
1348
1557
|
type: check.type,
|
|
1349
1558
|
label: check.label || check.type,
|
|
1350
1559
|
status: fatalCount ? "failed" : "passed",
|
|
1351
|
-
evidence: {
|
|
1560
|
+
evidence: {
|
|
1561
|
+
console_fatal_count: unallowedConsoleEvents.length,
|
|
1562
|
+
page_error_count: unallowedPageErrors.length,
|
|
1563
|
+
total_console_fatal_count: fatalConsoleEvents.length,
|
|
1564
|
+
total_page_error_count: pageErrors.length,
|
|
1565
|
+
allowed_console_fatal_count: allowedConsoleEvents.length,
|
|
1566
|
+
allowed_page_error_count: allowedPageErrors.length,
|
|
1567
|
+
allowed_console_texts: check.allowed_console_texts || [],
|
|
1568
|
+
allowed_console_patterns: check.allowed_console_patterns || [],
|
|
1569
|
+
allowed_page_error_texts: check.allowed_page_error_texts || [],
|
|
1570
|
+
allowed_page_error_patterns: check.allowed_page_error_patterns || [],
|
|
1571
|
+
},
|
|
1352
1572
|
message: fatalCount ? String(fatalCount) + " fatal browser error(s) were captured." : undefined,
|
|
1353
1573
|
});
|
|
1354
1574
|
continue;
|
|
@@ -1645,6 +1865,108 @@ async function selectorTextSequence(selector) {
|
|
|
1645
1865
|
};
|
|
1646
1866
|
}).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
1647
1867
|
}
|
|
1868
|
+
async function frameEvidence(selector) {
|
|
1869
|
+
const result = { selector, count: 0, frame_count: 0, frames: [], errors: [] };
|
|
1870
|
+
let handles = [];
|
|
1871
|
+
try {
|
|
1872
|
+
const locator = page.locator(selector);
|
|
1873
|
+
result.count = await locator.count();
|
|
1874
|
+
handles = await locator.elementHandles();
|
|
1875
|
+
} catch (error) {
|
|
1876
|
+
result.errors.push(String(error && error.message ? error.message : error).slice(0, 500));
|
|
1877
|
+
return result;
|
|
1878
|
+
}
|
|
1879
|
+
for (let index = 0; index < handles.length; index += 1) {
|
|
1880
|
+
const handle = handles[index];
|
|
1881
|
+
try {
|
|
1882
|
+
const frame = typeof handle.contentFrame === "function" ? await handle.contentFrame() : null;
|
|
1883
|
+
if (!frame) {
|
|
1884
|
+
result.frames.push({ index, attached: false, error: "content_frame_unavailable" });
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
const snapshot = await frame.evaluate(() => {
|
|
1888
|
+
const body = document.body;
|
|
1889
|
+
const documentElement = document.documentElement;
|
|
1890
|
+
const text = (body ? body.innerText : "").replace(/\s+/g, " ").trim();
|
|
1891
|
+
const clientWidth = documentElement ? documentElement.clientWidth : window.innerWidth;
|
|
1892
|
+
const clientHeight = documentElement ? documentElement.clientHeight : window.innerHeight;
|
|
1893
|
+
const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
|
|
1894
|
+
const viewportWidth = clientWidth || window.innerWidth;
|
|
1895
|
+
const overflowOffenders = [];
|
|
1896
|
+
function isContainedByHorizontalScroller(element) {
|
|
1897
|
+
let current = element.parentElement;
|
|
1898
|
+
while (current && current !== body && current !== documentElement) {
|
|
1899
|
+
const style = window.getComputedStyle(current);
|
|
1900
|
+
const overflowX = style.overflowX || style.overflow || "";
|
|
1901
|
+
if ((overflowX === "auto" || overflowX === "scroll") && current.scrollWidth > current.clientWidth + 1) {
|
|
1902
|
+
const currentRect = current.getBoundingClientRect();
|
|
1903
|
+
const contained = currentRect.left >= -0.5 && currentRect.right <= viewportWidth + 0.5;
|
|
1904
|
+
if (contained) return true;
|
|
1905
|
+
}
|
|
1906
|
+
current = current.parentElement;
|
|
1907
|
+
}
|
|
1908
|
+
return false;
|
|
1909
|
+
}
|
|
1910
|
+
for (const element of Array.from(body ? body.querySelectorAll("*") : [])) {
|
|
1911
|
+
const rect = element.getBoundingClientRect();
|
|
1912
|
+
if (!rect || rect.width < 1 || rect.height < 1) continue;
|
|
1913
|
+
const style = window.getComputedStyle(element);
|
|
1914
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") continue;
|
|
1915
|
+
const leftOverflow = Math.max(0, -rect.left);
|
|
1916
|
+
const rightOverflow = Math.max(0, rect.right - viewportWidth);
|
|
1917
|
+
const overflow = Math.max(leftOverflow, rightOverflow);
|
|
1918
|
+
if (overflow <= 0.5) continue;
|
|
1919
|
+
if (isContainedByHorizontalScroller(element)) continue;
|
|
1920
|
+
const tag = element.tagName ? element.tagName.toLowerCase() : "element";
|
|
1921
|
+
const id = element.id ? "#" + element.id : "";
|
|
1922
|
+
const className = typeof element.className === "string"
|
|
1923
|
+
? element.className.trim().split(/\s+/).filter(Boolean).slice(0, 2).join(".")
|
|
1924
|
+
: "";
|
|
1925
|
+
overflowOffenders.push({
|
|
1926
|
+
selector: tag + id + (className ? "." + className : ""),
|
|
1927
|
+
tag,
|
|
1928
|
+
text: (element.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120),
|
|
1929
|
+
overflow,
|
|
1930
|
+
left_overflow_px: leftOverflow,
|
|
1931
|
+
right_overflow_px: rightOverflow,
|
|
1932
|
+
viewport_width: viewportWidth,
|
|
1933
|
+
rect: {
|
|
1934
|
+
left: rect.left,
|
|
1935
|
+
right: rect.right,
|
|
1936
|
+
width: rect.width,
|
|
1937
|
+
},
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
overflowOffenders.sort((a, b) => b.overflow - a.overflow);
|
|
1941
|
+
const boundsOverflowPx = Math.max(
|
|
1942
|
+
0,
|
|
1943
|
+
scrollWidth - (clientWidth || viewportWidth),
|
|
1944
|
+
...overflowOffenders.map((offender) => offender.overflow),
|
|
1945
|
+
);
|
|
1946
|
+
return {
|
|
1947
|
+
url: location.href,
|
|
1948
|
+
title: document.title,
|
|
1949
|
+
text_length: text.length,
|
|
1950
|
+
text_sample: text.slice(0, 8000),
|
|
1951
|
+
body_text_sample: text.slice(0, 8000),
|
|
1952
|
+
viewport_width: viewportWidth,
|
|
1953
|
+
viewport_height: clientHeight || window.innerHeight,
|
|
1954
|
+
scroll_width: scrollWidth,
|
|
1955
|
+
client_width: clientWidth,
|
|
1956
|
+
overflow_px: Math.max(0, scrollWidth - (clientWidth || viewportWidth)),
|
|
1957
|
+
bounds_overflow_px: Math.round(boundsOverflowPx * 100) / 100,
|
|
1958
|
+
overflow_offender_count: overflowOffenders.length,
|
|
1959
|
+
overflow_offenders: overflowOffenders.slice(0, 10),
|
|
1960
|
+
};
|
|
1961
|
+
});
|
|
1962
|
+
result.frames.push({ index, attached: true, ...snapshot });
|
|
1963
|
+
} catch (error) {
|
|
1964
|
+
result.frames.push({ index, attached: false, error: String(error && error.message ? error.message : error).slice(0, 1000) });
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
result.frame_count = result.frames.filter((frame) => frame && frame.attached !== false && !frame.error).length;
|
|
1968
|
+
return result;
|
|
1969
|
+
}
|
|
1648
1970
|
function inventoryAppPathFromUrl(urlLike) {
|
|
1649
1971
|
const url = new URL(String(urlLike), targetUrl);
|
|
1650
1972
|
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
@@ -2049,6 +2371,7 @@ async function captureViewport(viewport) {
|
|
|
2049
2371
|
evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
|
|
2050
2372
|
}));
|
|
2051
2373
|
const selectors = {};
|
|
2374
|
+
const frames = {};
|
|
2052
2375
|
const text_sequences = {};
|
|
2053
2376
|
const text_matches = {};
|
|
2054
2377
|
for (const check of profile.checks || []) {
|
|
@@ -2062,6 +2385,10 @@ async function captureViewport(viewport) {
|
|
|
2062
2385
|
if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
|
|
2063
2386
|
text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
|
|
2064
2387
|
}
|
|
2388
|
+
if ((check.type === "frame_text_visible" || check.type === "frame_no_horizontal_overflow") && check.selector) {
|
|
2389
|
+
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
2390
|
+
frames[check.selector] = frames[check.selector] || await frameEvidence(check.selector);
|
|
2391
|
+
}
|
|
2065
2392
|
}
|
|
2066
2393
|
const screenshotLabel = profileSlug + "-" + viewport.name;
|
|
2067
2394
|
try {
|
|
@@ -2122,6 +2449,7 @@ async function captureViewport(viewport) {
|
|
|
2122
2449
|
bounds_overflow_px: dom.bounds_overflow_px,
|
|
2123
2450
|
overflow_offenders: dom.overflow_offenders || [],
|
|
2124
2451
|
selectors,
|
|
2452
|
+
frames,
|
|
2125
2453
|
text_sequences,
|
|
2126
2454
|
text_matches,
|
|
2127
2455
|
route_inventory: routeInventory,
|
|
@@ -2157,6 +2485,20 @@ function buildProfileEvidence(currentViewports) {
|
|
|
2157
2485
|
overflow_px: currentViewports.map((viewport) => viewport.overflow_px),
|
|
2158
2486
|
bounds_overflow_px: currentViewports.map((viewport) => viewport.bounds_overflow_px),
|
|
2159
2487
|
overflow_offender_counts: currentViewports.map((viewport) => (viewport.overflow_offenders || []).length),
|
|
2488
|
+
frames: currentViewports
|
|
2489
|
+
.filter((viewport) => viewport.frames)
|
|
2490
|
+
.map((viewport) => ({
|
|
2491
|
+
viewport: viewport.name,
|
|
2492
|
+
selectors: Object.entries(viewport.frames || {}).map(([selector, frameSet]) => ({
|
|
2493
|
+
selector,
|
|
2494
|
+
count: frameSet && frameSet.count,
|
|
2495
|
+
frame_count: frameSet && frameSet.frame_count,
|
|
2496
|
+
max_bounds_overflow_px: Math.max(
|
|
2497
|
+
0,
|
|
2498
|
+
...((frameSet && Array.isArray(frameSet.frames) ? frameSet.frames : [])).map((frame) => horizontalBoundsOverflowPx(frame)),
|
|
2499
|
+
),
|
|
2500
|
+
})),
|
|
2501
|
+
})),
|
|
2160
2502
|
route_inventory: currentViewports
|
|
2161
2503
|
.filter((viewport) => viewport.route_inventory)
|
|
2162
2504
|
.map((viewport) => ({
|