@riddledc/riddle-proof 0.7.37 → 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/dist/cli.cjs CHANGED
@@ -6888,6 +6888,8 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
6888
6888
  "selector_visible",
6889
6889
  "selector_count_at_least",
6890
6890
  "selector_text_order",
6891
+ "frame_text_visible",
6892
+ "frame_no_horizontal_overflow",
6891
6893
  "text_visible",
6892
6894
  "text_absent",
6893
6895
  "route_inventory",
@@ -7186,6 +7188,12 @@ function normalizeCheck(input, index) {
7186
7188
  if ((type === "selector_visible" || type === "selector_count_at_least") && !stringValue2(input.selector)) {
7187
7189
  throw new Error(`checks[${index}] ${type} requires selector.`);
7188
7190
  }
7191
+ if ((type === "frame_text_visible" || type === "frame_no_horizontal_overflow") && !stringValue2(input.selector)) {
7192
+ throw new Error(`checks[${index}] ${type} requires selector.`);
7193
+ }
7194
+ if (type === "frame_text_visible" && !stringValue2(input.text) && !stringValue2(input.pattern)) {
7195
+ throw new Error(`checks[${index}] frame_text_visible requires text or pattern.`);
7196
+ }
7189
7197
  if ((type === "text_visible" || type === "text_absent") && !stringValue2(input.text) && !stringValue2(input.pattern)) {
7190
7198
  throw new Error(`checks[${index}] ${type} requires text or pattern.`);
7191
7199
  }
@@ -7336,6 +7344,22 @@ function textOrderMatch(texts, expectedTexts) {
7336
7344
  }
7337
7345
  return { matched: true, positions };
7338
7346
  }
7347
+ function frameEvidenceForSelector(viewport, selector) {
7348
+ const container = viewport.frames?.[selector];
7349
+ if (!isRecord(container)) return [];
7350
+ const frames = Array.isArray(container.frames) ? container.frames : [];
7351
+ return frames.filter((frame) => isRecord(frame));
7352
+ }
7353
+ function frameTextSample(frame) {
7354
+ const parts = [
7355
+ frame.title,
7356
+ frame.text_sample,
7357
+ frame.body_text_sample,
7358
+ frame.body_text,
7359
+ frame.text
7360
+ ];
7361
+ return parts.map((part) => String(part || "")).filter(Boolean).join(" ");
7362
+ }
7339
7363
  function summarizeRouteInventory(viewport, inventory) {
7340
7364
  const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
7341
7365
  const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
@@ -7500,6 +7524,67 @@ function assessCheckFromEvidence(check, evidence) {
7500
7524
  message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
7501
7525
  };
7502
7526
  }
7527
+ if (check.type === "frame_text_visible") {
7528
+ const key = selectorKey(check);
7529
+ const results = viewports.map((viewport) => {
7530
+ const frames = frameEvidenceForSelector(viewport, key);
7531
+ const matches = frames.map((frame, index) => ({ index, frame, matched: matchText(frameTextSample(frame), check) })).filter((item) => item.matched);
7532
+ return {
7533
+ viewport: viewport.name,
7534
+ frame_count: frames.length,
7535
+ matched_count: matches.length,
7536
+ matched: matches.length > 0,
7537
+ urls: frames.map((frame) => String(frame.url || "")).filter(Boolean).slice(0, 10),
7538
+ samples: matches.map((item) => frameTextSample(item.frame).replace(/\s+/g, " ").trim().slice(0, 240)).slice(0, 3)
7539
+ };
7540
+ });
7541
+ const failed = results.filter((result) => !result.matched).length;
7542
+ return {
7543
+ type: check.type,
7544
+ label: checkLabel(check),
7545
+ status: failed ? "failed" : "passed",
7546
+ evidence: {
7547
+ selector: key,
7548
+ text: check.text || null,
7549
+ pattern: check.pattern || null,
7550
+ viewports: results.map((result) => toJsonValue(result))
7551
+ },
7552
+ message: failed ? `Frame selector ${key} did not contain expected text in ${failed} viewport(s).` : void 0
7553
+ };
7554
+ }
7555
+ if (check.type === "frame_no_horizontal_overflow") {
7556
+ const key = selectorKey(check);
7557
+ const maxOverflow = check.max_overflow_px ?? 4;
7558
+ const results = viewports.map((viewport) => {
7559
+ const frames = frameEvidenceForSelector(viewport, key);
7560
+ const frameOverflows = frames.map((frame, index) => ({
7561
+ index,
7562
+ url: String(frame.url || ""),
7563
+ overflow_px: horizontalBoundsOverflowPx(frame),
7564
+ offender_count: boundsOffendersForEvidence(frame).length
7565
+ }));
7566
+ const maxFrameOverflow = frameOverflows.reduce((max, frame) => Math.max(max, frame.overflow_px), 0);
7567
+ return {
7568
+ viewport: viewport.name,
7569
+ frame_count: frames.length,
7570
+ max_overflow_px: roundPixels(maxFrameOverflow),
7571
+ failed_frame_count: frameOverflows.filter((frame) => frame.overflow_px > maxOverflow).length,
7572
+ frames: frameOverflows.map((frame) => toJsonValue(frame))
7573
+ };
7574
+ });
7575
+ const failed = results.filter((result) => result.frame_count < 1 || result.failed_frame_count > 0).length;
7576
+ return {
7577
+ type: check.type,
7578
+ label: checkLabel(check),
7579
+ status: failed ? "failed" : "passed",
7580
+ evidence: {
7581
+ selector: key,
7582
+ max_overflow_px: maxOverflow,
7583
+ viewports: results.map((result) => toJsonValue(result))
7584
+ },
7585
+ message: failed ? `Frame selector ${key} overflow exceeded ${maxOverflow}px or was missing in ${failed} viewport(s).` : void 0
7586
+ };
7587
+ }
7503
7588
  if (check.type === "text_visible" || check.type === "text_absent") {
7504
7589
  const key = textKey(check);
7505
7590
  const expectedVisible = check.type === "text_visible";
@@ -7859,6 +7944,21 @@ function textOrderMatch(texts, expectedTexts) {
7859
7944
  }
7860
7945
  return { matched: true, positions };
7861
7946
  }
