@riddledc/riddle-proof 0.7.34 → 0.7.36

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/index.cjs CHANGED
@@ -8728,6 +8728,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
8728
8728
  "route_loaded",
8729
8729
  "selector_visible",
8730
8730
  "selector_count_at_least",
8731
+ "selector_text_order",
8731
8732
  "text_visible",
8732
8733
  "text_absent",
8733
8734
  "route_inventory",
@@ -9007,6 +9008,15 @@ function normalizeRouteInventoryRoutes(value, index) {
9007
9008
  }
9008
9009
  return routes;
9009
9010
  }
9011
+ function normalizeExpectedTexts(value, index) {
9012
+ if (value === void 0) return void 0;
9013
+ if (!Array.isArray(value) || value.length === 0) {
9014
+ throw new Error(`checks[${index}] selector_text_order expected_texts must be a non-empty array.`);
9015
+ }
9016
+ const texts = value.map((item) => String(item).replace(/\s+/g, " ").trim()).filter(Boolean);
9017
+ if (!texts.length) throw new Error(`checks[${index}] selector_text_order expected_texts must contain non-empty strings.`);
9018
+ return texts;
9019
+ }
9010
9020
  function normalizeCheck(input, index) {
9011
9021
  if (!isRecord2(input)) throw new Error(`checks[${index}] must be an object.`);
9012
9022
  const type = stringValue5(input.type);
@@ -9023,6 +9033,11 @@ function normalizeCheck(input, index) {
9023
9033
  if (type === "selector_count_at_least" && numberValue3(input.min_count) === void 0) {
9024
9034
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
9025
9035
  }
9036
+ const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
9037
+ if (type === "selector_text_order") {
9038
+ if (!stringValue5(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
9039
+ if (!expectedTexts?.length) throw new Error(`checks[${index}] selector_text_order requires expected_texts.`);
9040
+ }
9026
9041
  const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
9027
9042
  if (type === "route_inventory" && !expectedRoutes?.length) {
9028
9043
  throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
@@ -9033,6 +9048,7 @@ function normalizeCheck(input, index) {
9033
9048
  expected_path: stringValue5(input.expected_path),
9034
9049
  expected_routes: expectedRoutes,
9035
9050
  selector: stringValue5(input.selector),
9051
+ expected_texts: expectedTexts,
9036
9052
  link_selector: stringValue5(input.link_selector) || stringValue5(input.linkSelector),
9037
9053
  source_selector: stringValue5(input.source_selector) || stringValue5(input.sourceSelector),
9038
9054
  route_path_prefix: stringValue5(input.route_path_prefix) || stringValue5(input.routePathPrefix),
@@ -9048,6 +9064,7 @@ function normalizeCheck(input, index) {
9048
9064
  timeout_ms: numberValue3(input.timeout_ms) ?? numberValue3(input.timeoutMs),
9049
9065
  run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
9050
9066
  run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
9067
+ run_all_viewports: input.run_all_viewports === true || input.runAllViewports === true,
9051
9068
  require_unique_routes: input.require_unique_routes === false || input.requireUniqueRoutes === false ? false : true,
9052
9069
  allow_unexpected_routes: input.allow_unexpected_routes === true || input.allowUnexpectedRoutes === true,
9053
9070
  save_route_screenshots: input.save_route_screenshots === true || input.saveRouteScreenshots === true
@@ -9138,6 +9155,47 @@ function selectorKey(check) {
9138
9155
  function textKey(check) {
9139
9156
  return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
9140
9157
  }
9158
+ function textSequenceForCheck(viewport, check) {
9159
+ const key = selectorKey(check);
9160
+ const sequence = viewport.text_sequences?.[key];
9161
+ if (isRecord2(sequence)) {
9162
+ const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
9163
+ const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
9164
+ const candidates = visibleTexts.length ? visibleTexts : texts;
9165
+ return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
9166
+ }
9167
+ return [];
9168
+ }
9169
+ function textOrderMatch(texts, expectedTexts) {
9170
+ const positions = [];
9171
+ let startAt = 0;
9172
+ for (const expected of expectedTexts) {
9173
+ const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
9174
+ if (index < 0) return { matched: false, positions };
9175
+ positions.push(index);
9176
+ startAt = index + 1;
9177
+ }
9178
+ return { matched: true, positions };
9179
+ }
9180
+ function summarizeRouteInventory(viewport, inventory) {
9181
+ const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
9182
+ const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
9183
+ const sourceLinkCount = numberValue3(inventory.source_link_count) ?? numberValue3(inventory.home_game_link_count) ?? null;
9184
+ const sourceUniqueLinkCount = numberValue3(inventory.source_unique_link_count) ?? numberValue3(inventory.home_unique_game_link_count) ?? null;
9185
+ const duplicateSourceLinks = Array.isArray(inventory.duplicate_source_link_paths) ? inventory.duplicate_source_link_paths.map((path6) => String(path6)) : [];
9186
+ const duplicateSourceLinkCount = numberValue3(inventory.duplicate_source_link_count) ?? (sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null);
9187
+ const failures = Array.isArray(inventory.failures) ? inventory.failures : [];
9188
+ return {
9189
+ viewport,
9190
+ source_link_count: sourceLinkCount,
9191
+ source_unique_link_count: sourceUniqueLinkCount,
9192
+ duplicate_source_link_count: duplicateSourceLinkCount,
9193
+ duplicate_source_links: duplicateSourceLinks,
9194
+ direct_route_count: directRoutes.length,
9195
+ clickthrough_count: clickthroughs.length,
9196
+ failure_count: failures.length
9197
+ };
9198
+ }
9141
9199
  function matchText(sample, check) {
9142
9200
  if (check.pattern) {
9143
9201
  try {
@@ -9257,6 +9315,32 @@ function assessCheckFromEvidence(check, evidence) {
9257
9315
  message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
9258
9316
  };
9259
9317
  }
9318
+ if (check.type === "selector_text_order") {
9319
+ const key = selectorKey(check);
9320
+ const expectedTexts = check.expected_texts || [];
9321
+ const results = viewports.map((viewport) => {
9322
+ const texts = textSequenceForCheck(viewport, check);
9323
+ const match = textOrderMatch(texts, expectedTexts);
9324
+ return {
9325
+ viewport: viewport.name,
9326
+ matched: match.matched,
9327
+ matched_positions: match.positions,
9328
+ visible_texts: texts.slice(0, 20)
9329
+ };
9330
+ });
9331
+ const failed = results.filter((result) => !result.matched).length;
9332
+ return {
9333
+ type: check.type,
9334
+ label: checkLabel(check),
9335
+ status: failed ? "failed" : "passed",
9336
+ evidence: {
9337
+ selector: key,
9338
+ expected_texts: expectedTexts,
9339
+ viewports: results.map((result) => toJsonValue(result))
9340
+ },
9341
+ message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
9342
+ };
9343
+ }
9260
9344
  if (check.type === "text_visible" || check.type === "text_absent") {
9261
9345
  const key = textKey(check);
9262
9346
  const expectedVisible = check.type === "text_visible";
@@ -9288,6 +9372,7 @@ function assessCheckFromEvidence(check, evidence) {
9288
9372
  message: "No route inventory evidence was captured."
9289
9373
  };
9290
9374
  }
9375
+ const viewportSummaries = inventories.map((item) => summarizeRouteInventory(item.viewport, item.inventory));
9291
9376
  const failures = inventories.flatMap((item) => Array.isArray(item.inventory?.failures) ? item.inventory.failures.map((failure) => toJsonValue({ viewport: item.viewport, failure })) : []);
9292
9377
  const first = inventories[0]?.inventory;
9293
9378
  const directRoutes = Array.isArray(first?.direct_routes) ? first.direct_routes : [];
@@ -9311,6 +9396,8 @@ function assessCheckFromEvidence(check, evidence) {
9311
9396
  homepage_unique_link_count: numberValue3(first?.home_unique_game_link_count) ?? sourceUniqueLinkCount,
9312
9397
  direct_route_count: directRoutes.length,
9313
9398
  clickthrough_count: clickthroughs.length,
9399
+ viewport_count: inventories.length,
9400
+ viewports: viewportSummaries.map((summary) => toJsonValue(summary)),
9314
9401
  failures
9315
9402
  },
9316
9403
  message: failures.length ? `Route inventory failed with ${failures.length} issue(s).` : void 0
@@ -9607,6 +9694,47 @@ function textMatches(sample, check) {
9607
9694
  }
9608
9695
  return String(sample || "").includes(check.text || "");
9609
9696
  }
9697
+ function textSequenceForCheck(viewport, check) {
9698
+ const key = check.selector || "";
9699
+ const sequence = viewport.text_sequences && viewport.text_sequences[key];
9700
+ if (sequence && typeof sequence === "object") {
9701
+ const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
9702
+ const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
9703
+ const candidates = visibleTexts.length ? visibleTexts : texts;
9704
+ return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
9705
+ }
9706
+ return [];
9707
+ }
9708
+ function textOrderMatch(texts, expectedTexts) {
9709
+ const positions = [];
9710
+ let startAt = 0;
9711
+ for (const expected of expectedTexts || []) {
9712
+ const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
9713
+ if (index < 0) return { matched: false, positions };
9714
+ positions.push(index);
9715
+ startAt = index + 1;
9716
+ }
9717
+ return { matched: true, positions };
9718
+ }
9719
+ function summarizeRouteInventory(viewport, inventory) {
9720
+ const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
9721
+ const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
9722
+ const sourceLinkCount = typeof inventory.source_link_count === "number" ? inventory.source_link_count : typeof inventory.home_game_link_count === "number" ? inventory.home_game_link_count : null;
9723
+ const sourceUniqueLinkCount = typeof inventory.source_unique_link_count === "number" ? inventory.source_unique_link_count : typeof inventory.home_unique_game_link_count === "number" ? inventory.home_unique_game_link_count : null;
9724
+ const duplicateSourceLinks = Array.isArray(inventory.duplicate_source_link_paths) ? inventory.duplicate_source_link_paths.map((path) => String(path)) : [];
9725
+ const duplicateSourceLinkCount = typeof inventory.duplicate_source_link_count === "number" ? inventory.duplicate_source_link_count : sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null;
9726
+ const failures = Array.isArray(inventory.failures) ? inventory.failures : [];
9727
+ return {
9728
+ viewport,
9729
+ source_link_count: sourceLinkCount,
9730
+ source_unique_link_count: sourceUniqueLinkCount,
9731
+ duplicate_source_link_count: duplicateSourceLinkCount,
9732
+ duplicate_source_links: duplicateSourceLinks,
9733
+ direct_route_count: directRoutes.length,
9734
+ clickthrough_count: clickthroughs.length,
9735
+ failure_count: failures.length,
9736
+ };
9737
+ }
9610
9738
  function numberValue(value) {
9611
9739
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
9612
9740
  }
@@ -9811,6 +9939,29 @@ function assessProfile(profile, evidence) {
9811
9939
  });
9812
9940
  continue;
9813
9941
  }
9942
+ if (check.type === "selector_text_order") {
9943
+ const selector = check.selector || "";
9944
+ const expectedTexts = check.expected_texts || [];
9945
+ const results = viewports.map((viewport) => {
9946
+ const texts = textSequenceForCheck(viewport, check);
9947
+ const match = textOrderMatch(texts, expectedTexts);
9948
+ return {
9949
+ viewport: viewport.name,
9950
+ matched: match.matched,
9951
+ matched_positions: match.positions,
9952
+ visible_texts: texts.slice(0, 20),
9953
+ };
9954
+ });
9955
+ const failed = results.filter((result) => !result.matched).length;
9956
+ checks.push({
9957
+ type: check.type,
9958
+ label: check.label || check.type,
9959
+ status: failed ? "failed" : "passed",
9960
+ evidence: { selector, expected_texts: expectedTexts, viewports: results },
9961
+ message: failed ? "Selector " + selector + " text order failed in " + failed + " viewport(s)." : undefined,
9962
+ });
9963
+ continue;
9964
+ }
9814
9965
  if (check.type === "text_visible" || check.type === "text_absent") {
9815
9966
  const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
9816
9967
  const expectedVisible = check.type === "text_visible";
@@ -9839,6 +9990,7 @@ function assessProfile(profile, evidence) {
9839
9990
  });
9840
9991
  continue;
9841
9992
  }
9993
+ const viewportSummaries = inventories.map((item) => summarizeRouteInventory(item.viewport, item.inventory || {}));
9842
9994
  const failures = [];
9843
9995
  for (const item of inventories) {
9844
9996
  for (const failure of Array.isArray(item.inventory.failures) ? item.inventory.failures : []) {
@@ -9867,6 +10019,8 @@ function assessProfile(profile, evidence) {
9867
10019
  homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : sourceUniqueLinkCount,
9868
10020
  direct_route_count: directRoutes.length,
9869
10021
  clickthrough_count: clickthroughs.length,
10022
+ viewport_count: inventories.length,
10023
+ viewports: viewportSummaries,
9870
10024
  failures,
9871
10025
  },
9872
10026
  message: failures.length ? "Route inventory failed with " + failures.length + " issue(s)." : undefined,
@@ -10184,6 +10338,27 @@ async function selectorStats(selector) {
10184
10338
  return { count: elements.length, visible_count: elements.filter(isVisible).length };
10185
10339
  }).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
10186
10340
  }
10341
+ async function selectorTextSequence(selector) {
10342
+ return page.locator(selector).evaluateAll((elements) => {
10343
+ const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
10344
+ const isVisible = (element) => {
10345
+ const style = window.getComputedStyle(element);
10346
+ const rect = element.getBoundingClientRect();
10347
+ return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
10348
+ };
10349
+ const rows = elements.map((element, index) => ({
10350
+ index,
10351
+ text: compact(element.innerText || element.textContent || ""),
10352
+ visible: isVisible(element),
10353
+ }));
10354
+ return {
10355
+ count: rows.length,
10356
+ visible_count: rows.filter((row) => row.visible).length,
10357
+ texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
10358
+ visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
10359
+ };
10360
+ }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
10361
+ }
10187
10362
  function inventoryAppPathFromUrl(urlLike) {
10188
10363
  const url = new URL(String(urlLike), targetUrl);
10189
10364
  const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
@@ -10581,11 +10756,16 @@ async function captureViewport(viewport) {
10581
10756
  evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
10582
10757
  }));
10583
10758
  const selectors = {};
10759
+ const text_sequences = {};
10584
10760
  const text_matches = {};
10585
10761
  for (const check of profile.checks || []) {
10586
10762
  if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
10587
10763
  selectors[check.selector] = await selectorStats(check.selector);
10588
10764
  }
10765
+ if (check.type === "selector_text_order" && check.selector) {
10766
+ selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
10767
+ text_sequences[check.selector] = await selectorTextSequence(check.selector);
10768
+ }
10589
10769
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
10590
10770
  text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
10591
10771
  }
@@ -10599,7 +10779,7 @@ async function captureViewport(viewport) {
10599
10779
  let routeInventory;
10600
10780
  const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
10601
10781
  const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
10602
- if (routeInventoryCheck && (!firstViewportName || viewport.name === firstViewportName)) {
10782
+ if (routeInventoryCheck && (routeInventoryCheck.run_all_viewports || !firstViewportName || viewport.name === firstViewportName)) {
10603
10783
  try {
10604
10784
  routeInventory = await collectRouteInventory(routeInventoryCheck, viewport);
10605
10785
  } catch (error) {
@@ -10649,6 +10829,7 @@ async function captureViewport(viewport) {
10649
10829
  bounds_overflow_px: dom.bounds_overflow_px,
10650
10830
  overflow_offenders: dom.overflow_offenders || [],
10651
10831
  selectors,
10832
+ text_sequences,
10652
10833
  text_matches,
10653
10834
  route_inventory: routeInventory,
10654
10835
  setup_action_results: setupActionResults,
package/dist/index.js CHANGED
@@ -58,7 +58,7 @@ import {
58
58
  resolveRiddleProofProfileTimeoutSec,
59
59
  slugifyRiddleProofProfileName,
60
60
  summarizeRiddleProofProfileResult
61
- } from "./chunk-PWGJQLOS.js";
61
+ } from "./chunk-VEK6QHPE.js";
62
62
  import {
63
63
  DEFAULT_RIDDLE_API_BASE_URL,
64
64
  DEFAULT_RIDDLE_API_KEY_FILE,
package/dist/profile.cjs CHANGED
@@ -57,6 +57,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
57
57
  "route_loaded",
58
58
  "selector_visible",
59
59
  "selector_count_at_least",
60
+ "selector_text_order",
60
61
  "text_visible",
61
62
  "text_absent",
62
63
  "route_inventory",
@@ -336,6 +337,15 @@ function normalizeRouteInventoryRoutes(value, index) {
336
337
  }
337
338
  return routes;
338
339
  }
340
+ function normalizeExpectedTexts(value, index) {
341
+ if (value === void 0) return void 0;
342
+ if (!Array.isArray(value) || value.length === 0) {
343
+ throw new Error(`checks[${index}] selector_text_order expected_texts must be a non-empty array.`);
344
+ }
345
+ const texts = value.map((item) => String(item).replace(/\s+/g, " ").trim()).filter(Boolean);
346
+ if (!texts.length) throw new Error(`checks[${index}] selector_text_order expected_texts must contain non-empty strings.`);
347
+ return texts;
348
+ }
339
349
  function normalizeCheck(input, index) {
340
350
  if (!isRecord(input)) throw new Error(`checks[${index}] must be an object.`);
341
351
  const type = stringValue(input.type);
@@ -352,6 +362,11 @@ function normalizeCheck(input, index) {
352
362
  if (type === "selector_count_at_least" && numberValue(input.min_count) === void 0) {
353
363
  throw new Error(`checks[${index}] selector_count_at_least requires min_count.`);
354
364
  }
365
+ const expectedTexts = normalizeExpectedTexts(input.expected_texts ?? input.expectedTexts, index);
366
+ if (type === "selector_text_order") {
367
+ if (!stringValue(input.selector)) throw new Error(`checks[${index}] selector_text_order requires selector.`);
368
+ if (!expectedTexts?.length) throw new Error(`checks[${index}] selector_text_order requires expected_texts.`);
369
+ }
355
370
  const expectedRoutes = normalizeRouteInventoryRoutes(input.expected_routes ?? input.expectedRoutes, index);
356
371
  if (type === "route_inventory" && !expectedRoutes?.length) {
357
372
  throw new Error(`checks[${index}] route_inventory requires expected_routes.`);
@@ -362,6 +377,7 @@ function normalizeCheck(input, index) {
362
377
  expected_path: stringValue(input.expected_path),
363
378
  expected_routes: expectedRoutes,
364
379
  selector: stringValue(input.selector),
380
+ expected_texts: expectedTexts,
365
381
  link_selector: stringValue(input.link_selector) || stringValue(input.linkSelector),
366
382
  source_selector: stringValue(input.source_selector) || stringValue(input.sourceSelector),
367
383
  route_path_prefix: stringValue(input.route_path_prefix) || stringValue(input.routePathPrefix),
@@ -377,6 +393,7 @@ function normalizeCheck(input, index) {
377
393
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
378
394
  run_direct_routes: input.run_direct_routes === false || input.runDirectRoutes === false ? false : true,
379
395
  run_clickthroughs: input.run_clickthroughs === false || input.runClickthroughs === false ? false : true,
396
+ run_all_viewports: input.run_all_viewports === true || input.runAllViewports === true,
380
397
  require_unique_routes: input.require_unique_routes === false || input.requireUniqueRoutes === false ? false : true,
381
398
  allow_unexpected_routes: input.allow_unexpected_routes === true || input.allowUnexpectedRoutes === true,
382
399
  save_route_screenshots: input.save_route_screenshots === true || input.saveRouteScreenshots === true
@@ -467,6 +484,47 @@ function selectorKey(check) {
467
484
  function textKey(check) {
468
485
  return check.pattern ? `pattern:${check.pattern}/${check.flags || ""}` : `text:${check.text || ""}`;
469
486
  }
487
+ function textSequenceForCheck(viewport, check) {
488
+ const key = selectorKey(check);
489
+ const sequence = viewport.text_sequences?.[key];
490
+ if (isRecord(sequence)) {
491
+ const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
492
+ const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
493
+ const candidates = visibleTexts.length ? visibleTexts : texts;
494
+ return candidates.map((text) => String(text).replace(/\s+/g, " ").trim()).filter(Boolean);
495
+ }
496
+ return [];
497
+ }
498
+ function textOrderMatch(texts, expectedTexts) {
499
+ const positions = [];
500
+ let startAt = 0;
501
+ for (const expected of expectedTexts) {
502
+ const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
503
+ if (index < 0) return { matched: false, positions };
504
+ positions.push(index);
505
+ startAt = index + 1;
506
+ }
507
+ return { matched: true, positions };
508
+ }
509
+ function summarizeRouteInventory(viewport, inventory) {
510
+ const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
511
+ const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
512
+ const sourceLinkCount = numberValue(inventory.source_link_count) ?? numberValue(inventory.home_game_link_count) ?? null;
513
+ const sourceUniqueLinkCount = numberValue(inventory.source_unique_link_count) ?? numberValue(inventory.home_unique_game_link_count) ?? null;
514
+ const duplicateSourceLinks = Array.isArray(inventory.duplicate_source_link_paths) ? inventory.duplicate_source_link_paths.map((path) => String(path)) : [];
515
+ const duplicateSourceLinkCount = numberValue(inventory.duplicate_source_link_count) ?? (sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null);
516
+ const failures = Array.isArray(inventory.failures) ? inventory.failures : [];
517
+ return {
518
+ viewport,
519
+ source_link_count: sourceLinkCount,
520
+ source_unique_link_count: sourceUniqueLinkCount,
521
+ duplicate_source_link_count: duplicateSourceLinkCount,
522
+ duplicate_source_links: duplicateSourceLinks,
523
+ direct_route_count: directRoutes.length,
524
+ clickthrough_count: clickthroughs.length,
525
+ failure_count: failures.length
526
+ };
527
+ }
470
528
  function matchText(sample, check) {
471
529
  if (check.pattern) {
472
530
  try {
@@ -586,6 +644,32 @@ function assessCheckFromEvidence(check, evidence) {
586
644
  message: failed.length ? `Selector ${key} count was below ${minCount} in ${failed.length} viewport(s).` : void 0
587
645
  };
588
646
  }
647
+ if (check.type === "selector_text_order") {
648
+ const key = selectorKey(check);
649
+ const expectedTexts = check.expected_texts || [];
650
+ const results = viewports.map((viewport) => {
651
+ const texts = textSequenceForCheck(viewport, check);
652
+ const match = textOrderMatch(texts, expectedTexts);
653
+ return {
654
+ viewport: viewport.name,
655
+ matched: match.matched,
656
+ matched_positions: match.positions,
657
+ visible_texts: texts.slice(0, 20)
658
+ };
659
+ });
660
+ const failed = results.filter((result) => !result.matched).length;
661
+ return {
662
+ type: check.type,
663
+ label: checkLabel(check),
664
+ status: failed ? "failed" : "passed",
665
+ evidence: {
666
+ selector: key,
667
+ expected_texts: expectedTexts,
668
+ viewports: results.map((result) => toJsonValue(result))
669
+ },
670
+ message: failed ? `Selector ${key} text order failed in ${failed} viewport(s).` : void 0
671
+ };
672
+ }
589
673
  if (check.type === "text_visible" || check.type === "text_absent") {
590
674
  const key = textKey(check);
591
675
  const expectedVisible = check.type === "text_visible";
@@ -617,6 +701,7 @@ function assessCheckFromEvidence(check, evidence) {
617
701
  message: "No route inventory evidence was captured."
618
702
  };
619
703
  }
704
+ const viewportSummaries = inventories.map((item) => summarizeRouteInventory(item.viewport, item.inventory));
620
705
  const failures = inventories.flatMap((item) => Array.isArray(item.inventory?.failures) ? item.inventory.failures.map((failure) => toJsonValue({ viewport: item.viewport, failure })) : []);
621
706
  const first = inventories[0]?.inventory;
622
707
  const directRoutes = Array.isArray(first?.direct_routes) ? first.direct_routes : [];
@@ -640,6 +725,8 @@ function assessCheckFromEvidence(check, evidence) {
640
725
  homepage_unique_link_count: numberValue(first?.home_unique_game_link_count) ?? sourceUniqueLinkCount,
641
726
  direct_route_count: directRoutes.length,
642
727
  clickthrough_count: clickthroughs.length,
728
+ viewport_count: inventories.length,
729
+ viewports: viewportSummaries.map((summary) => toJsonValue(summary)),
643
730
  failures
644
731
  },
645
732
  message: failures.length ? `Route inventory failed with ${failures.length} issue(s).` : void 0
@@ -936,6 +1023,47 @@ function textMatches(sample, check) {
936
1023
  }
937
1024
  return String(sample || "").includes(check.text || "");
938
1025
  }
1026
+ function textSequenceForCheck(viewport, check) {
1027
+ const key = check.selector || "";
1028
+ const sequence = viewport.text_sequences && viewport.text_sequences[key];
1029
+ if (sequence && typeof sequence === "object") {
1030
+ const visibleTexts = Array.isArray(sequence.visible_texts) ? sequence.visible_texts : [];
1031
+ const texts = Array.isArray(sequence.texts) ? sequence.texts : [];
1032
+ const candidates = visibleTexts.length ? visibleTexts : texts;
1033
+ return candidates.map((text) => String(text || "").replace(/\s+/g, " ").trim()).filter(Boolean);
1034
+ }
1035
+ return [];
1036
+ }
1037
+ function textOrderMatch(texts, expectedTexts) {
1038
+ const positions = [];
1039
+ let startAt = 0;
1040
+ for (const expected of expectedTexts || []) {
1041
+ const index = texts.findIndex((text, offset) => offset >= startAt && text.includes(expected));
1042
+ if (index < 0) return { matched: false, positions };
1043
+ positions.push(index);
1044
+ startAt = index + 1;
1045
+ }
1046
+ return { matched: true, positions };
1047
+ }
1048
+ function summarizeRouteInventory(viewport, inventory) {
1049
+ const directRoutes = Array.isArray(inventory.direct_routes) ? inventory.direct_routes : [];
1050
+ const clickthroughs = Array.isArray(inventory.clickthroughs) ? inventory.clickthroughs : [];
1051
+ const sourceLinkCount = typeof inventory.source_link_count === "number" ? inventory.source_link_count : typeof inventory.home_game_link_count === "number" ? inventory.home_game_link_count : null;
1052
+ const sourceUniqueLinkCount = typeof inventory.source_unique_link_count === "number" ? inventory.source_unique_link_count : typeof inventory.home_unique_game_link_count === "number" ? inventory.home_unique_game_link_count : null;
1053
+ const duplicateSourceLinks = Array.isArray(inventory.duplicate_source_link_paths) ? inventory.duplicate_source_link_paths.map((path) => String(path)) : [];
1054
+ const duplicateSourceLinkCount = typeof inventory.duplicate_source_link_count === "number" ? inventory.duplicate_source_link_count : sourceLinkCount !== null && sourceUniqueLinkCount !== null ? Math.max(0, sourceLinkCount - sourceUniqueLinkCount) : null;
1055
+ const failures = Array.isArray(inventory.failures) ? inventory.failures : [];
1056
+ return {
1057
+ viewport,
1058
+ source_link_count: sourceLinkCount,
1059
+ source_unique_link_count: sourceUniqueLinkCount,
1060
+ duplicate_source_link_count: duplicateSourceLinkCount,
1061
+ duplicate_source_links: duplicateSourceLinks,
1062
+ direct_route_count: directRoutes.length,
1063
+ clickthrough_count: clickthroughs.length,
1064
+ failure_count: failures.length,
1065
+ };
1066
+ }
939
1067
  function numberValue(value) {
940
1068
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
941
1069
  }
@@ -1140,6 +1268,29 @@ function assessProfile(profile, evidence) {
1140
1268
  });
1141
1269
  continue;
1142
1270
  }
1271
+ if (check.type === "selector_text_order") {
1272
+ const selector = check.selector || "";
1273
+ const expectedTexts = check.expected_texts || [];
1274
+ const results = viewports.map((viewport) => {
1275
+ const texts = textSequenceForCheck(viewport, check);
1276
+ const match = textOrderMatch(texts, expectedTexts);
1277
+ return {
1278
+ viewport: viewport.name,
1279
+ matched: match.matched,
1280
+ matched_positions: match.positions,
1281
+ visible_texts: texts.slice(0, 20),
1282
+ };
1283
+ });
1284
+ const failed = results.filter((result) => !result.matched).length;
1285
+ checks.push({
1286
+ type: check.type,
1287
+ label: check.label || check.type,
1288
+ status: failed ? "failed" : "passed",
1289
+ evidence: { selector, expected_texts: expectedTexts, viewports: results },
1290
+ message: failed ? "Selector " + selector + " text order failed in " + failed + " viewport(s)." : undefined,
1291
+ });
1292
+ continue;
1293
+ }
1143
1294
  if (check.type === "text_visible" || check.type === "text_absent") {
1144
1295
  const key = check.pattern ? "pattern:" + check.pattern + "/" + (check.flags || "") : "text:" + (check.text || "");
1145
1296
  const expectedVisible = check.type === "text_visible";
@@ -1168,6 +1319,7 @@ function assessProfile(profile, evidence) {
1168
1319
  });
1169
1320
  continue;
1170
1321
  }
1322
+ const viewportSummaries = inventories.map((item) => summarizeRouteInventory(item.viewport, item.inventory || {}));
1171
1323
  const failures = [];
1172
1324
  for (const item of inventories) {
1173
1325
  for (const failure of Array.isArray(item.inventory.failures) ? item.inventory.failures : []) {
@@ -1196,6 +1348,8 @@ function assessProfile(profile, evidence) {
1196
1348
  homepage_unique_link_count: typeof first.home_unique_game_link_count === "number" ? first.home_unique_game_link_count : sourceUniqueLinkCount,
1197
1349
  direct_route_count: directRoutes.length,
1198
1350
  clickthrough_count: clickthroughs.length,
1351
+ viewport_count: inventories.length,
1352
+ viewports: viewportSummaries,
1199
1353
  failures,
1200
1354
  },
1201
1355
  message: failures.length ? "Route inventory failed with " + failures.length + " issue(s)." : undefined,
@@ -1513,6 +1667,27 @@ async function selectorStats(selector) {
1513
1667
  return { count: elements.length, visible_count: elements.filter(isVisible).length };
1514
1668
  }).catch((error) => ({ count: 0, visible_count: 0, error: String(error && error.message ? error.message : error).slice(0, 500) }));
1515
1669
  }
1670
+ async function selectorTextSequence(selector) {
1671
+ return page.locator(selector).evaluateAll((elements) => {
1672
+ const compact = (value) => String(value || "").replace(/\s+/g, " ").trim().slice(0, 240);
1673
+ const isVisible = (element) => {
1674
+ const style = window.getComputedStyle(element);
1675
+ const rect = element.getBoundingClientRect();
1676
+ return style && style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
1677
+ };
1678
+ const rows = elements.map((element, index) => ({
1679
+ index,
1680
+ text: compact(element.innerText || element.textContent || ""),
1681
+ visible: isVisible(element),
1682
+ }));
1683
+ return {
1684
+ count: rows.length,
1685
+ visible_count: rows.filter((row) => row.visible).length,
1686
+ texts: rows.map((row) => row.text).filter(Boolean).slice(0, 40),
1687
+ visible_texts: rows.filter((row) => row.visible).map((row) => row.text).filter(Boolean).slice(0, 40),
1688
+ };
1689
+ }).catch((error) => ({ count: 0, visible_count: 0, texts: [], visible_texts: [], error: String(error && error.message ? error.message : error).slice(0, 500) }));
1690
+ }
1516
1691
  function inventoryAppPathFromUrl(urlLike) {
1517
1692
  const url = new URL(String(urlLike), targetUrl);
1518
1693
  const mountPrefix = previewMountPrefix(new URL(targetUrl).pathname);
@@ -1910,11 +2085,16 @@ async function captureViewport(viewport) {
1910
2085
  evaluation_error: String(error && error.message ? error.message : error).slice(0, 1000),
1911
2086
  }));
1912
2087
  const selectors = {};
2088
+ const text_sequences = {};
1913
2089
  const text_matches = {};
1914
2090
  for (const check of profile.checks || []) {
1915
2091
  if ((check.type === "selector_visible" || check.type === "selector_count_at_least") && check.selector) {
1916
2092
  selectors[check.selector] = await selectorStats(check.selector);
1917
2093
  }
2094
+ if (check.type === "selector_text_order" && check.selector) {
2095
+ selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
2096
+ text_sequences[check.selector] = await selectorTextSequence(check.selector);
2097
+ }
1918
2098
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
1919
2099
  text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
1920
2100
  }
@@ -1928,7 +2108,7 @@ async function captureViewport(viewport) {
1928
2108
  let routeInventory;
1929
2109
  const routeInventoryCheck = (profile.checks || []).find((check) => check.type === "route_inventory");
1930
2110
  const firstViewportName = (profile.target.viewports || [])[0] && (profile.target.viewports || [])[0].name;
1931
- if (routeInventoryCheck && (!firstViewportName || viewport.name === firstViewportName)) {
2111
+ if (routeInventoryCheck && (routeInventoryCheck.run_all_viewports || !firstViewportName || viewport.name === firstViewportName)) {
1932
2112
  try {
1933
2113
  routeInventory = await collectRouteInventory(routeInventoryCheck, viewport);
1934
2114
  } catch (error) {
@@ -1978,6 +2158,7 @@ async function captureViewport(viewport) {
1978
2158
  bounds_overflow_px: dom.bounds_overflow_px,
1979
2159
  overflow_offenders: dom.overflow_offenders || [],
1980
2160
  selectors,
2161
+ text_sequences,
1981
2162
  text_matches,
1982
2163
  route_inventory: routeInventory,
1983
2164
  setup_action_results: setupActionResults,
@@ -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", "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", "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];
@@ -66,6 +66,7 @@ interface RiddleProofProfileCheck {
66
66
  expected_path?: string;
67
67
  expected_routes?: RiddleProofProfileRouteInventoryRoute[];
68
68
  selector?: string;
69
+ expected_texts?: string[];
69
70
  link_selector?: string;
70
71
  source_selector?: string;
71
72
  route_path_prefix?: string;
@@ -81,6 +82,7 @@ interface RiddleProofProfileCheck {
81
82
  timeout_ms?: number;
82
83
  run_direct_routes?: boolean;
83
84
  run_clickthroughs?: boolean;
85
+ run_all_viewports?: boolean;
84
86
  require_unique_routes?: boolean;
85
87
  allow_unexpected_routes?: boolean;
86
88
  save_route_screenshots?: boolean;
@@ -137,6 +139,7 @@ interface RiddleProofProfileViewportEvidence {
137
139
  count: number;
138
140
  visible_count: number;
139
141
  }>;
142
+ text_sequences?: Record<string, Record<string, JsonValue>>;
140
143
  text_matches?: Record<string, boolean>;
141
144
  route_inventory?: Record<string, JsonValue>;
142
145
  setup_action_results?: Array<Record<string, JsonValue>>;