@riddledc/riddle-proof 0.7.36 → 0.7.38
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 +26 -1
- package/dist/{chunk-VEK6QHPE.js → chunk-ZB7AFRN3.js} +287 -2
- package/dist/cli.cjs +287 -2
- package/dist/cli.js +1 -1
- package/dist/index.cjs +287 -2
- package/dist/index.js +1 -1
- package/dist/profile.cjs +287 -2
- package/dist/profile.d.cts +2 -1
- package/dist/profile.d.ts +2 -1
- package/dist/profile.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -217,6 +217,29 @@ The check records the visible text sequence for the selector and passes when
|
|
|
217
217
|
the expected texts appear in that order as a subsequence. This is less brittle
|
|
218
218
|
than matching one large body-text regex when only row or card order matters.
|
|
219
219
|
|
|
220
|
+
Use `frame_text_visible` and `frame_no_horizontal_overflow` for embedded app,
|
|
221
|
+
game, or preview surfaces that render inside iframes:
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
[
|
|
225
|
+
{
|
|
226
|
+
"type": "frame_text_visible",
|
|
227
|
+
"selector": ".game-player-root iframe",
|
|
228
|
+
"text": "Start Game"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
"type": "frame_no_horizontal_overflow",
|
|
232
|
+
"selector": ".game-player-root iframe",
|
|
233
|
+
"max_overflow_px": 1
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Frame checks capture each matching iframe's URL, title, compact text sample,
|
|
239
|
+
scroll width, client width, measured horizontal overflow, and top visible
|
|
240
|
+
overflow offenders. This keeps embedded-player audits in profile mode instead
|
|
241
|
+
of requiring bespoke iframe inspection scripts.
|
|
242
|
+
|
|
220
243
|
Use the `route_inventory` check for source-page route coverage audits where a
|
|
221
244
|
navigation surface must expose a known set of routes and each route must load
|
|
222
245
|
both directly and through real link clicks:
|
|
@@ -247,7 +270,9 @@ Set `run_direct_routes: false`, `run_clickthroughs: false`,
|
|
|
247
270
|
`save_route_screenshots: true` when a profile needs a narrower or more
|
|
248
271
|
artifact-heavy audit. `require_unique_routes: false` is useful when a navigation
|
|
249
272
|
surface intentionally links to the same route from multiple cards or anchors,
|
|
250
|
-
while still proving the unique expected route set.
|
|
273
|
+
while still proving the unique expected route set. When `run_all_viewports` and
|
|
274
|
+
`save_route_screenshots` are both enabled, route screenshot artifact labels
|
|
275
|
+
include the viewport name so desktop and mobile route artifacts remain distinct.
|
|
251
276
|
|
|
252
277
|
The result uses `riddle-proof.profile-result.v1` and separates product failures
|
|
253
278
|
from weak proof and environment blockers:
|
|
@@ -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",
|
|
@@ -313,6 +315,12 @@ function normalizeCheck(input, index) {
|
|
|
313
315
|
if ((type === "selector_visible" || type === "selector_count_at_least") && !stringValue(input.selector)) {
|
|
314
316
|
throw new Error(`checks[${index}] ${type} requires selector.`);
|
|
315
317
|
}
|
|
318
|
+
if ((type === "frame_text_visible" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
|
|
319
|
+
throw new Error(`checks[${index}] ${type} requires selector.`);
|
|
320
|
+
}
|
|
321
|
+
if (type === "frame_text_visible" && !stringValue(input.text) && !stringValue(input.pattern)) {
|
|
322
|
+
throw new Error(`checks[${index}] frame_text_visible requires text or pattern.`);
|
|
323
|
+
}
|
|
316
324
|
if ((type === "text_visible" || type === "text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
|
|
317
325
|
throw new Error(`checks[${index}] ${type} requires text or pattern.`);
|
|
318
326
|
}
|
|
@@ -463,6 +471,22 @@ function textOrderMatch(texts, expectedTexts) {
|
|
|
463
471
|
}
|
|
464
472
|
return { matched: true, positions };
|
|
465
473
|
}
|
|
474
|
+
function frameEvidenceForSelector(viewport, selector) {
|
|
475
|
+
const container = viewport.frames?.[selector];
|
|
476
|
+
if (!isRecord(container)) return [];
|
|
477
|
+
const frames = Array.isArray(container.frames) ? container.frames : [];
|
|
478
|
+
return frames.filter((frame) => isRecord(frame));
|
|
479
|
+
}
|
|
480
|
+
function frameTextSample(frame) {
|
|
481
|
+
const parts = [
|
|
482
|
+
frame.title,
|
|
483
|
+
frame.text_sample,
|
|
484
|
+
frame.body_text_sample,
|
|
485
|
+
frame.body_text,
|
|
486
|
+
frame.text
|
|
487
|
+
];
|
|
488
|
+
return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
|
|
489
|
+
}
|
|
466
490
|
function summarizeRouteInventory(viewport, inventory) {
|
|
467
491
|
const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
|
|
468
492
|
const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
|
|
@@ -627,6 +651,67 @@ function assessCheckFromEvidence(check, evidence) {
|
|
|
627
651
|
message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
|
|
628
652
|
};
|
|
629
653
|
}
|
|
654
|
+
if (check.type === "frame_text_visible") {
|
|
655
|
+
const key = selectorKey(check);
|
|
656
|
+
const results = viewports.map((viewport) => {
|
|
657
|
+
const frames = frameEvidenceForSelector(viewport, key);
|
|
658
|
+
const matches = frames.map((frame, index) => ({ index, frame, matched: matchText(frameTextSample(frame), check) })).filter((item) => item.matched);
|
|
659
|
+
return {
|
|
660
|
+
viewport: viewport.name,
|
|
661
|
+
frame_count: frames.length,
|
|
662
|
+
matched_count: matches.length,
|
|
663
|
+
matched: matches.length > 0,
|
|
664
|
+
urls: frames.map((frame) => String(frame.url || "")).filter(Boolean).slice(0, 10),
|
|
665
|
+
samples: matches.map((item) => frameTextSample(item.frame).replace(/\s+/g, " ").trim().slice(0, 240)).slice(0, 3)
|
|
666
|
+
};
|
|
667
|
+
});
|
|
668
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
669
|
+
return {
|
|
670
|
+
type: check.type,
|
|
671
|
+
label: checkLabel(check),
|
|
672
|
+
status: failed ? "failed" : "passed",
|
|
673
|
+
evidence: {
|
|
674
|
+
selector: key,
|
|
675
|
+
text: check.text || null,
|
|
676
|
+
pattern: check.pattern || null,
|
|
677
|
+
viewports: results.map((result) => toJsonValue(result))
|
|
678
|
+
},
|
|
679
|
+
message: failed ? `Frame selector ${key} did not contain expected text in ${failed} viewport(s).` : void 0
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
if (check.type === "frame_no_horizontal_overflow") {
|
|
683
|
+
const key = selectorKey(check);
|
|
684
|
+
const maxOverflow = check.max_overflow_px ?? 4;
|
|
685
|
+
const results = viewports.map((viewport) => {
|
|
686
|
+
const frames = frameEvidenceForSelector(viewport, key);
|
|
687
|
+
const frameOverflows = frames.map((frame, index) => ({
|
|
688
|
+
index,
|
|
689
|
+
url: String(frame.url || ""),
|
|
690
|
+
overflow_px: horizontalBoundsOverflowPx(frame),
|
|
691
|
+
offender_count: boundsOffendersForEvidence(frame).length
|
|
692
|
+
}));
|
|
693
|
+
const maxFrameOverflow = frameOverflows.reduce((max, frame) => Math.max(max, frame.overflow_px), 0);
|
|
694
|
+
return {
|
|
695
|
+
viewport: viewport.name,
|
|
696
|
+
frame_count: frames.length,
|
|
697
|
+
max_overflow_px: roundPixels(maxFrameOverflow),
|
|
698
|
+
failed_frame_count: frameOverflows.filter((frame) => frame.overflow_px > maxOverflow).length,
|
|
699
|
+
frames: frameOverflows.map((frame) => toJsonValue(frame))
|
|
700
|
+
};
|
|
701
|
+
});
|
|
702
|
+
const failed = results.filter((result) => result.frame_count < 1 || result.failed_frame_count > 0).length;
|
|
703
|
+
return {
|
|
704
|
+
type: check.type,
|
|
705
|
+
label: checkLabel(check),
|
|
706
|
+
status: failed ? "failed" : "passed",
|
|
707
|
+
evidence: {
|
|
708
|
+
selector: key,
|
|
709
|
+
max_overflow_px: maxOverflow,
|
|
710
|
+
viewports: results.map((result) => toJsonValue(result))
|
|
711
|
+
},
|
|
712
|
+
message: failed ? `Frame selector ${key} overflow exceeded ${maxOverflow}px or was missing in ${failed} viewport(s).` : void 0
|
|
713
|
+
};
|
|
714
|
+
}
|
|
630
715
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
631
716
|
const key = textKey(check);
|
|
632
717
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -1002,6 +1087,21 @@ function textOrderMatch(texts, expectedTexts) {
|
|
|
1002
1087
|
}
|
|
1003
1088
|
return { matched: true, positions };
|
|
1004
1089
|
}
|
|
1090
|
+
function frameEvidenceForSelector(viewport, selector) {
|
|
1091
|
+
const container = viewport.frames && viewport.frames[selector || ""];
|
|
1092
|
+
if (!container || typeof container !== "object" || Array.isArray(container)) return [];
|
|
1093
|
+
const frames = Array.isArray(container.frames) ? container.frames : [];
|
|
1094
|
+
return frames.filter((frame) => frame && typeof frame === "object" && !Array.isArray(frame));
|
|
1095
|
+
}
|
|
1096
|
+
function frameTextSample(frame) {
|
|
1097
|
+
return [
|
|
1098
|
+
frame && frame.title,
|
|
1099
|
+
frame && frame.text_sample,
|
|
1100
|
+
frame && frame.body_text_sample,
|
|
1101
|
+
frame && frame.body_text,
|
|
1102
|
+
frame && frame.text,
|
|
1103
|
+
].map((part) => String(part || "")).filter(Boolean).join(" ");
|
|
1104
|
+
}
|
|
1005
1105
|
function summarizeRouteInventory(viewport, inventory) {
|
|
1006
1106
|
const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
|
|
1007
1107
|
const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
|
|
@@ -1248,6 +1348,62 @@ function assessProfile(profile, evidence) {
|
|
|
1248
1348
|
});
|
|
1249
1349
|
continue;
|
|
1250
1350
|
}
|
|
1351
|
+
if (check.type === "frame_text_visible") {
|
|
1352
|
+
const selector = check.selector || "";
|
|
1353
|
+
const results = viewports.map((viewport) => {
|
|
1354
|
+
const frames = frameEvidenceForSelector(viewport, selector);
|
|
1355
|
+
const matches = frames
|
|
1356
|
+
.map((frame, index) => ({ index, frame, matched: textMatches(frameTextSample(frame), check) }))
|
|
1357
|
+
.filter((item) => item.matched);
|
|
1358
|
+
return {
|
|
1359
|
+
viewport: viewport.name,
|
|
1360
|
+
frame_count: frames.length,
|
|
1361
|
+
matched_count: matches.length,
|
|
1362
|
+
matched: matches.length > 0,
|
|
1363
|
+
urls: frames.map((frame) => String(frame.url || "")).filter(Boolean).slice(0, 10),
|
|
1364
|
+
samples: matches.map((item) => frameTextSample(item.frame).replace(/\s+/g, " ").trim().slice(0, 240)).slice(0, 3),
|
|
1365
|
+
};
|
|
1366
|
+
});
|
|
1367
|
+
const failed = results.filter((result) => !result.matched).length;
|
|
1368
|
+
checks.push({
|
|
1369
|
+
type: check.type,
|
|
1370
|
+
label: check.label || check.type,
|
|
1371
|
+
status: failed ? "failed" : "passed",
|
|
1372
|
+
evidence: { selector, text: check.text || null, pattern: check.pattern || null, viewports: results },
|
|
1373
|
+
message: failed ? "Frame selector " + selector + " did not contain expected text in " + failed + " viewport(s)." : undefined,
|
|
1374
|
+
});
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
if (check.type === "frame_no_horizontal_overflow") {
|
|
1378
|
+
const selector = check.selector || "";
|
|
1379
|
+
const maxOverflow = check.max_overflow_px == null ? 4 : check.max_overflow_px;
|
|
1380
|
+
const results = viewports.map((viewport) => {
|
|
1381
|
+
const frames = frameEvidenceForSelector(viewport, selector);
|
|
1382
|
+
const frameOverflows = frames.map((frame, index) => ({
|
|
1383
|
+
index,
|
|
1384
|
+
url: String(frame.url || ""),
|
|
1385
|
+
overflow_px: horizontalBoundsOverflowPx(frame),
|
|
1386
|
+
offender_count: boundsOffendersForEvidence(frame).length,
|
|
1387
|
+
}));
|
|
1388
|
+
const maxFrameOverflow = frameOverflows.reduce((max, frame) => Math.max(max, frame.overflow_px), 0);
|
|
1389
|
+
return {
|
|
1390
|
+
viewport: viewport.name,
|
|
1391
|
+
frame_count: frames.length,
|
|
1392
|
+
max_overflow_px: roundPixels(maxFrameOverflow),
|
|
1393
|
+
failed_frame_count: frameOverflows.filter((frame) => frame.overflow_px > maxOverflow).length,
|
|
1394
|
+
frames: frameOverflows,
|
|
1395
|
+
};
|
|
1396
|
+
});
|
|
1397
|
+
const failed = results.filter((result) => result.frame_count < 1 || result.failed_frame_count > 0).length;
|
|
1398
|
+
checks.push({
|
|
1399
|
+
type: check.type,
|
|
1400
|
+
label: check.label || check.type,
|
|
1401
|
+
status: failed ? "failed" : "passed",
|
|
1402
|
+
evidence: { selector, max_overflow_px: maxOverflow, viewports: results },
|
|
1403
|
+
message: failed ? "Frame selector " + selector + " overflow exceeded " + maxOverflow + "px or was missing in " + failed + " viewport(s)." : undefined,
|
|
1404
|
+
});
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1251
1407
|
if (check.type === "text_visible" || check.type === "text_absent") {
|
|
1252
1408
|
const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
|
|
1253
1409
|
const expectedVisible = check.type === "text_visible";
|
|
@@ -1645,6 +1801,108 @@ async function selectorTextSequence(selector) {
|
|
|
1645
1801
|
};
|
|
1646
1802
|
}).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
|
|
1647
1803
|
}
|
|
1804
|
+
async function frameEvidence(selector) {
|
|
1805
|
+
const result = { selector, count: 0, frame_count: 0, frames: [], errors: [] };
|
|
1806
|
+
let handles = [];
|
|
1807
|
+
try {
|
|
1808
|
+
const locator = page.locator(selector);
|
|
1809
|
+
result.count = await locator.count();
|
|
1810
|
+
handles = await locator.elementHandles();
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
result.errors.push(String(error && error.message ? error.message : error).slice(0, 500));
|
|
1813
|
+
return result;
|
|
1814
|
+
}
|
|
1815
|
+
for (let index = 0; index < handles.length; index += 1) {
|
|
1816
|
+
const handle = handles[index];
|
|
1817
|
+
try {
|
|
1818
|
+
const frame = typeof handle.contentFrame === "function" ? await handle.contentFrame() : null;
|
|
1819
|
+
if (!frame) {
|
|
1820
|
+
result.frames.push({ index, attached: false, error: "content_frame_unavailable" });
|
|
1821
|
+
continue;
|
|
1822
|
+
}
|
|
1823
|
+
const snapshot = await frame.evaluate(() => {
|
|
1824
|
+
const body = document.body;
|
|
1825
|
+
const documentElement = document.documentElement;
|
|
1826
|
+
const text = (body ? body.innerText : "").replace(/\s+/g, " ").trim();
|
|
1827
|
+
const clientWidth = documentElement ? documentElement.clientWidth : window.innerWidth;
|
|
1828
|
+
const clientHeight = documentElement ? documentElement.clientHeight : window.innerHeight;
|
|
1829
|
+
const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
|
|
1830
|
+
const viewportWidth = clientWidth || window.innerWidth;
|
|
1831
|
+
const overflowOffenders = [];
|
|
1832
|
+
function isContainedByHorizontalScroller(element) {
|
|
1833
|
+
let current = element.parentElement;
|
|
1834
|
+
while (current && current !== body && current !== documentElement) {
|
|
1835
|
+
const style = window.getComputedStyle(current);
|
|
1836
|
+
const overflowX = style.overflowX || style.overflow || "";
|
|
1837
|
+
if ((overflowX === "auto" || overflowX === "scroll") && current.scrollWidth > current.clientWidth + 1) {
|
|
1838
|
+
const currentRect = current.getBoundingClientRect();
|
|
1839
|
+
const contained = currentRect.left >= -0.5 && currentRect.right <= viewportWidth + 0.5;
|
|
1840
|
+
if (contained) return true;
|
|
1841
|
+
}
|
|
1842
|
+
current = current.parentElement;
|
|
1843
|
+
}
|
|
1844
|
+
return false;
|
|
1845
|
+
}
|
|
1846
|
+
for (const element of Array.from(body ? body.querySelectorAll("*") : [])) {
|
|
1847
|
+
const rect = element.getBoundingClientRect();
|
|
1848
|
+
if (!rect || rect.width < 1 || rect.height < 1) continue;
|
|
1849
|
+
const style = window.getComputedStyle(element);
|
|
1850
|
+
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") continue;
|
|
1851
|
+
const leftOverflow = Math.max(0, -rect.left);
|
|
1852
|
+
const rightOverflow = Math.max(0, rect.right - viewportWidth);
|
|
1853
|
+
const overflow = Math.max(leftOverflow, rightOverflow);
|
|
1854
|
+
if (overflow <= 0.5) continue;
|
|
1855
|
+
if (isContainedByHorizontalScroller(element)) continue;
|
|
1856
|
+
const tag = element.tagName ? element.tagName.toLowerCase() : "element";
|
|
1857
|
+
const id = element.id ? "#" + element.id : "";
|
|
1858
|
+
const className = typeof element.className === "string"
|
|
1859
|
+
? element.className.trim().split(/\s+/).filter(Boolean).slice(0, 2).join(".")
|
|
1860
|
+
: "";
|
|
1861
|
+
overflowOffenders.push({
|
|
1862
|
+
selector: tag + id + (className ? "." + className : ""),
|
|
1863
|
+
tag,
|
|
1864
|
+
text: (element.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120),
|
|
1865
|
+
overflow,
|
|
1866
|
+
left_overflow_px: leftOverflow,
|
|
1867
|
+
right_overflow_px: rightOverflow,
|
|
1868
|
+
viewport_width: viewportWidth,
|
|
1869
|
+
rect: {
|
|
1870
|
+
left: rect.left,
|
|
1871
|
+
right: rect.right,
|
|
1872
|
+
width: rect.width,
|
|
1873
|
+
},
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
overflowOffenders.sort((a, b) => b.overflow - a.overflow);
|
|
1877
|
+
const boundsOverflowPx = Math.max(
|
|
1878
|
+
0,
|
|
1879
|
+
scrollWidth - (clientWidth || viewportWidth),
|
|
1880
|
+
...overflowOffenders.map((offender) => offender.overflow),
|
|
1881
|
+
);
|
|
1882
|
+
return {
|
|
1883
|
+
url: location.href,
|
|
1884
|
+
title: document.title,
|
|
1885
|
+
text_length: text.length,
|
|
1886
|
+
text_sample: text.slice(0, 8000),
|
|
1887
|
+
body_text_sample: text.slice(0, 8000),
|
|
1888
|
+
viewport_width: viewportWidth,
|
|
1889
|
+
viewport_height: clientHeight || window.innerHeight,
|
|
1890
|
+
scroll_width: scrollWidth,
|
|
1891
|
+
client_width: clientWidth,
|
|
1892
|
+
overflow_px: Math.max(0, scrollWidth - (clientWidth || viewportWidth)),
|
|
1893
|
+
bounds_overflow_px: Math.round(boundsOverflowPx * 100) / 100,
|
|
1894
|
+
overflow_offender_count: overflowOffenders.length,
|
|
1895
|
+
overflow_offenders: overflowOffenders.slice(0, 10),
|
|
1896
|
+
};
|
|
1897
|
+
});
|
|
1898
|
+
result.frames.push({ index, attached: true, ...snapshot });
|
|
1899
|
+
} catch (error) {
|
|
1900
|
+
result.frames.push({ index, attached: false, error: String(error && error.message ? error.message : error).slice(0, 1000) });
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
result.frame_count = result.frames.filter((frame) => frame && frame.attached !== false && !frame.error).length;
|
|
1904
|
+
return result;
|
|
1905
|
+
}
|
|
1648
1906
|
function inventoryAppPathFromUrl(urlLike) {
|
|
1649
1907
|
const url = new URL(String(urlLike), targetUrl);
|
|
1650
1908
|
const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
|
|
@@ -1666,6 +1924,13 @@ function inventoryRouteUrl(expectedPath) {
|
|
|
1666
1924
|
function inventorySlugFromPath(path) {
|
|
1667
1925
|
return String(path || "route").split("/").filter(Boolean).pop()?.replace(/[^a-z0-9]+/gi, "-").toLowerCase() || "route";
|
|
1668
1926
|
}
|
|
1927
|
+
function inventorySlugFromViewport(viewport) {
|
|
1928
|
+
return String(viewport && viewport.name || "viewport").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase() || "viewport";
|
|
1929
|
+
}
|
|
1930
|
+
function inventoryScreenshotLabel(check, viewport, phase, path) {
|
|
1931
|
+
const viewportPart = check.run_all_viewports ? "-" + inventorySlugFromViewport(viewport) : "";
|
|
1932
|
+
return profileSlug + viewportPart + "-" + phase + "-" + inventorySlugFromPath(path);
|
|
1933
|
+
}
|
|
1669
1934
|
function inventoryCheckTimeout(check) {
|
|
1670
1935
|
const timeout = Number(check && check.timeout_ms);
|
|
1671
1936
|
return Number.isFinite(timeout) && timeout > 0 ? timeout : 45000;
|
|
@@ -1874,7 +2139,7 @@ async function collectRouteInventory(check, viewport) {
|
|
|
1874
2139
|
const snapshot = await collectInventoryRouteSnapshot(expectedRoute, "direct", check, error);
|
|
1875
2140
|
directRoutes.push(snapshot);
|
|
1876
2141
|
if (check.save_route_screenshots) {
|
|
1877
|
-
await saveScreenshot(
|
|
2142
|
+
await saveScreenshot(inventoryScreenshotLabel(check, viewport, "direct", expectedRoute.path)).catch(() => {});
|
|
1878
2143
|
}
|
|
1879
2144
|
if (error) failures.push({ code: "direct_route_unhealthy", name: expectedRoute.name || null, path: expectedRoute.path, error });
|
|
1880
2145
|
else if (snapshot.actual_app_path !== normalizeRoutePath(expectedRoute.path)) failures.push({ code: "direct_route_wrong_path", name: expectedRoute.name || null, path: expectedRoute.path, actual_app_path: snapshot.actual_app_path });
|
|
@@ -1908,7 +2173,7 @@ async function collectRouteInventory(check, viewport) {
|
|
|
1908
2173
|
result.snapshot = await collectInventoryRouteSnapshot(expectedRoute, "clickthrough", check, result.error || "");
|
|
1909
2174
|
clickthroughs.push(result);
|
|
1910
2175
|
if (check.save_route_screenshots) {
|
|
1911
|
-
await saveScreenshot(
|
|
2176
|
+
await saveScreenshot(inventoryScreenshotLabel(check, viewport, "click", expectedRoute.path)).catch(() => {});
|
|
1912
2177
|
}
|
|
1913
2178
|
if (result.error && result.error !== "source_link_not_found") failures.push({ code: "source_link_clickthrough_unhealthy", name: expectedRoute.name || null, path: expectedRoute.path, error: result.error });
|
|
1914
2179
|
else if (result.snapshot.actual_app_path !== normalizeRoutePath(expectedRoute.path)) failures.push({ code: "source_link_clickthrough_wrong_path", name: expectedRoute.name || null, path: expectedRoute.path, actual_app_path: result.snapshot.actual_app_path });
|
|
@@ -2042,6 +2307,7 @@ async function captureViewport(viewport) {
|
|
|
2042
2307
|
evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
|
|
2043
2308
|
}));
|
|
2044
2309
|
const selectors = {};
|
|
2310
|
+
const frames = {};
|
|
2045
2311
|
const text_sequences = {};
|
|
2046
2312
|
const text_matches = {};
|
|
2047
2313
|
for (const check of profile.checks || []) {
|
|
@@ -2055,6 +2321,10 @@ async function captureViewport(viewport) {
|
|
|
2055
2321
|
if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
|
|
2056
2322
|
text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
|
|
2057
2323
|
}
|
|
2324
|
+
if ((check.type === "frame_text_visible" || check.type === "frame_no_horizontal_overflow") && check.selector) {
|
|
2325
|
+
selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
|
|
2326
|
+
frames[check.selector] = frames[check.selector] || await frameEvidence(check.selector);
|
|
2327
|
+
}
|
|
2058
2328
|
}
|
|
2059
2329
|
const screenshotLabel = profileSlug + "-" + viewport.name;
|
|
2060
2330
|
try {
|
|
@@ -2115,6 +2385,7 @@ async function captureViewport(viewport) {
|
|
|
2115
2385
|
bounds_overflow_px: dom.bounds_overflow_px,
|
|
2116
2386
|
overflow_offenders: dom.overflow_offenders || [],
|
|
2117
2387
|
selectors,
|
|
2388
|
+
frames,
|
|
2118
2389
|
text_sequences,
|
|
2119
2390
|
text_matches,
|
|
2120
2391
|
route_inventory: routeInventory,
|
|
@@ -2150,6 +2421,20 @@ function buildProfileEvidence(currentViewports) {
|
|
|
2150
2421
|
overflow_px: currentViewports.map((viewport) => viewport.overflow_px),
|
|
2151
2422
|
bounds_overflow_px: currentViewports.map((viewport) => viewport.bounds_overflow_px),
|
|
2152
2423
|
overflow_offender_counts: currentViewports.map((viewport) => (viewport.overflow_offenders || []).length),
|
|
2424
|
+
frames: currentViewports
|
|
2425
|
+
.filter((viewport) => viewport.frames)
|
|
2426
|
+
.map((viewport) => ({
|
|
2427
|
+
viewport: viewport.name,
|
|
2428
|
+
selectors: Object.entries(viewport.frames || {}).map(([selector, frameSet]) => ({
|
|
2429
|
+
selector,
|
|
2430
|
+
count: frameSet && frameSet.count,
|
|
2431
|
+
frame_count: frameSet && frameSet.frame_count,
|
|
2432
|
+
max_bounds_overflow_px: Math.max(
|
|
2433
|
+
0,
|
|
2434
|
+
...((frameSet && Array.isArray(frameSet.frames) ? frameSet.frames : [])).map((frame) => horizontalBoundsOverflowPx(frame)),
|
|
2435
|
+
),
|
|
2436
|
+
})),
|
|
2437
|
+
})),
|
|
2153
2438
|
route_inventory: currentViewports
|
|
2154
2439
|
.filter((viewport) => viewport.route_inventory)
|
|
2155
2440
|
.map((viewport) => ({
|