7947
+ function frameEvidenceForSelector(viewport, selector) {
7948
+ const container = viewport.frames && viewport.frames[selector || ""];
7949
+ if (!container || typeof container !== "object" || Array.isArray(container)) return [];
7950
+ const frames = Array.isArray(container.frames) ? container.frames : [];
7951
+ return frames.filter((frame) => frame && typeof frame === "object" && !Array.isArray(frame));
7952
+ }
7953
+ function frameTextSample(frame) {
7954
+ return [
7955
+ frame && frame.title,
7956
+ frame && frame.text_sample,
7957
+ frame && frame.body_text_sample,
7958
+ frame && frame.body_text,
7959
+ frame && frame.text,
7960
+ ].map((part) => String(part || "")).filter(Boolean).join(" ");
7961
+ }
7862
7962
  function summarizeRouteInventory(viewport, inventory) {
7863
7963
  const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
7864
7964
  const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
@@ -8105,6 +8205,62 @@ function assessProfile(profile, evidence) {
8105
8205
  });
8106
8206
  continue;
8107
8207
  }
8208
+ if (check.type === "frame_text_visible") {
8209
+ const selector = check.selector || "";
8210
+ const results = viewports.map((viewport) => {
8211
+ const frames = frameEvidenceForSelector(viewport, selector);
8212
+ const matches = frames
8213
+ .map((frame, index) => ({ index, frame, matched: textMatches(frameTextSample(frame), check) }))
8214
+ .filter((item) => item.matched);
8215
+ return {
8216
+ viewport: viewport.name,
8217
+ frame_count: frames.length,
8218
+ matched_count: matches.length,
8219
+ matched: matches.length > 0,
8220
+ urls: frames.map((frame) => String(frame.url || "")).filter(Boolean).slice(0, 10),
8221
+ samples: matches.map((item) => frameTextSample(item.frame).replace(/\s+/g, " ").trim().slice(0, 240)).slice(0, 3),
8222
+ };
8223
+ });
8224
+ const failed = results.filter((result) => !result.matched).length;
8225
+ checks.push({
8226
+ type: check.type,
8227
+ label: check.label || check.type,
8228
+ status: failed ? "failed" : "passed",
8229
+ evidence: { selector, text: check.text || null, pattern: check.pattern || null, viewports: results },
8230
+ message: failed ? "Frame selector " + selector + " did not contain expected text in " + failed + " viewport(s)." : undefined,
8231
+ });
8232
+ continue;
8233
+ }
8234
+ if (check.type === "frame_no_horizontal_overflow") {
8235
+ const selector = check.selector || "";
8236
+ const maxOverflow = check.max_overflow_px == null ? 4 : check.max_overflow_px;
8237
+ const results = viewports.map((viewport) => {
8238
+ const frames = frameEvidenceForSelector(viewport, selector);
8239
+ const frameOverflows = frames.map((frame, index) => ({
8240
+ index,
8241
+ url: String(frame.url || ""),
8242
+ overflow_px: horizontalBoundsOverflowPx(frame),
8243
+ offender_count: boundsOffendersForEvidence(frame).length,
8244
+ }));
8245
+ const maxFrameOverflow = frameOverflows.reduce((max, frame) => Math.max(max, frame.overflow_px), 0);
8246
+ return {
8247
+ viewport: viewport.name,
8248
+ frame_count: frames.length,
8249
+ max_overflow_px: roundPixels(maxFrameOverflow),
8250
+ failed_frame_count: frameOverflows.filter((frame) => frame.overflow_px > maxOverflow).length,
8251
+ frames: frameOverflows,
8252
+ };
8253
+ });
8254
+ const failed = results.filter((result) => result.frame_count < 1 || result.failed_frame_count > 0).length;
8255
+ checks.push({
8256
+ type: check.type,
8257
+ label: check.label || check.type,
8258
+ status: failed ? "failed" : "passed",
8259
+ evidence: { selector, max_overflow_px: maxOverflow, viewports: results },
8260
+ message: failed ? "Frame selector " + selector + " overflow exceeded " + maxOverflow + "px or was missing in " + failed + " viewport(s)." : undefined,
8261
+ });
8262
+ continue;
8263
+ }
8108
8264
  if (check.type === "text_visible" || check.type === "text_absent") {
8109
8265
  const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
8110
8266
  const expectedVisible = check.type === "text_visible";
@@ -8502,6 +8658,108 @@ async function selectorTextSequence(selector) {
8502
8658
  };
8503
8659
  }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
