@riddledc/riddle-proof 0.7.20 → 0.7.22

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.
@@ -79,11 +79,13 @@ function assessBasicGameplayEvidence(evidence, options = {}) {
79
79
  }
80
80
  const routeResults = (run.results || []).map((route) => augmentRouteAssessmentWithProgressionChecks(
81
81
  assessBasicGameplayRoute(route, options),
82
- route
82
+ route,
83
+ options
83
84
  ));
84
85
  const failingRoutes = routeResults.filter((result) => !result.ok).map((result) => ({
85
86
  name: result.name,
86
87
  path: result.path,
88
+ assessment_mode: result.assessment_mode,
87
89
  failures: result.failures,
88
90
  warnings: result.warnings,
89
91
  suite_failures: result.suite_failures
@@ -110,6 +112,8 @@ function assessBasicGameplayRoute(route, options = {}) {
110
112
  const failOnConsoleError = options.failOnConsoleError ?? false;
111
113
  const failures = [];
112
114
  const warnings = [];
115
+ const assessmentMode = routeAssessmentMode(route);
116
+ const auditAssessment = isAuditAssessmentRoute(route);
113
117
  const initial = route.initial || {};
114
118
  const timed = route.timed || {};
115
119
  const afterAction = route.after_action || route.afterAction || {};
@@ -121,7 +125,9 @@ function assessBasicGameplayRoute(route, options = {}) {
121
125
  const continuedActionChange = changed(afterAction, afterContinue);
122
126
  const cleanupBase = route.after_continue || route.afterContinue ? afterContinue : afterAction;
123
127
  const cleanupActionChange = changed(cleanupBase, afterCleanup);
124
- const surfaceVisible = numberValue(initial.visible_canvas_count) > 0 || numberValue(initial.enabled_clickable_count) > 0 || numberValue(initial.visible_large_node_count) >= minSurfaceLargeNodes;
128
+ const interactiveSurfaceVisible = numberValue(initial.visible_canvas_count) > 0 || numberValue(initial.enabled_clickable_count) > 0 || numberValue(initial.visible_large_node_count) >= minSurfaceLargeNodes;
129
+ const auditSurfaceVisible = numberValue(initial.body_text_length) >= minBodyTextLength || numberValue(initial.visible_large_node_count) >= minVisibleLargeNodes || numberValue(initial.enabled_clickable_count) > 0 || numberValue(initial.visible_canvas_count) > 0;
130
+ const surfaceVisible = auditAssessment ? auditSurfaceVisible : interactiveSurfaceVisible;
125
131
  const actionResults = listValue(route.action_results || route.actionResults);
126
132
  const continuedActionResults = listValue(route.continued_action_results || route.continuedActionResults);
127
133
  const continuedCleanupActionResults = listValue(route.continued_cleanup_action_results || route.continuedCleanupActionResults);
@@ -135,7 +141,7 @@ function assessBasicGameplayRoute(route, options = {}) {
135
141
  const responseStatus = firstNumber(route.http_status, route.response_status, route.status);
136
142
  const pageErrorCount = numberValue(route.page_error_count);
137
143
  const consoleErrorCount = numberValue(route.console_error_count);
138
- const mobileOverflowPx = numberValue(mobile.overflow_px);
144
+ const mobileOverflowPx = horizontalBoundsOverflowPx(mobile);
139
145
  if (responseStatus !== null && responseStatus >= 400) failures.push("route_http_error");
140
146
  if (pageErrorCount > 0) failures.push("fatal_page_error");
141
147
  if (numberValue(initial.body_text_length) < minBodyTextLength && numberValue(initial.visible_large_node_count) < minVisibleLargeNodes) {
@@ -143,20 +149,21 @@ function assessBasicGameplayRoute(route, options = {}) {
143
149
  }
144
150
  if (!surfaceVisible) failures.push("no_game_surface");
145
151
  if (mobileOverflowPx > maxMobileOverflowPx) failures.push("mobile_horizontal_overflow");
146
- if (!actionAttempted && !timedChange.changed) failures.push("primary_control_missing");
147
- if (actionAttempted && !stateChangeObserved) failures.push("primary_control_inert");
152
+ if (!auditAssessment && !actionAttempted && !timedChange.changed) failures.push("primary_control_missing");
153
+ if (!auditAssessment && actionAttempted && !stateChangeObserved) failures.push("primary_control_inert");
148
154
  if (failOnConsoleError && consoleErrorCount > 0) failures.push("fatal_page_error");
149
155
  if (numberValue(initial.visible_canvas_count) > 0 && actionAttempted && !timedChange.canvas_changed && !actionChange.canvas_changed && !actionChange.screenshot_changed) {
150
156
  warnings.push("canvas_inert");
151
157
  }
152
158
  if (actionFailed) warnings.push("some_actions_failed");
153
- if (warnOnMissingResetPath && !resetPathPresent && route.requires_reset !== false && actionAttempted && stateChangeObserved) {
159
+ if (!auditAssessment && warnOnMissingResetPath && !resetPathPresent && route.requires_reset !== false && actionAttempted && stateChangeObserved) {
154
160
  warnings.push("missing_reset_path");
155
161
  }
156
162
  if (warnOnConsoleError && consoleErrorCount > 0) warnings.push("critical_console_error");
157
163
  return {
158
164
  name: route.name,
159
165
  path: route.path,
166
+ assessment_mode: assessmentMode,
160
167
  ok: failures.length === 0,
161
168
  failures,
162
169
  warnings,
@@ -277,15 +284,16 @@ function assessBasicGameplayProgressionCheck(check) {
277
284
  function assessBasicGameplayProgressionChecks(route) {
278
285
  return progressionChecksForRoute(route).map((check) => assessBasicGameplayProgressionCheck(check));
279
286
  }
280
- function augmentBasicGameplayAssessmentWithProgressionChecks(assessment, evidence) {
287
+ function augmentBasicGameplayAssessmentWithProgressionChecks(assessment, evidence, options = {}) {
281
288
  const run = extractBasicGameplayEvidence(evidence);
282
289
  if (!run?.results?.length) return assessment;
283
290
  const routeResults = assessment.route_results.map(
284
- (result, index) => augmentRouteAssessmentWithProgressionChecks(result, run.results?.[index] || {})
291
+ (result, index) => augmentRouteAssessmentWithProgressionChecks(result, run.results?.[index] || {}, options)
285
292
  );
286
293
  const failingRoutes = routeResults.filter((result) => !result.ok).map((result) => ({
287
294
  name: result.name,
288
295
  path: result.path,
296
+ assessment_mode: result.assessment_mode,
289
297
  failures: result.failures,
290
298
  warnings: result.warnings,
291
299
  suite_failures: result.suite_failures
@@ -342,7 +350,7 @@ function attachBasicGameplayArtifactScreenshotHashes(evidence, routesOrOptions =
342
350
  }
343
351
  return evidence;
344
352
  }
345
- function createBasicGameplayCatchRecords(assessment, evidence) {
353
+ function createBasicGameplayCatchRecords(assessment, evidence, options = {}) {
346
354
  const run = extractBasicGameplayEvidence(evidence);
347
355
  if (!run?.results?.length) return [];
348
356
  const catches = [];
@@ -416,6 +424,24 @@ function createBasicGameplayCatchRecords(assessment, evidence) {
416
424
  summary: `${route.name || route.path || "Route"}: responsive setup failed on ${failure.viewport?.label || "viewport"} (${failure.reason || "responsive_setup_failed"})`
417
425
  });
418
426
  }
427
+ for (const failure of responsiveBoundsFailures(route, options.maxMobileOverflowPx ?? 4)) {
428
+ catches.push({
429
+ site: run.site || null,
430
+ route: route.name,
431
+ path: route.path,
432
+ code: failure.code,
433
+ label: failure.label,
434
+ type: "responsive_bounds",
435
+ selector: null,
436
+ reason: failure.reason || "responsive_bounds_clipped",
437
+ phase: failure.viewport?.phase || void 0,
438
+ viewport: failure.viewport,
439
+ overflow_px: failure.overflow_px,
440
+ max_allowed_px: failure.max_allowed_px,
441
+ offenders: failure.offenders,
442
+ summary: `${route.name || route.path || "Route"}: responsive bounds exceeded ${failure.max_allowed_px}px on ${failure.viewport?.label || "viewport"} (${failure.overflow_px}px)`
443
+ });
444
+ }
419
445
  for (const check of assessBasicGameplayProgressionChecks(route).filter((item) => item.ok === false)) {
420
446
  catches.push({
421
447
  site: run.site || null,
@@ -570,13 +596,15 @@ function changed(before, after) {
570
596
  function canvasHashes(canvases) {
571
597
  return (canvases || []).map((canvas) => canvas.hash).filter(Boolean).join("|");
572
598
  }
573
- function augmentRouteAssessmentWithProgressionChecks(result, route) {
599
+ function augmentRouteAssessmentWithProgressionChecks(result, route, options = {}) {
574
600
  const progressionFailures = assessBasicGameplayProgressionChecks(route).filter((check) => check.ok === false);
575
601
  const responsiveFailures = responsiveSetupFailures(route);
576
- if (!progressionFailures.length && !responsiveFailures.length) return result;
602
+ const responsiveBounds = responsiveBoundsFailures(route, options.maxMobileOverflowPx ?? 4);
603
+ if (!progressionFailures.length && !responsiveFailures.length && !responsiveBounds.length) return result;
577
604
  const failures = new Set(result.failures);
578
605
  if (progressionFailures.length) failures.add("progression_assertion_failed");
579
606
  for (const failure of responsiveFailures) failures.add(failure.code);
607
+ for (const failure of responsiveBounds) failures.add(failure.code);
580
608
  return {
581
609
  ...result,
582
610
  ok: false,
@@ -592,7 +620,8 @@ function augmentRouteAssessmentWithProgressionChecks(result, route) {
592
620
  state_call: check.state_call || null,
593
621
  property_path: check.property_path || null
594
622
  })),
595
- ...responsiveFailures
623
+ ...responsiveFailures,
624
+ ...responsiveBounds
596
625
  ]
597
626
  };
598
627
  }
@@ -618,6 +647,28 @@ function responsiveSetupFailures(route) {
618
647
  }
619
648
  return failures;
620
649
  }
650
+ function responsiveBoundsFailures(route, maxOverflowPx) {
651
+ const failures = [];
652
+ for (const viewport of responsiveViewportsForRoute(route)) {
653
+ const overflowPx = horizontalBoundsOverflowPx(viewport);
654
+ if (overflowPx <= maxOverflowPx) continue;
655
+ failures.push({
656
+ code: "responsive_bounds_clipped",
657
+ label: `${viewport.label || "responsive"} horizontal bounds`,
658
+ reason: `horizontal bounds overflow ${overflowPx}px exceeded ${maxOverflowPx}px`,
659
+ overflow_px: overflowPx,
660
+ max_allowed_px: maxOverflowPx,
661
+ offenders: boundsOffendersForEvidence(viewport).slice(0, 10),
662
+ viewport: {
663
+ label: viewport.label || null,
664
+ width: numberValue(viewport.width) || null,
665
+ height: numberValue(viewport.height) || null,
666
+ phase: viewport.phase || null
667
+ }
668
+ });
669
+ }
670
+ return failures;
671
+ }
621
672
  function responsiveViewportsForRoute(route) {
622
673
  return listValue(route.responsive_viewports || route.responsiveViewports).filter((item) => Boolean(recordValue(item)));
623
674
  }
@@ -627,6 +678,82 @@ function progressionChecksForRoute(route) {
627
678
  function metricValue(value) {
628
679
  return recordValue(value);
629
680
  }
681
+ function horizontalBoundsOverflowPx(value) {
682
+ const record = recordValue(value);
683
+ if (!record) return 0;
684
+ let max = maxPositiveNumber(
685
+ record.overflow_px,
686
+ record.overflow,
687
+ record.bounds_overflow_px,
688
+ record.horizontal_overflow_px,
689
+ record.left_overflow_px,
690
+ record.right_overflow_px
691
+ );
692
+ for (const offender of boundsOffendersForEvidence(record)) {
693
+ max = Math.max(max, horizontalOffenderOverflowPx(offender));
694
+ }
695
+ return roundPixels(max);
696
+ }
697
+ function horizontalOffenderOverflowPx(value) {
698
+ const record = recordValue(value);
699
+ if (!record) return 0;
700
+ let max = maxPositiveNumber(
701
+ record.overflow,
702
+ record.overflow_px,
703
+ record.bounds_overflow_px,
704
+ record.horizontal_overflow_px,
705
+ record.left_overflow_px,
706
+ record.right_overflow_px,
707
+ record.leftOverflowPx,
708
+ record.rightOverflowPx
709
+ );
710
+ const clipped = recordValue(record.clipped || record.clip || record.clipping);
711
+ if (clipped) {
712
+ max = Math.max(max, maxPositiveNumber(
713
+ clipped.left,
714
+ clipped.right,
715
+ clipped.left_px,
716
+ clipped.right_px,
717
+ clipped.leftPx,
718
+ clipped.rightPx
719
+ ));
720
+ }
721
+ const rect = recordValue(record.rect || record.bounds || record.bounding_rect || record.boundingRect);
722
+ const viewportWidth = numericValue(record.viewport_width ?? record.viewportWidth);
723
+ if (rect && viewportWidth !== null) {
724
+ const left = numericValue(rect.left);
725
+ const right = numericValue(rect.right);
726
+ if (left !== null && left < 0) max = Math.max(max, Math.abs(left));
727
+ if (right !== null && right > viewportWidth) max = Math.max(max, right - viewportWidth);
728
+ }
729
+ return roundPixels(max);
730
+ }
731
+ function boundsOffendersForEvidence(value) {
732
+ const record = recordValue(value);
733
+ if (!record) return [];
734
+ const offenders = [
735
+ ...listValue(record.overflow_offenders),
736
+ ...listValue(record.overflowOffenders),
737
+ ...listValue(record.bounds_offenders),
738
+ ...listValue(record.boundsOffenders),
739
+ ...listValue(record.clipped_elements),
740
+ ...listValue(record.clippedElements),
741
+ ...listValue(record.clipping_offenders),
742
+ ...listValue(record.clippingOffenders)
743
+ ];
744
+ return offenders.filter((item) => Boolean(recordValue(item))).sort((a, b) => horizontalOffenderOverflowPx(b) - horizontalOffenderOverflowPx(a));
745
+ }
746
+ function maxPositiveNumber(...values) {
747
+ let max = 0;
748
+ for (const value of values) {
749
+ const number = numericValue(value);
750
+ if (number !== null && number > max) max = number;
751
+ }
752
+ return max;
753
+ }
754
+ function roundPixels(value) {
755
+ return Math.round(value * 100) / 100;
756
+ }
630
757
  function textMatches(text, pattern, flags) {
631
758
  if (!pattern) return Boolean(text);
632
759
  try {
@@ -720,6 +847,24 @@ function countCodes(codes) {
720
847
  for (const code of codes) counts[code] = (counts[code] || 0) + 1;
721
848
  return counts;
722
849
  }
850
+ function routeAssessmentMode(route) {
851
+ const raw = route.assessment_mode ?? route.assessmentMode ?? route.proof_mode ?? route.proofMode ?? null;
852
+ if (raw === null || raw === void 0) return route.audit === true ? "audit" : null;
853
+ const mode = String(raw).trim().toLowerCase().replace(/_/g, "-");
854
+ return mode || (route.audit === true ? "audit" : null);
855
+ }
856
+ function isAuditAssessmentRoute(route) {
857
+ const mode = routeAssessmentMode(route);
858
+ return route.audit === true || [
859
+ "audit",
860
+ "audit-only",
861
+ "no-diff",
862
+ "not-required",
863
+ "profile",
864
+ "report-only",
865
+ "static"
866
+ ].includes(String(mode || ""));
867
+ }
723
868
  function firstNumber(...values) {
724
869
  for (const value of values) {
725
870
  const number = numericValue(value);
@@ -38,6 +38,79 @@ function stringValue(value) {
38
38
  function numberValue(value) {
39
39
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
40
40
  }
41
+ function horizontalBoundsOverflowPx(value) {
42
+ if (!isRecord(value)) return 0;
43
+ let max = maxPositiveNumber(
44
+ value.overflow_px,
45
+ value.overflow,
46
+ value.bounds_overflow_px,
47
+ value.horizontal_overflow_px,
48
+ value.left_overflow_px,
49
+ value.right_overflow_px
50
+ );
51
+ for (const offender of boundsOffendersForEvidence(value)) {
52
+ max = Math.max(max, horizontalOffenderOverflowPx(offender));
53
+ }
54
+ return roundPixels(max);
55
+ }
56
+ function horizontalOffenderOverflowPx(value) {
57
+ if (!isRecord(value)) return 0;
58
+ let max = maxPositiveNumber(
59
+ value.overflow,
60
+ value.overflow_px,
61
+ value.bounds_overflow_px,
62
+ value.horizontal_overflow_px,
63
+ value.left_overflow_px,
64
+ value.right_overflow_px,
65
+ value.leftOverflowPx,
66
+ value.rightOverflowPx
67
+ );
68
+ const clipped = isRecord(value.clipped || value.clip || value.clipping) ? value.clipped || value.clip || value.clipping : void 0;
69
+ if (isRecord(clipped)) {
70
+ max = Math.max(max, maxPositiveNumber(
71
+ clipped.left,
72
+ clipped.right,
73
+ clipped.left_px,
74
+ clipped.right_px,
75
+ clipped.leftPx,
76
+ clipped.rightPx
77
+ ));
78
+ }
79
+ const rect = isRecord(value.rect || value.bounds || value.bounding_rect || value.boundingRect) ? value.rect || value.bounds || value.bounding_rect || value.boundingRect : void 0;
80
+ const viewportWidth = numberValue(value.viewport_width ?? value.viewportWidth);
81
+ if (isRecord(rect) && viewportWidth !== void 0) {
82
+ const left = numberValue(rect.left);
83
+ const right = numberValue(rect.right);
84
+ if (left !== void 0 && left < 0) max = Math.max(max, Math.abs(left));
85
+ if (right !== void 0 && right > viewportWidth) max = Math.max(max, right - viewportWidth);
86
+ }
87
+ return roundPixels(max);
88
+ }
89
+ function boundsOffendersForEvidence(value) {
90
+ if (!isRecord(value)) return [];
91
+ const offenders = [
92
+ ...Array.isArray(value.overflow_offenders) ? value.overflow_offenders : [],
93
+ ...Array.isArray(value.overflowOffenders) ? value.overflowOffenders : [],
94
+ ...Array.isArray(value.bounds_offenders) ? value.bounds_offenders : [],
95
+ ...Array.isArray(value.boundsOffenders) ? value.boundsOffenders : [],
96
+ ...Array.isArray(value.clipped_elements) ? value.clipped_elements : [],
97
+ ...Array.isArray(value.clippedElements) ? value.clippedElements : [],
98
+ ...Array.isArray(value.clipping_offenders) ? value.clipping_offenders : [],
99
+ ...Array.isArray(value.clippingOffenders) ? value.clippingOffenders : []
100
+ ];
101
+ return offenders.filter((item) => isRecord(item)).sort((a, b) => horizontalOffenderOverflowPx(b) - horizontalOffenderOverflowPx(a));
102
+ }
103
+ function maxPositiveNumber(...values) {
104
+ let max = 0;
105
+ for (const value of values) {
106
+ const number = numberValue(value);
107
+ if (number !== void 0 && number > max) max = number;
108
+ }
109
+ return max;
110
+ }
111
+ function roundPixels(value) {
112
+ return Math.round(value * 100) / 100;
113
+ }
41
114
  function timeoutSecValue(value) {
42
115
  const number = numberValue(value);
43
116
  return number && number > 0 ? Math.ceil(number) : void 0;
@@ -380,7 +453,7 @@ function assessCheckFromEvidence(check, evidence) {
380
453
  message: "No applicable viewport evidence was captured for overflow check."
381
454
  };
382
455
  }
383
- const failed = applicable.filter((viewport) => (viewport.overflow_px ?? 0) > maxOverflow);
456
+ const failed = applicable.filter((viewport) => horizontalBoundsOverflowPx(viewport) > maxOverflow);
384
457
  return {
385
458
  type: check.type,
386
459
  label: checkLabel(check),
@@ -388,9 +461,11 @@ function assessCheckFromEvidence(check, evidence) {
388
461
  evidence: {
389
462
  max_overflow_px: maxOverflow,
390
463
  overflow_px: applicable.map((viewport) => viewport.overflow_px ?? null),
464
+ bounds_overflow_px: applicable.map((viewport) => horizontalBoundsOverflowPx(viewport)),
465
+ overflow_offender_counts: applicable.map((viewport) => boundsOffendersForEvidence(viewport).length),
391
466
  viewports: applicable.map((viewport) => viewport.name)
392
467
  },
393
- message: failed.length ? `Horizontal overflow exceeded ${maxOverflow}px in ${failed.length} viewport(s).` : void 0
468
+ message: failed.length ? `Horizontal bounds overflow exceeded ${maxOverflow}px in ${failed.length} viewport(s).` : void 0
394
469
  };
395
470
  }
396
471
  if (check.type === "no_fatal_console_errors") {
@@ -612,6 +687,77 @@ function textMatches(sample, check) {
612
687
  }
613
688
  return String(sample || "").includes(check.text || "");
614
689
  }
690
+ function numberValue(value) {
691
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
692
+ }
693
+ function maxPositiveNumber() {
694
+ let max = 0;
695
+ for (const value of arguments) {
696
+ const number = numberValue(value);
697
+ if (number !== undefined && number > max) max = number;
698
+ }
699
+ return max;
700
+ }
701
+ function roundPixels(value) {
702
+ return Math.round(value * 100) / 100;
703
+ }
704
+ function isRecord(value) {
705
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
706
+ }
707
+ function boundsOffendersForEvidence(value) {
708
+ if (!isRecord(value)) return [];
709
+ const offenders = []
710
+ .concat(Array.isArray(value.overflow_offenders) ? value.overflow_offenders : [])
711
+ .concat(Array.isArray(value.overflowOffenders) ? value.overflowOffenders : [])
712
+ .concat(Array.isArray(value.bounds_offenders) ? value.bounds_offenders : [])
713
+ .concat(Array.isArray(value.boundsOffenders) ? value.boundsOffenders : [])
714
+ .concat(Array.isArray(value.clipped_elements) ? value.clipped_elements : [])
715
+ .concat(Array.isArray(value.clippedElements) ? value.clippedElements : [])
716
+ .concat(Array.isArray(value.clipping_offenders) ? value.clipping_offenders : [])
717
+ .concat(Array.isArray(value.clippingOffenders) ? value.clippingOffenders : []);
718
+ return offenders.filter(isRecord).sort((a, b) => horizontalOffenderOverflowPx(b) - horizontalOffenderOverflowPx(a));
719
+ }
720
+ function horizontalOffenderOverflowPx(value) {
721
+ if (!isRecord(value)) return 0;
722
+ let max = maxPositiveNumber(
723
+ value.overflow,
724
+ value.overflow_px,
725
+ value.bounds_overflow_px,
726
+ value.horizontal_overflow_px,
727
+ value.left_overflow_px,
728
+ value.right_overflow_px,
729
+ value.leftOverflowPx,
730
+ value.rightOverflowPx
731
+ );
732
+ const clipped = value.clipped || value.clip || value.clipping;
733
+ if (isRecord(clipped)) {
734
+ max = Math.max(max, maxPositiveNumber(clipped.left, clipped.right, clipped.left_px, clipped.right_px, clipped.leftPx, clipped.rightPx));
735
+ }
736
+ const rect = value.rect || value.bounds || value.bounding_rect || value.boundingRect;
737
+ const viewportWidth = numberValue(value.viewport_width != null ? value.viewport_width : value.viewportWidth);
738
+ if (isRecord(rect) && viewportWidth !== undefined) {
739
+ const left = numberValue(rect.left);
740
+ const right = numberValue(rect.right);
741
+ if (left !== undefined && left < 0) max = Math.max(max, Math.abs(left));
742
+ if (right !== undefined && right > viewportWidth) max = Math.max(max, right - viewportWidth);
743
+ }
744
+ return roundPixels(max);
745
+ }
746
+ function horizontalBoundsOverflowPx(value) {
747
+ if (!isRecord(value)) return 0;
748
+ let max = maxPositiveNumber(
749
+ value.overflow_px,
750
+ value.overflow,
751
+ value.bounds_overflow_px,
752
+ value.horizontal_overflow_px,
753
+ value.left_overflow_px,
754
+ value.right_overflow_px
755
+ );
756
+ for (const offender of boundsOffendersForEvidence(value)) {
757
+ max = Math.max(max, horizontalOffenderOverflowPx(offender));
758
+ }
759
+ return roundPixels(max);
760
+ }
615
761
  function assessProfile(profile, evidence) {
616
762
  const checks = [];
617
763
  const viewports = evidence.viewports || [];
@@ -729,13 +875,19 @@ function assessProfile(profile, evidence) {
729
875
  });
730
876
  continue;
731
877
  }
732
- const failed = applicable.filter((viewport) => (viewport.overflow_px || 0) > maxOverflow);
878
+ const failed = applicable.filter((viewport) => horizontalBoundsOverflowPx(viewport) > maxOverflow);
733
879
  checks.push({
734
880
  type: check.type,
735
881
  label: check.label || check.type,
736
882
  status: failed.length ? "failed" : "passed",
737
- evidence: { max_overflow_px: maxOverflow, overflow_px: applicable.map((viewport) => viewport.overflow_px ?? null), viewports: applicable.map((viewport) => viewport.name) },
738
- message: failed.length ? "Horizontal overflow exceeded " + maxOverflow + "px in " + failed.length + " viewport(s)." : undefined,
883
+ evidence: {
884
+ max_overflow_px: maxOverflow,
885
+ overflow_px: applicable.map((viewport) => viewport.overflow_px ?? null),
886
+ bounds_overflow_px: applicable.map((viewport) => horizontalBoundsOverflowPx(viewport)),
887
+ overflow_offender_counts: applicable.map((viewport) => boundsOffendersForEvidence(viewport).length),
888
+ viewports: applicable.map((viewport) => viewport.name)
889
+ },
890
+ message: failed.length ? "Horizontal bounds overflow exceeded " + maxOverflow + "px in " + failed.length + " viewport(s)." : undefined,
739
891
  });
740
892
  continue;
741
893
  }
@@ -965,14 +1117,55 @@ async function captureViewport(viewport) {
965
1117
  const body = document.body;
966
1118
  const documentElement = document.documentElement;
967
1119
  const text = (body ? body.innerText : "").replace(/\s+/g, " ").trim();
1120
+ const clientWidth = documentElement ? documentElement.clientWidth : window.innerWidth;
1121
+ const scrollWidth = documentElement ? documentElement.scrollWidth : 0;
1122
+ const viewportWidth = clientWidth || window.innerWidth;
1123
+ const overflowOffenders = [];
1124
+ for (const element of Array.from(body ? body.querySelectorAll("*") : [])) {
1125
+ const rect = element.getBoundingClientRect();
1126
+ if (!rect || rect.width < 1 || rect.height < 1) continue;
1127
+ const style = window.getComputedStyle(element);
1128
+ if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") continue;
1129
+ const leftOverflow = Math.max(0, -rect.left);
1130
+ const rightOverflow = Math.max(0, rect.right - viewportWidth);
1131
+ const overflow = Math.max(leftOverflow, rightOverflow);
1132
+ if (overflow <= 0.5) continue;
1133
+ const tag = element.tagName ? element.tagName.toLowerCase() : "element";
1134
+ const id = element.id ? "#" + element.id : "";
1135
+ const className = typeof element.className === "string"
1136
+ ? element.className.trim().split(/\s+/).filter(Boolean).slice(0, 2).join(".")
1137
+ : "";
1138
+ overflowOffenders.push({
1139
+ selector: tag + id + (className ? "." + className : ""),
1140
+ tag,
1141
+ text: (element.textContent || "").replace(/\s+/g, " ").trim().slice(0, 120),
1142
+ overflow,
1143
+ left_overflow_px: leftOverflow,
1144
+ right_overflow_px: rightOverflow,
1145
+ viewport_width: viewportWidth,
1146
+ rect: {
1147
+ left: rect.left,
1148
+ right: rect.right,
1149
+ width: rect.width,
1150
+ },
1151
+ });
1152
+ }
1153
+ overflowOffenders.sort((a, b) => b.overflow - a.overflow);
1154
+ const boundsOverflowPx = Math.max(
1155
+ 0,
1156
+ scrollWidth - (clientWidth || viewportWidth),
1157
+ ...overflowOffenders.map((offender) => offender.overflow),
1158
+ );
968
1159
  return {
969
1160
  url: location.href,
970
1161
  pathname: location.pathname,
971
1162
  title: document.title,
972
1163
  body_text_length: text.length,
973
1164
  body_text_sample: text.slice(0, 8000),
974
- scroll_width: documentElement ? documentElement.scrollWidth : 0,
975
- client_width: documentElement ? documentElement.clientWidth : window.innerWidth,
1165
+ scroll_width: scrollWidth,
1166
+ client_width: clientWidth,
1167
+ bounds_overflow_px: Math.round(boundsOverflowPx * 100) / 100,
1168
+ overflow_offenders: overflowOffenders.slice(0, 10),
976
1169
  };
977
1170
  }).catch((error) => ({
978
1171
  url: page.url(),
@@ -982,6 +1175,8 @@ async function captureViewport(viewport) {
982
1175
  body_text_sample: "",
983
1176
  scroll_width: 0,
984
1177
  client_width: viewport.width,
1178
+ bounds_overflow_px: 0,
1179
+ overflow_offenders: [],
985
1180
  evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
986
1181
  }));
987
1182
  const selectors = {};
@@ -1020,6 +1215,8 @@ async function captureViewport(viewport) {
1020
1215
  scroll_width: dom.scroll_width,
1021
1216
  client_width: dom.client_width,
1022
1217
  overflow_px: Math.max(0, (dom.scroll_width || 0) - (dom.client_width || viewport.width)),
1218
+ bounds_overflow_px: dom.bounds_overflow_px,
1219
+ overflow_offenders: dom.overflow_offenders || [],
1023
1220
  selectors,
1024
1221
  text_matches,
1025
1222
  setup_action_results: setupActionResults,
@@ -1051,6 +1248,8 @@ function buildProfileEvidence(currentViewports) {
1051
1248
  routes: currentViewports.map((viewport) => viewport.route),
1052
1249
  titles: currentViewports.map((viewport) => viewport.title),
1053
1250
  overflow_px: currentViewports.map((viewport) => viewport.overflow_px),
1251
+ bounds_overflow_px: currentViewports.map((viewport) => viewport.bounds_overflow_px),
1252
+ overflow_offender_counts: currentViewports.map((viewport) => (viewport.overflow_offenders || []).length),
1054
1253
  },
1055
1254
  };
1056
1255
  }