@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/dist/profile.cjs CHANGED
@@ -58,6 +58,8 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
58
58
  "selector_visible",
59
59
  "selector_count_at_least",
60
60
  "selector_text_order",
61
+ "frame_text_visible",
62
+ "frame_no_horizontal_overflow",
61
63
  "text_visible",
62
64
  "text_absent",
63
65
  "route_inventory",
@@ -346,6 +348,13 @@ function normalizeExpectedTexts(value, index) {
346
348
  if (!texts.length) throw new Error(`checks[${index}] selector_text_order expected_texts must contain non-empty strings.`);
347
349
  return texts;
348
350
  }
351
+ function normalizeStringList(value, label) {
352
+ if (value === void 0) return void 0;
353
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
354
+ const values = value.map((item) => String(item).replace(/\s+/g, " ").trim()).filter(Boolean);
355
+ if (!values.length) throw new Error(`${label} must contain non-empty strings.`);
356
+ return values;
357
+ }
349
358
  function normalizeCheck(input, index) {
350
359
  if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
351
360
  const type = stringValue(input.type);
@@ -356,6 +365,12 @@ function normalizeCheck(input, index) {
356
365
  if ((type === "selector_visible" || type === "selector_count_at_least") && !stringValue(input.selector)) {
357
366
  throw new Error(`checks[${index}] ${type} requires selector.`);
358
367
  }
368
+ if ((type === "frame_text_visible" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
369
+ throw new Error(`checks[${index}] ${type} requires selector.`);
370
+ }
371
+ if (type === "frame_text_visible" && !stringValue(input.text) && !stringValue(input.pattern)) {
372
+ throw new Error(`checks[${index}] frame_text_visible requires text or pattern.`);
373
+ }
359
374
  if ((type === "text_visible" || type === "text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
360
375
  throw new Error(`checks[${index}] ${type} requires text or pattern.`);
361
376
  }
@@ -388,6 +403,10 @@ function normalizeCheck(input, index) {
388
403
  text: stringValue(input.text),
389
404
  pattern: stringValue(input.pattern),
390
405
  flags: stringValue(input.flags),
406
+ allowed_console_texts: normalizeStringList(input.allowed_console_texts ?? input.allowedConsoleTexts ?? input.allow_console_texts ?? input.allowConsoleTexts, `checks[${index}] allowed_console_texts`),
407
+ allowed_console_patterns: normalizeStringList(input.allowed_console_patterns ?? input.allowedConsolePatterns ?? input.allow_console_patterns ?? input.allowConsolePatterns, `checks[${index}] allowed_console_patterns`),
408
+ 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`),
409
+ 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`),
391
410
  min_count: numberValue(input.min_count),
392
411
  max_overflow_px: numberValue(input.max_overflow_px),
393
412
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
@@ -506,6 +525,22 @@ function textOrderMatch(texts, expectedTexts) {
506
525
  }
507
526
  return { matched: true, positions };
508
527
  }
528
+ function frameEvidenceForSelector(viewport, selector) {
529
+ const container = viewport.frames?.[selector];
530
+ if (!isRecord(container)) return [];
531
+ const frames = Array.isArray(container.frames) ? container.frames : [];
532
+ return frames.filter((frame) => isRecord(frame));
533
+ }
534
+ function frameTextSample(frame) {
535
+ const parts = [
536
+ frame.title,
537
+ frame.text_sample,
538
+ frame.body_text_sample,
539
+ frame.body_text,
540
+ frame.text
541
+ ];
542
+ return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
543
+ }
509
544
  function summarizeRouteInventory(viewport, inventory) {
510
545
  const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
511
546
  const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
@@ -535,6 +570,18 @@ function matchText(sample, check) {
535
570
  }
536
571
  return sample.includes(check.text || "");
537
572
  }
573
+ function matchesAllowedMessage(message, texts, patterns) {
574
+ const sample = String(message || "");
575
+ if (texts?.some((text) => sample.includes(text))) return true;
576
+ for (const pattern of patterns || []) {
577
+ try {
578
+ if (new RegExp(pattern).test(sample)) return true;
579
+ } catch {
580
+ continue;
581
+ }
582
+ }
583
+ return false;
584
+ }
538
585
  function normalizeRoutePath(path) {
539
586
  const value = path || "/";
540
587
  if (value === "/") return "/";
@@ -670,6 +717,67 @@ function assessCheckFromEvidence(check, evidence) {
670
717
  message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
671
718
  };
672
719
  }
720
+ if (check.type === "frame_text_visible") {
721
+ const key = selectorKey(check);
722
+ const results = viewports.map((viewport) => {
723
+ const frames = frameEvidenceForSelector(viewport, key);
724
+ const matches = frames.map((frame, index) => ({ index, frame, matched: matchText(frameTextSample(frame), check) })).filter((item) => item.matched);
725
+ return {
726
+ viewport: viewport.name,
727
+ frame_count: frames.length,
728
+ matched_count: matches.length,
729
+ matched: matches.length > 0,
730
+ urls: frames.map((frame) => String(frame.url || "")).filter(Boolean).slice(0, 10),
731
+ samples: matches.map((item) => frameTextSample(item.frame).replace(/\s+/g, " ").trim().slice(0, 240)).slice(0, 3)
732
+ };
733
+ });
734
+ const failed = results.filter((result) => !result.matched).length;
735
+ return {
736
+ type: check.type,
737
+ label: checkLabel(check),
738
+ status: failed ? "failed" : "passed",
739
+ evidence: {
740
+ selector: key,
741
+ text: check.text || null,
742
+ pattern: check.pattern || null,
743
+ viewports: results.map((result) => toJsonValue(result))
744
+ },
745
+ message: failed ? `Frame selector ${key} did not contain expected text in ${failed} viewport(s).` : void 0
746
+ };
747
+ }
748
+ if (check.type === "frame_no_horizontal_overflow") {
749
+ const key = selectorKey(check);
750
+ const maxOverflow = check.max_overflow_px ?? 4;
751
+ const results = viewports.map((viewport) => {
752
+ const frames = frameEvidenceForSelector(viewport, key);
753
+ const frameOverflows = frames.map((frame, index) => ({
754
+ index,
755
+ url: String(frame.url || ""),
756
+ overflow_px: horizontalBoundsOverflowPx(frame),
757
+ offender_count: boundsOffendersForEvidence(frame).length
758
+ }));
759
+ const maxFrameOverflow = frameOverflows.reduce((max, frame) => Math.max(max, frame.overflow_px), 0);
760
+ return {
761
+ viewport: viewport.name,
762
+ frame_count: frames.length,
763
+ max_overflow_px: roundPixels(maxFrameOverflow),
764
+ failed_frame_count: frameOverflows.filter((frame) => frame.overflow_px > maxOverflow).length,
765
+ frames: frameOverflows.map((frame) => toJsonValue(frame))
766
+ };
767
+ });
768
+ const failed = results.filter((result) => result.frame_count < 1 || result.failed_frame_count > 0).length;
769
+ return {
770
+ type: check.type,
771
+ label: checkLabel(check),
772
+ status: failed ? "failed" : "passed",
773
+ evidence: {
774
+ selector: key,
775
+ max_overflow_px: maxOverflow,
776
+ viewports: results.map((result) => toJsonValue(result))
777
+ },
778
+ message: failed ? `Frame selector ${key} overflow exceeded ${maxOverflow}px or was missing in ${failed} viewport(s).` : void 0
779
+ };
780
+ }
673
781
  if (check.type === "text_visible" || check.type === "text_absent") {
674
782
  const key = textKey(check);
675
783
  const expectedVisible = check.type === "text_visible";
@@ -760,14 +868,28 @@ function assessCheckFromEvidence(check, evidence) {
760
868
  };
761
869
  }
762
870
  if (check.type === "no_fatal_console_errors") {
763
- const fatalCount = (evidence.console?.fatal_count || 0) + (evidence.page_errors?.length || 0);
871
+ const fatalConsoleEvents = (evidence.console?.events || []).filter((event) => event.type === "error" || event.type === "assert");
872
+ const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
873
+ const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
874
+ const pageErrors = evidence.page_errors || [];
875
+ const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
876
+ const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
877
+ const fatalCount = unallowedConsoleEvents.length + unallowedPageErrors.length;
764
878
  return {
765
879
  type: check.type,
766
880
  label: checkLabel(check),
767
881
  status: fatalCount ? "failed" : "passed",
768
882
  evidence: {
769
- console_fatal_count: evidence.console?.fatal_count || 0,
770
- page_error_count: evidence.page_errors?.length || 0
883
+ console_fatal_count: unallowedConsoleEvents.length,
884
+ page_error_count: unallowedPageErrors.length,
885
+ total_console_fatal_count: fatalConsoleEvents.length,
886
+ total_page_error_count: pageErrors.length,
887
+ allowed_console_fatal_count: allowedConsoleEvents.length,
888
+ allowed_page_error_count: allowedPageErrors.length,
889
+ allowed_console_texts: check.allowed_console_texts || [],
890
+ allowed_console_patterns: check.allowed_console_patterns || [],
891
+ allowed_page_error_texts: check.allowed_page_error_texts || [],
892
+ allowed_page_error_patterns: check.allowed_page_error_patterns || []
771
893
  },
772
894
  message: fatalCount ? `${fatalCount} fatal browser error(s) were captured.` : void 0
773
895
  };
@@ -1023,6 +1145,16 @@ function textMatches(sample, check) {
1023
1145
  }
1024
1146
  return String(sample || "").includes(check.text || "");
1025
1147
  }
1148
+ function matchesAllowedMessage(message, texts, patterns) {
1149
+ const sample = String(message || "");
1150
+ if ((texts || []).some((text) => sample.includes(text))) return true;
1151
+ for (const pattern of patterns || []) {
1152
+ try {
1153
+ if (new RegExp(pattern).test(sample)) return true;
1154
+ } catch {}
1155
+ }
1156
+ return false;
1157
+ }
1026
1158
  function textSequenceForCheck(viewport, check) {
1027
1159
  const key = check.selector || "";
1028
1160
  const sequence = viewport.text_sequences && viewport.text_sequences[key];
@@ -1045,6 +1177,21 @@ function textOrderMatch(texts, expectedTexts) {
1045
1177
  }
1046
1178
  return { matched: true, positions };
1047
1179
  }
1180
+ function frameEvidenceForSelector(viewport, selector) {
1181
+ const container = viewport.frames && viewport.frames[selector || ""];
1182
+ if (!container || typeof container !== "object" || Array.isArray(container)) return [];
1183
+ const frames = Array.isArray(container.frames) ? container.frames : [];
1184
+ return frames.filter((frame) => frame && typeof frame === "object" && !Array.isArray(frame));
1185
+ }
1186
+ function frameTextSample(frame) {
1187
+ return [
1188
+ frame && frame.title,
1189
+ frame && frame.text_sample,
1190
+ frame && frame.body_text_sample,
1191
+ frame && frame.body_text,
1192
+ frame && frame.text,
1193
+ ].map((part) => String(part || "")).filter(Boolean).join(" ");
1194
+ }
1048
1195
  function summarizeRouteInventory(viewport, inventory) {
1049
1196
  const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
1050
1197
  const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
@@ -1291,6 +1438,62 @@ function assessProfile(profile, evidence) {
1291
1438
  });
1292
1439
  continue;
1293
1440
  }
1441
+ if (check.type === "frame_text_visible") {
1442
+ const selector = check.selector || "";
1443
+ const results = viewports.map((viewport) => {
1444
+ const frames = frameEvidenceForSelector(viewport, selector);
1445
+ const matches = frames
1446
+ .map((frame, index) => ({ index, frame, matched: textMatches(frameTextSample(frame), check) }))
1447
+ .filter((item) => item.matched);
1448
+ return {
1449
+ viewport: viewport.name,
1450
+ frame_count: frames.length,
1451
+ matched_count: matches.length,
1452
+ matched: matches.length > 0,
1453
+ urls: frames.map((frame) => String(frame.url || "")).filter(Boolean).slice(0, 10),
1454
+ samples: matches.map((item) => frameTextSample(item.frame).replace(/\s+/g, " ").trim().slice(0, 240)).slice(0, 3),
1455
+ };
1456
+ });
1457
+ const failed = results.filter((result) => !result.matched).length;
1458
+ checks.push({
1459
+ type: check.type,
1460
+ label: check.label || check.type,
1461
+ status: failed ? "failed" : "passed",
1462
+ evidence: { selector, text: check.text || null, pattern: check.pattern || null, viewports: results },
1463
+ message: failed ? "Frame selector " + selector + " did not contain expected text in " + failed + " viewport(s)." : undefined,
1464
+ });
1465
+ continue;
1466
+ }
1467
+ if (check.type === "frame_no_horizontal_overflow") {
1468
+ const selector = check.selector || "";
1469
+ const maxOverflow = check.max_overflow_px == null ? 4 : check.max_overflow_px;
1470
+ const results = viewports.map((viewport) => {
1471
+ const frames = frameEvidenceForSelector(viewport, selector);
1472
+ const frameOverflows = frames.map((frame, index) => ({
1473
+ index,
1474
+ url: String(frame.url || ""),
1475
+ overflow_px: horizontalBoundsOverflowPx(frame),
1476
+ offender_count: boundsOffendersForEvidence(frame).length,
1477
+ }));
1478
+ const maxFrameOverflow = frameOverflows.reduce((max, frame) => Math.max(max, frame.overflow_px), 0);
1479
+ return {
1480
+ viewport: viewport.name,
1481
+ frame_count: frames.length,
1482
+ max_overflow_px: roundPixels(maxFrameOverflow),
1483
+ failed_frame_count: frameOverflows.filter((frame) => frame.overflow_px > maxOverflow).length,
1484
+ frames: frameOverflows,
1485
+ };
1486
+ });
1487
+ const failed = results.filter((result) => result.frame_count < 1 || result.failed_frame_count > 0).length;
1488
+ checks.push({
1489
+ type: check.type,
1490
+ label: check.label || check.type,
1491
+ status: failed ? "failed" : "passed",
1492
+ evidence: { selector, max_overflow_px: maxOverflow, viewports: results },
1493
+ message: failed ? "Frame selector " + selector + " overflow exceeded " + maxOverflow + "px or was missing in " + failed + " viewport(s)." : undefined,
1494
+ });
1495
+ continue;
1496
+ }
1294
1497
  if (check.type === "text_visible" || check.type === "text_absent") {
1295
1498
  const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
1296
1499
  const expectedVisible = check.type === "text_visible";
@@ -1386,12 +1589,29 @@ function assessProfile(profile, evidence) {
1386
1589
  continue;
1387
1590
  }
1388
1591
  if (check.type === "no_fatal_console_errors") {
1389
- const fatalCount = ((evidence.console && evidence.console.fatal_count) || 0) + ((evidence.page_errors || []).length);
1592
+ const fatalConsoleEvents = ((evidence.console && evidence.console.events) || []).filter((event) => event && (event.type === "error" || event.type === "assert"));
1593
+ const allowedConsoleEvents = fatalConsoleEvents.filter((event) => matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
1594
+ const unallowedConsoleEvents = fatalConsoleEvents.filter((event) => !matchesAllowedMessage(event.text, check.allowed_console_texts, check.allowed_console_patterns));
1595
+ const pageErrors = evidence.page_errors || [];
1596
+ const allowedPageErrors = pageErrors.filter((error) => matchesAllowedMessage(error && error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
1597
+ const unallowedPageErrors = pageErrors.filter((error) => !matchesAllowedMessage(error && error.message, check.allowed_page_error_texts, check.allowed_page_error_patterns));
1598
+ const fatalCount = unallowedConsoleEvents.length + unallowedPageErrors.length;
1390
1599
  checks.push({
1391
1600
  type: check.type,
1392
1601
  label: check.label || check.type,
1393
1602
  status: fatalCount ? "failed" : "passed",
1394
- evidence: { console_fatal_count: (evidence.console && evidence.console.fatal_count) || 0, page_error_count: (evidence.page_errors || []).length },
1603
+ evidence: {
1604
+ console_fatal_count: unallowedConsoleEvents.length,
1605
+ page_error_count: unallowedPageErrors.length,
1606
+ total_console_fatal_count: fatalConsoleEvents.length,
1607
+ total_page_error_count: pageErrors.length,
1608
+ allowed_console_fatal_count: allowedConsoleEvents.length,
1609
+ allowed_page_error_count: allowedPageErrors.length,
1610
+ allowed_console_texts: check.allowed_console_texts || [],
1611
+ allowed_console_patterns: check.allowed_console_patterns || [],
1612
+ allowed_page_error_texts: check.allowed_page_error_texts || [],
1613
+ allowed_page_error_patterns: check.allowed_page_error_patterns || [],
1614
+ },
1395
1615
  message: fatalCount ? String(fatalCount) + " fatal browser error(s) were captured." : undefined,
1396
1616
  });
1397
1617
  continue;
@@ -1688,6 +1908,108 @@ async function selectorTextSequence(selector) {
1688
1908
  };
1689
1909
  }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
1690
1910
  }
1911
+ async function frameEvidence(selector) {
1912
+ const result = { selector, count: 0, frame_count: 0, frames: [], errors: [] };
1913
+ let handles = [];
1914
+ try {
1915
+ const locator = page.locator(selector);
1916
+ result.count = await locator.count();
1917
+ handles = await locator.elementHandles();
1918
+ } catch (error) {
1919
+ result.errors.push(String(error && error.message ? error.message : error).slice(0, 500));
1920
+ return result;
1921
+ }
1922
+ for (let index = 0; index < handles.length; index += 1) {
1923
+ const handle = handles[index];
1924
+ try {
1925
+ const frame = typeof handle.contentFrame === "function" ? await handle.contentFrame() : null;
1926
+ if (!frame) {
1927
+ result.frames.push({ index, attached: false, error: "content_frame_unavailable" });
1928
+ continue;
1929
+ }
1930
+ const snapshot = await frame.evaluate(() => {
1931
+ const body = document.body;
1932
+ const documentElement = document.documentElement;
1933
+ const text = (body ? body.innerText : "").replace(/\s+/g, " ").trim();
1934
+ const clientWidth = documentElement ? documentElement.clientWidth : window.innerWidth;
1935
+ const clientHeight = documentElement ? documentElement.clientHeight : window.innerHeight;
1936
+ const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
1937
+ const viewportWidth = clientWidth || window.innerWidth;
1938
+ const overflowOffenders = [];
1939
+ function isContainedByHorizontalScroller(element) {
1940
+ let current = element.parentElement;
1941
+ while (current && current !== body && current !== documentElement) {
1942
+ const style = window.getComputedStyle(current);
1943
+ const overflowX = style.overflowX || style.overflow || "";
1944
+ if ((overflowX === "auto" || overflowX === "scroll") && current.scrollWidth > current.clientWidth + 1) {
1945
+ const currentRect = current.getBoundingClientRect();
1946
+ const contained = currentRect.left >= -0.5 && currentRect.right <= viewportWidth + 0.5;
1947
+ if (contained) return true;
1948
+ }
1949
+ current = current.parentElement;
1950
+ }
1951
+ return false;
1952
+ }
1953
+ for (const element of Array.from(body ? body.querySelectorAll("*") : [])) {
1954
+ const rect = element.getBoundingClientRect();
1955
+ if (!rect || rect.width < 1 || rect.height < 1) continue;
1956
+ const style = window.getComputedStyle(element);
1957
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") continue;
1958
+ const leftOverflow = Math.max(0, -rect.left);
1959
+ const rightOverflow = Math.max(0, rect.right - viewportWidth);
1960
+ const overflow = Math.max(leftOverflow, rightOverflow);
1961
+ if (overflow <= 0.5) continue;
1962
+ if (isContainedByHorizontalScroller(element)) continue;
1963
+ const tag = element.tagName ? element.tagName.toLowerCase() : "element";
1964
+ const id = element.id ? "#" + element.id : "";
1965
+ const className = typeof element.className === "string"
1966
+ ? element.className.trim().split(/\s+/).filter(Boolean).slice(0, 2).join(".")
1967
+ : "";
1968
+ overflowOffenders.push({
1969
+ selector: tag + id + (className ? "." + className : ""),
1970
+ tag,
1971
+ text: (element.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120),
1972
+ overflow,
1973
+ left_overflow_px: leftOverflow,
1974
+ right_overflow_px: rightOverflow,
1975
+ viewport_width: viewportWidth,
1976
+ rect: {
1977
+ left: rect.left,
1978
+ right: rect.right,
1979
+ width: rect.width,
1980
+ },
1981
+ });
1982
+ }
1983
+ overflowOffenders.sort((a, b) => b.overflow - a.overflow);
1984
+ const boundsOverflowPx = Math.max(
1985
+ 0,
1986
+ scrollWidth - (clientWidth || viewportWidth),
1987
+ ...overflowOffenders.map((offender) => offender.overflow),
1988
+ );
1989
+ return {
1990
+ url: location.href,
1991
+ title: document.title,
1992
+ text_length: text.length,
1993
+ text_sample: text.slice(0, 8000),
1994
+ body_text_sample: text.slice(0, 8000),
1995
+ viewport_width: viewportWidth,
1996
+ viewport_height: clientHeight || window.innerHeight,
1997
+ scroll_width: scrollWidth,
1998
+ client_width: clientWidth,
1999
+ overflow_px: Math.max(0, scrollWidth - (clientWidth || viewportWidth)),
2000
+ bounds_overflow_px: Math.round(boundsOverflowPx * 100) / 100,
2001
+ overflow_offender_count: overflowOffenders.length,
2002
+ overflow_offenders: overflowOffenders.slice(0, 10),
2003
+ };
2004
+ });
2005
+ result.frames.push({ index, attached: true, ...snapshot });
2006
+ } catch (error) {
2007
+ result.frames.push({ index, attached: false, error: String(error && error.message ? error.message : error).slice(0, 1000) });
2008
+ }
2009
+ }
2010
+ result.frame_count = result.frames.filter((frame) => frame && frame.attached !== false && !frame.error).length;
2011
+ return result;
2012
+ }
1691
2013
  function inventoryAppPathFromUrl(urlLike) {
1692
2014
  const url = new URL(String(urlLike), targetUrl);
1693
2015
  const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
@@ -2092,6 +2414,7 @@ async function captureViewport(viewport) {
2092
2414
  evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
2093
2415
  }));
2094
2416
  const selectors = {};
2417
+ const frames = {};
2095
2418
  const text_sequences = {};
2096
2419
  const text_matches = {};
2097
2420
  for (const check of profile.checks || []) {
@@ -2105,6 +2428,10 @@ async function captureViewport(viewport) {
2105
2428
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
2106
2429
  text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
2107
2430
  }
2431
+ if ((check.type === "frame_text_visible" || check.type === "frame_no_horizontal_overflow") && check.selector) {
2432
+ selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
2433
+ frames[check.selector] = frames[check.selector] || await frameEvidence(check.selector);
2434
+ }
2108
2435
  }
2109
2436
  const screenshotLabel = profileSlug + "-" + viewport.name;
2110
2437
  try {
@@ -2165,6 +2492,7 @@ async function captureViewport(viewport) {
2165
2492
  bounds_overflow_px: dom.bounds_overflow_px,
2166
2493
  overflow_offenders: dom.overflow_offenders || [],
2167
2494
  selectors,
2495
+ frames,
2168
2496
  text_sequences,
2169
2497
  text_matches,
2170
2498
  route_inventory: routeInventory,
@@ -2200,6 +2528,20 @@ function buildProfileEvidence(currentViewports) {
2200
2528
  overflow_px: currentViewports.map((viewport) => viewport.overflow_px),
2201
2529
  bounds_overflow_px: currentViewports.map((viewport) => viewport.bounds_overflow_px),
2202
2530
  overflow_offender_counts: currentViewports.map((viewport) => (viewport.overflow_offenders || []).length),
2531
+ frames: currentViewports
2532
+ .filter((viewport) => viewport.frames)
2533
+ .map((viewport) => ({
2534
+ viewport: viewport.name,
2535
+ selectors: Object.entries(viewport.frames || {}).map(([selector, frameSet]) => ({
2536
+ selector,
2537
+ count: frameSet && frameSet.count,
2538
+ frame_count: frameSet && frameSet.frame_count,
2539
+ max_bounds_overflow_px: Math.max(
2540
+ 0,
2541
+ ...((frameSet && Array.isArray(frameSet.frames) ? frameSet.frames : [])).map((frame) => horizontalBoundsOverflowPx(frame)),
2542
+ ),
2543
+ })),
2544
+ })),
2203
2545
  route_inventory: currentViewports
2204
2546
  .filter((viewport) => viewport.route_inventory)
2205
2547
  .map((viewport) => ({
@@ -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", "selector_text_order", "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", "frame_text_visible", "frame_no_horizontal_overflow", "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];
@@ -77,6 +77,10 @@ interface RiddleProofProfileCheck {
77
77
  text?: string;
78
78
  pattern?: string;
79
79
  flags?: string;
80
+ allowed_console_texts?: string[];
81
+ allowed_console_patterns?: string[];
82
+ allowed_page_error_texts?: string[];
83
+ allowed_page_error_patterns?: string[];
80
84
  min_count?: number;
81
85
  max_overflow_px?: number;
82
86
  timeout_ms?: number;
@@ -139,6 +143,7 @@ interface RiddleProofProfileViewportEvidence {
139
143
  count: number;
140
144
  visible_count: number;
141
145
  }>;
146
+ frames?: Record<string, Record<string, JsonValue>>;
142
147
  text_sequences?: Record<string, Record<string, JsonValue>>;
143
148
  text_matches?: Record<string, boolean>;
144
149
  route_inventory?: 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", "selector_text_order", "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", "frame_text_visible", "frame_no_horizontal_overflow", "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];
@@ -77,6 +77,10 @@ interface RiddleProofProfileCheck {
77
77
  text?: string;
78
78
  pattern?: string;
79
79
  flags?: string;
80
+ allowed_console_texts?: string[];
81
+ allowed_console_patterns?: string[];
82
+ allowed_page_error_texts?: string[];
83
+ allowed_page_error_patterns?: string[];
80
84
  min_count?: number;
81
85
  max_overflow_px?: number;
82
86
  timeout_ms?: number;
@@ -139,6 +143,7 @@ interface RiddleProofProfileViewportEvidence {
139
143
  count: number;
140
144
  visible_count: number;
141
145
  }>;
146
+ frames?: Record<string, Record<string, JsonValue>>;
142
147
  text_sequences?: Record<string, Record<string, JsonValue>>;
143
148
  text_matches?: Record<string, boolean>;
144
149
  route_inventory?: 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-KZRDAMI5.js";
22
+ } from "./chunk-L4FLSU7L.js";
23
23
  export {
24
24
  RIDDLE_PROOF_PROFILE_CHECK_TYPES,
25
25
  RIDDLE_PROOF_PROFILE_EVIDENCE_VERSION,
@@ -292,7 +292,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
292
292
  blocking?: boolean;
293
293
  details?: Record<string, unknown>;
294
294
  ok: boolean;
295
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
295
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
296
296
  state_path: string;
297
297
  stage: any;
298
298
  summary: string;
@@ -382,7 +382,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
382
382
  continueWithStage?: WorkflowStage | null;
383
383
  blocking?: boolean;
384
384
  details?: Record<string, unknown>;
385
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
385
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
386
386
  state_path: string;
387
387
  stage: any;
388
388
  checkpoint: string;
@@ -659,7 +659,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
659
659
  error?: undefined;
660
660
  } | {
661
661
  ok: boolean;
662
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
662
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
663
663
  state_path: string;
664
664
  stage: any;
665
665
  summary: string;
@@ -292,7 +292,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
292
292
  blocking?: boolean;
293
293
  details?: Record<string, unknown>;
294
294
  ok: boolean;
295
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
295
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
296
296
  state_path: string;
297
297
  stage: any;
298
298
  summary: string;
@@ -382,7 +382,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
382
382
  continueWithStage?: WorkflowStage | null;
383
383
  blocking?: boolean;
384
384
  details?: Record<string, unknown>;
385
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
385
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
386
386
  state_path: string;
387
387
  stage: any;
388
388
  checkpoint: string;
@@ -659,7 +659,7 @@ declare function executeWorkflow(params: WorkflowParams, pluginConfig: any, reso
659
659
  error?: undefined;
660
660
  } | {
661
661
  ok: boolean;
662
- action: "setup" | "recon" | "author" | "implement" | "verify" | "ship" | "run";
662
+ action: "author" | "recon" | "ship" | "implement" | "verify" | "setup" | "run";
663
663
  state_path: string;
664
664
  stage: any;
665
665
  summary: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@riddledc/riddle-proof",
3
- "version": "0.7.37",
3
+ "version": "0.7.39",
4
4
  "description": "Reusable Riddle Proof contracts and helpers for evidence-backed agent changes.",
5
5
  "license": "MIT",
6
6
  "author": "RiddleDC",