8504
8660
  }
8661
+ async function frameEvidence(selector) {
8662
+ const result = { selector, count: 0, frame_count: 0, frames: [], errors: [] };
8663
+ let handles = [];
8664
+ try {
8665
+ const locator = page.locator(selector);
8666
+ result.count = await locator.count();
8667
+ handles = await locator.elementHandles();
8668
+ } catch (error) {
8669
+ result.errors.push(String(error && error.message ? error.message : error).slice(0, 500));
8670
+ return result;
8671
+ }
8672
+ for (let index = 0; index < handles.length; index += 1) {
8673
+ const handle = handles[index];
8674
+ try {
8675
+ const frame = typeof handle.contentFrame === "function" ? await handle.contentFrame() : null;
8676
+ if (!frame) {
8677
+ result.frames.push({ index, attached: false, error: "content_frame_unavailable" });
8678
+ continue;
8679
+ }
8680
+ const snapshot = await frame.evaluate(() => {
8681
+ const body = document.body;
8682
+ const documentElement = document.documentElement;
8683
+ const text = (body ? body.innerText : "").replace(/\s+/g, " ").trim();
8684
+ const clientWidth = documentElement ? documentElement.clientWidth : window.innerWidth;
8685
+ const clientHeight = documentElement ? documentElement.clientHeight : window.innerHeight;
8686
+ const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
8687
+ const viewportWidth = clientWidth || window.innerWidth;
8688
+ const overflowOffenders = [];
8689
+ function isContainedByHorizontalScroller(element) {
8690
+ let current = element.parentElement;
8691
+ while (current && current !== body && current !== documentElement) {
8692
+ const style = window.getComputedStyle(current);
8693
+ const overflowX = style.overflowX || style.overflow || "";
8694
+ if ((overflowX === "auto" || overflowX === "scroll") && current.scrollWidth > current.clientWidth + 1) {
8695
+ const currentRect = current.getBoundingClientRect();
8696
+ const contained = currentRect.left >= -0.5 && currentRect.right <= viewportWidth + 0.5;
8697
+ if (contained) return true;
8698
+ }
8699
+ current = current.parentElement;
8700
+ }
8701
+ return false;
8702
+ }
8703
+ for (const element of Array.from(body ? body.querySelectorAll("*") : [])) {
8704
+ const rect = element.getBoundingClientRect();
8705
+ if (!rect || rect.width < 1 || rect.height < 1) continue;
8706
+ const style = window.getComputedStyle(element);
8707
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") continue;
8708
+ const leftOverflow = Math.max(0, -rect.left);
8709
+ const rightOverflow = Math.max(0, rect.right - viewportWidth);
8710
+ const overflow = Math.max(leftOverflow, rightOverflow);
8711
+ if (overflow <= 0.5) continue;
8712
+ if (isContainedByHorizontalScroller(element)) continue;
8713
+ const tag = element.tagName ? element.tagName.toLowerCase() : "element";
8714
+ const id = element.id ? "#" + element.id : "";
8715
+ const className = typeof element.className === "string"
8716
+ ? element.className.trim().split(/\s+/).filter(Boolean).slice(0, 2).join(".")
8717
+ : "";
8718
+ overflowOffenders.push({
8719
+ selector: tag + id + (className ? "." + className : ""),
8720
+ tag,
8721
+ text: (element.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120),
8722
+ overflow,
8723
+ left_overflow_px: leftOverflow,
8724
+ right_overflow_px: rightOverflow,
8725
+ viewport_width: viewportWidth,
8726
+ rect: {
8727
+ left: rect.left,
8728
+ right: rect.right,
8729
+ width: rect.width,
8730
+ },
8731
+ });
8732
+ }
8733
+ overflowOffenders.sort((a, b) => b.overflow - a.overflow);
8734
+ const boundsOverflowPx = Math.max(
8735
+ 0,
8736
+ scrollWidth - (clientWidth || viewportWidth),
8737
+ ...overflowOffenders.map((offender) => offender.overflow),
8738
+ );
8739
+ return {
8740
+ url: location.href,
8741
+ title: document.title,
8742
+ text_length: text.length,
8743
+ text_sample: text.slice(0, 8000),
8744
+ body_text_sample: text.slice(0, 8000),
8745
+ viewport_width: viewportWidth,
8746
+ viewport_height: clientHeight || window.innerHeight,
8747
+ scroll_width: scrollWidth,
8748
+ client_width: clientWidth,
8749
+ overflow_px: Math.max(0, scrollWidth - (clientWidth || viewportWidth)),
8750
+ bounds_overflow_px: Math.round(boundsOverflowPx * 100) / 100,
8751
+ overflow_offender_count: overflowOffenders.length,
8752
+ overflow_offenders: overflowOffenders.slice(0, 10),
8753
+ };
8754
+ });
8755
+ result.frames.push({ index, attached: true, ...snapshot });
8756
+ } catch (error) {
8757
+ result.frames.push({ index, attached: false, error: String(error && error.message ? error.message : error).slice(0, 1000) });
8758
+ }
8759
+ }
8760
+ result.frame_count = result.frames.filter((frame) => frame && frame.attached !== false && !frame.error).length;
8761
+ return result;
8762
+ }
8505
8763
  function inventoryAppPathFromUrl(urlLike) {
8506
8764
  const url = new URL(String(urlLike), targetUrl);
8507
8765
  const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
@@ -8906,6 +9164,7 @@ async function captureViewport(viewport) {
8906
9164
  evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
8907
9165
  }));
8908
9166
  const selectors = {};
9167
+ const frames = {};
8909
9168
  const text_sequences = {};
8910
9169
  const text_matches = {};
8911
9170
  for (const check of profile.checks || []) {
@@ -8919,6 +9178,10 @@ async function captureViewport(viewport) {
8919
9178
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
8920
9179
  text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
8921
9180
  }
9181
+ if ((check.type === "frame_text_visible" || check.type === "frame_no_horizontal_overflow") && check.selector) {
9182
+ selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
9183
+ frames[check.selector] = frames[check.selector] || await frameEvidence(check.selector);
9184
+ }
8922
9185
  }
8923
9186
  const screenshotLabel = profileSlug + "-" + viewport.name;
8924
9187
  try {
@@ -8979,6 +9242,7 @@ async function captureViewport(viewport) {
8979
9242
  bounds_overflow_px: dom.bounds_overflow_px,
8980
9243
  overflow_offenders: dom.overflow_offenders || [],
8981
9244
  selectors,
9245
+ frames,
8982
9246
  text_sequences,
8983
9247
  text_matches,
8984
9248
  route_inventory: routeInventory,
@@ -9014,6 +9278,20 @@ function buildProfileEvidence(currentViewports) {
9014
9278
  overflow_px: currentViewports.map((viewport) => viewport.overflow_px),
9015
9279
  bounds_overflow_px: currentViewports.map((viewport) => viewport.bounds_overflow_px),
9016
9280
  overflow_offender_counts: currentViewports.map((viewport) => (viewport.overflow_offenders || []).length),
9281
+ frames: currentViewports
9282
+ .filter((viewport) => viewport.frames)
9283
+ .map((viewport) => ({
9284
+ viewport: viewport.name,
9285
+ selectors: Object.entries(viewport.frames || {}).map(([selector, frameSet]) => ({
9286
+ selector,
9287
+ count: frameSet && frameSet.count,
9288
+ frame_count: frameSet && frameSet.frame_count,
9289
+ max_bounds_overflow_px: Math.max(
9290
+ 0,
9291
+ ...((frameSet && Array.isArray(frameSet.frames) ? frameSet.frames : [])).map((frame) => horizontalBoundsOverflowPx(frame)),
9292
+ ),
9293
+ })),
9294
+ })),
9017
9295
  route_inventory: currentViewports
9018
9296
  .filter((viewport) => viewport.route_inventory)
9019
9297
  .map((viewport) => ({
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  profileStatusExitCode,
11
11
  resolveRiddleProofProfileTargetUrl,
12
12
  resolveRiddleProofProfileTimeoutSec
13
- } from "./chunk-KZRDAMI5.js";
13
+ } from "./chunk-ZB7AFRN3.js";
14
14
  import {
15
15
  createRiddleApiClient,
16
16
  parseRiddleViewport