@riddledc/riddle-proof 0.7.58 → 0.7.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -270,6 +270,13 @@ can include `repeat` / `repeat_count` / `times` from 1 to 100; each repetition
270
270
  is recorded with `repeat_index` and `repeat_count`, and `after_ms` runs after
271
271
  each repetition. Use it for bounded game proof helpers, retry controls, or other
272
272
  workflows where one declarative action needs to advance the app several times.
273
+ Add `frame_selector` / `frameSelector` to a setup action when the interaction
274
+ target lives inside an embedded iframe, such as a community game player or
275
+ hosted preview surface. Selector-based actions, storage actions, window calls,
276
+ and setup assertions then execute in that frame context and record
277
+ `frame_selector`, `frame_index`, and `frame_count` in setup-action evidence.
278
+ Use `frame_index` / `frameIndex` when more than one matching iframe is present;
279
+ it defaults to the first frame.
273
280
 
274
281
  `target.timeout_sec` is optional. Use it for known-heavy profile targets so the
275
282
  profile carries its own hosted Riddle worker budget; an explicit CLI `--timeout`
@@ -348,6 +355,11 @@ game, or preview surfaces that render inside iframes:
348
355
 
349
356
  ```json
350
357
  [
358
+ {
359
+ "type": "frame_url_matches",
360
+ "selector": ".game-player-root iframe",
361
+ "pattern": "/saved/hot-path-.+/index\\.html$"
362
+ },
351
363
  {
352
364
  "type": "frame_text_visible",
353
365
  "selector": ".game-player-root iframe",
@@ -361,6 +373,11 @@ game, or preview surfaces that render inside iframes:
361
373
  ]
362
374
  ```
363
375
 
376
+ Use `frame_url_equals` when the iframe must resolve to one exact embedded
377
+ resource, or `frame_url_matches` when a preview/job/saved-game URL has a stable
378
+ shape but a generated ID. URL checks fail when the frame is missing, just like
379
+ frame text and overflow checks.
380
+
364
381
  Frame checks capture each matching iframe's URL, title, compact text sample,
365
382
  scroll width, client width, measured horizontal overflow, and top visible
366
383
  overflow offenders. This keeps embedded-player audits in profile mode instead
@@ -22,6 +22,8 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
22
22
  "selector_count_eq",
23
23
  "selector_text_order",
24
24
  "frame_text_visible",
25
+ "frame_url_equals",
26
+ "frame_url_matches",
25
27
  "frame_no_horizontal_overflow",
26
28
  "text_visible",
27
29
  "text_absent",
@@ -236,6 +238,11 @@ function normalizeSetupAction(input, index) {
236
238
  if (!isRecord(input)) throw new Error(`target.setup_actions[${index}] must be an object.`);
237
239
  const type = normalizeSetupActionType(stringValue(input.type), index);
238
240
  const selector = stringValue(input.selector);
241
+ const frameSelector = stringFromOwn(input, "frame_selector", "frameSelector", "iframe_selector", "iframeSelector");
242
+ const frameIndex = numberValue(valueFromOwn(input, "frame_index", "frameIndex", "iframe_index", "iframeIndex"));
243
+ if (frameIndex !== void 0 && (!Number.isInteger(frameIndex) || frameIndex < 0)) {
244
+ throw new Error(`target.setup_actions[${index}].frame_index must be a non-negative integer.`);
245
+ }
239
246
  if ((type === "click" || type === "fill" || type === "set_input_value" || type === "wait_for_selector" || type === "wait_for_text" || type === "assert_text_visible" || type === "assert_text_absent" || type === "assert_selector_count") && !selector) {
240
247
  throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
241
248
  }
@@ -288,6 +295,8 @@ function normalizeSetupAction(input, index) {
288
295
  return {
289
296
  type,
290
297
  selector,
298
+ frame_selector: frameSelector,
299
+ frame_index: frameIndex,
291
300
  force: type === "click" && (input.force === true || input.force_click === true || input.forceClick === true),
292
301
  key,
293
302
  value,
@@ -499,12 +508,19 @@ function normalizeCheck(input, index) {
499
508
  if ((type === "selector_visible" || type === "selector_absent" || type === "selector_count_at_least" || type === "selector_count_equals" || type === "selector_count_equal" || type === "selector_count_eq") && !stringValue(input.selector)) {
500
509
  throw new Error(`checks[${index}] ${type} requires selector.`);
501
510
  }
502
- if ((type === "frame_text_visible" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
511
+ if ((type === "frame_text_visible" || type === "frame_url_equals" || type === "frame_url_matches" || type === "frame_no_horizontal_overflow") && !stringValue(input.selector)) {
503
512
  throw new Error(`checks[${index}] ${type} requires selector.`);
504
513
  }
505
514
  if (type === "frame_text_visible" && !stringValue(input.text) && !stringValue(input.pattern)) {
506
515
  throw new Error(`checks[${index}] frame_text_visible requires text or pattern.`);
507
516
  }
517
+ const expectedUrl = stringFromOwn(input, "expected_url", "expectedUrl", "url", "expected_value", "expectedValue", "value");
518
+ if (type === "frame_url_equals" && expectedUrl === void 0) {
519
+ throw new Error(`checks[${index}] frame_url_equals requires expected_url.`);
520
+ }
521
+ if (type === "frame_url_matches" && !stringValue(input.pattern)) {
522
+ throw new Error(`checks[${index}] frame_url_matches requires pattern.`);
523
+ }
508
524
  if ((type === "text_visible" || type === "text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
509
525
  throw new Error(`checks[${index}] ${type} requires text or pattern.`);
510
526
  }
@@ -537,6 +553,7 @@ function normalizeCheck(input, index) {
537
553
  expected_path: stringValue(input.expected_path),
538
554
  param: stringValue(input.param) || stringValue(input.search_param) || stringValue(input.searchParam) || stringValue(input.key),
539
555
  expected_value: expectedValue,
556
+ expected_url: expectedUrl,
540
557
  expected_routes: expectedRoutes,
541
558
  selector: stringValue(input.selector),
542
559
  expected_texts: expectedTexts,
@@ -1004,6 +1021,35 @@ function assessCheckFromEvidence(check, evidence) {
1004
1021
  message: failed ? `Frame selector ${key} did not contain expected text in ${failed} viewport(s).` : void 0
1005
1022
  };
1006
1023
  }
1024
+ if (check.type === "frame_url_equals" || check.type === "frame_url_matches") {
1025
+ const key = selectorKey(check);
1026
+ const expectedUrl = check.expected_url || check.expected_value || "";
1027
+ const results = viewports.map((viewport) => {
1028
+ const frames = frameEvidenceForSelector(viewport, key);
1029
+ const urls = frames.map((frame) => String(frame.url || "")).filter(Boolean);
1030
+ const matches = urls.filter((url) => check.type === "frame_url_equals" ? url === expectedUrl : matchText(url, check));
1031
+ return {
1032
+ viewport: viewport.name,
1033
+ frame_count: frames.length,
1034
+ matched_count: matches.length,
1035
+ matched: matches.length > 0,
1036
+ urls: urls.slice(0, 10)
1037
+ };
1038
+ });
1039
+ const failed = results.filter((result) => !result.matched).length;
1040
+ return {
1041
+ type: check.type,
1042
+ label: checkLabel(check),
1043
+ status: failed ? "failed" : "passed",
1044
+ evidence: {
1045
+ selector: key,
1046
+ expected_url: check.type === "frame_url_equals" ? expectedUrl : null,
1047
+ pattern: check.type === "frame_url_matches" ? check.pattern || null : null,
1048
+ viewports: results.map((result) => toJsonValue(result))
1049
+ },
1050
+ message: failed ? `Frame selector ${key} URL assertion failed in ${failed} viewport(s).` : void 0
1051
+ };
1052
+ }
1007
1053
  if (check.type === "frame_no_horizontal_overflow") {
1008
1054
  const key = selectorKey(check);
1009
1055
  const maxOverflow = check.max_overflow_px ?? 4;
@@ -1174,6 +1220,8 @@ function assessSetupActionsFromEvidence(profile, evidence) {
1174
1220
  viewport: viewport.name,
1175
1221
  action: result.action ?? result.type ?? null,
1176
1222
  selector: result.selector ?? null,
1223
+ frame_selector: result.frame_selector ?? null,
1224
+ frame_index: result.frame_index ?? null,
1177
1225
  reason: result.reason ?? result.error ?? null
1178
1226
  });
1179
1227
  }
@@ -1686,10 +1734,12 @@ function assessProfile(profile, evidence) {
1686
1734
  if (result && result.ok === false) {
1687
1735
  failed.push({
1688
1736
  viewport: viewport.name,
1689
- action: result.action || result.type || null,
1690
- selector: result.selector || null,
1691
- reason: result.reason || result.error || null,
1692
- });
1737
+ action: result.action || result.type || null,
1738
+ selector: result.selector || null,
1739
+ frame_selector: result.frame_selector || null,
1740
+ frame_index: result.frame_index ?? null,
1741
+ reason: result.reason || result.error || null,
1742
+ });
1693
1743
  }
1694
1744
  }
1695
1745
  if (results.length < actionCount && results.every((result) => !result || result.ok !== false)) {
@@ -1891,6 +1941,36 @@ function assessProfile(profile, evidence) {
1891
1941
  });
1892
1942
  continue;
1893
1943
  }
1944
+ if (check.type === "frame_url_equals" || check.type === "frame_url_matches") {
1945
+ const selector = check.selector || "";
1946
+ const expectedUrl = check.expected_url || check.expected_value || "";
1947
+ const results = checkViewports.map((viewport) => {
1948
+ const frames = frameEvidenceForSelector(viewport, selector);
1949
+ const urls = frames.map((frame) => String(frame.url || "")).filter(Boolean);
1950
+ const matches = urls.filter((url) => check.type === "frame_url_equals" ? url === expectedUrl : textMatches(url, check));
1951
+ return {
1952
+ viewport: viewport.name,
1953
+ frame_count: frames.length,
1954
+ matched_count: matches.length,
1955
+ matched: matches.length > 0,
1956
+ urls: urls.slice(0, 10),
1957
+ };
1958
+ });
1959
+ const failed = results.filter((result) => !result.matched).length;
1960
+ checks.push({
1961
+ type: check.type,
1962
+ label: check.label || check.type,
1963
+ status: failed ? "failed" : "passed",
1964
+ evidence: {
1965
+ selector,
1966
+ expected_url: check.type === "frame_url_equals" ? expectedUrl : null,
1967
+ pattern: check.type === "frame_url_matches" ? check.pattern || null : null,
1968
+ viewports: results,
1969
+ },
1970
+ message: failed ? "Frame selector " + selector + " URL assertion failed in " + failed + " viewport(s)." : undefined,
1971
+ });
1972
+ continue;
1973
+ }
1894
1974
  if (check.type === "frame_no_horizontal_overflow") {
1895
1975
  const selector = check.selector || "";
1896
1976
  const maxOverflow = check.max_overflow_px == null ? 4 : check.max_overflow_px;
@@ -2152,8 +2232,8 @@ function setupJsonValue(value) {
2152
2232
  function setupValuesEqual(left, right) {
2153
2233
  return JSON.stringify(setupJsonValue(left)) === JSON.stringify(setupJsonValue(right));
2154
2234
  }
2155
- async function setupReadWindowValue(path) {
2156
- return await page.evaluate(({ path }) => {
2235
+ async function setupReadWindowValue(context, path) {
2236
+ return await context.evaluate(({ path }) => {
2157
2237
  const toJsonValue = (value) => {
2158
2238
  if (value === null || value === undefined) return null;
2159
2239
  if (typeof value === "string" || typeof value === "boolean") return value;
@@ -2175,6 +2255,80 @@ async function setupReadWindowValue(path) {
2175
2255
  return { ok: true, value: toJsonValue(current) };
2176
2256
  }, { path });
2177
2257
  }
2258
+ function setupFrameSelector(action) {
2259
+ return String(action?.frame_selector || action?.frameSelector || action?.iframe_selector || action?.iframeSelector || "").trim();
2260
+ }
2261
+ function setupFrameIndex(action) {
2262
+ const raw = action?.frame_index ?? action?.frameIndex ?? action?.iframe_index ?? action?.iframeIndex ?? 0;
2263
+ const number = Number(raw);
2264
+ return Number.isInteger(number) && number >= 0 ? number : 0;
2265
+ }
2266
+ function setupScopeEvidence(scope) {
2267
+ if (!scope || !scope.frame_selector) return {};
2268
+ return {
2269
+ frame_selector: scope.frame_selector,
2270
+ frame_index: scope.frame_index,
2271
+ frame_count: scope.frame_count,
2272
+ };
2273
+ }
2274
+ function setupScopeFailure(base, scope) {
2275
+ return {
2276
+ ...base,
2277
+ ...setupScopeEvidence(scope),
2278
+ reason: scope?.reason || "frame_scope_unavailable",
2279
+ error: scope?.error || undefined,
2280
+ };
2281
+ }
2282
+ async function setupActionScope(action, timeout) {
2283
+ const frameSelector = setupFrameSelector(action);
2284
+ if (!frameSelector) return { ok: true, context: page };
2285
+ const frameIndex = setupFrameIndex(action);
2286
+ let frameCount = 0;
2287
+ let locator = null;
2288
+ try {
2289
+ await page.waitForSelector(frameSelector, { state: "attached", timeout });
2290
+ locator = page.locator(frameSelector);
2291
+ frameCount = await locator.count();
2292
+ } catch (error) {
2293
+ return {
2294
+ ok: false,
2295
+ reason: "frame_selector_not_found",
2296
+ frame_selector: frameSelector,
2297
+ frame_index: frameIndex,
2298
+ frame_count: frameCount,
2299
+ error: String(error && error.message ? error.message : error).slice(0, 1000),
2300
+ };
2301
+ }
2302
+ if (!frameCount) {
2303
+ return { ok: false, reason: "frame_selector_not_found", frame_selector: frameSelector, frame_index: frameIndex, frame_count: frameCount };
2304
+ }
2305
+ if (frameIndex >= frameCount) {
2306
+ return { ok: false, reason: "frame_index_out_of_range", frame_selector: frameSelector, frame_index: frameIndex, frame_count: frameCount };
2307
+ }
2308
+ const handle = await locator.nth(frameIndex).elementHandle({ timeout }).catch((error) => ({ __riddle_error: error }));
2309
+ if (!handle || handle.__riddle_error) {
2310
+ return {
2311
+ ok: false,
2312
+ reason: "frame_element_unavailable",
2313
+ frame_selector: frameSelector,
2314
+ frame_index: frameIndex,
2315
+ frame_count: frameCount,
2316
+ error: handle?.__riddle_error ? String(handle.__riddle_error && handle.__riddle_error.message ? handle.__riddle_error.message : handle.__riddle_error).slice(0, 1000) : undefined,
2317
+ };
2318
+ }
2319
+ const frame = typeof handle.contentFrame === "function" ? await handle.contentFrame().catch((error) => ({ __riddle_error: error })) : null;
2320
+ if (!frame || frame.__riddle_error) {
2321
+ return {
2322
+ ok: false,
2323
+ reason: "content_frame_unavailable",
2324
+ frame_selector: frameSelector,
2325
+ frame_index: frameIndex,
2326
+ frame_count: frameCount,
2327
+ error: frame?.__riddle_error ? String(frame.__riddle_error && frame.__riddle_error.message ? frame.__riddle_error.message : frame.__riddle_error).slice(0, 1000) : undefined,
2328
+ };
2329
+ }
2330
+ return { ok: true, context: frame, frame_selector: frameSelector, frame_index: frameIndex, frame_count: frameCount };
2331
+ }
2178
2332
  async function setupLocatorText(locator, index) {
2179
2333
  return await locator.nth(index).textContent({ timeout: 1000 }).catch(() => "");
2180
2334
  }
@@ -2336,7 +2490,8 @@ async function registerNetworkMocks(mocks) {
2336
2490
  }
2337
2491
  async function executeSetupAction(action, ordinal) {
2338
2492
  const type = setupActionType(action);
2339
- const base = { ok: false, action: type || "unknown", ordinal, selector: action.selector || null };
2493
+ const frameSelector = setupFrameSelector(action);
2494
+ const base = { ok: false, action: type || "unknown", ordinal, selector: action.selector || null, frame_selector: frameSelector || null };
2340
2495
  const timeout = setupNumber(action.timeout_ms, 5000);
2341
2496
  try {
2342
2497
  if (type === "wait") {
@@ -2345,51 +2500,65 @@ async function executeSetupAction(action, ordinal) {
2345
2500
  return { ...base, ok: true, ms };
2346
2501
  }
2347
2502
  if (type === "wait_for_selector") {
2348
- await page.waitForSelector(action.selector, { state: "visible", timeout });
2349
- return { ...base, ok: true, timeout_ms: timeout };
2503
+ const scope = await setupActionScope(action, timeout);
2504
+ if (!scope.ok) return setupScopeFailure(base, scope);
2505
+ await scope.context.waitForSelector(action.selector, { state: "visible", timeout });
2506
+ return { ...base, ...setupScopeEvidence(scope), ok: true, timeout_ms: timeout };
2350
2507
  }
2351
2508
  if (type === "press") {
2352
2509
  const key = String(action.key || "").trim();
2353
2510
  if (!key) return { ...base, reason: "missing_key" };
2511
+ const scope = await setupActionScope(action, timeout);
2512
+ if (!scope.ok) return setupScopeFailure(base, scope);
2354
2513
  if (!action.selector) {
2355
- await page.keyboard.press(key);
2356
- return { ...base, ok: true, key };
2514
+ if (scope.frame_selector) {
2515
+ await scope.context.locator("body").press(key, { timeout });
2516
+ } else {
2517
+ await page.keyboard.press(key);
2518
+ }
2519
+ return { ...base, ...setupScopeEvidence(scope), ok: true, key };
2357
2520
  }
2358
- const locator = page.locator(action.selector);
2521
+ const locator = scope.context.locator(action.selector);
2359
2522
  const count = await locator.count();
2360
2523
  if (!count) return { ...base, reason: "selector_not_found", count, key };
2361
2524
  const targetIndex = Number.isInteger(action.index) ? action.index : 0;
2362
2525
  if (targetIndex < 0 || targetIndex >= count) return { ...base, reason: "index_out_of_range", count, target_index: targetIndex, key };
2363
2526
  await locator.nth(targetIndex).press(key, { timeout });
2364
- return { ...base, ok: true, count, target_index: targetIndex, key };
2527
+ return { ...base, ...setupScopeEvidence(scope), ok: true, count, target_index: targetIndex, key };
2365
2528
  }
2366
2529
  if (type === "local_storage" || type === "session_storage") {
2367
2530
  const value = setupActionValue(action);
2368
- await page.evaluate(({ type, key, value }) => {
2531
+ const scope = await setupActionScope(action, timeout);
2532
+ if (!scope.ok) return setupScopeFailure(base, scope);
2533
+ await scope.context.evaluate(({ type, key, value }) => {
2369
2534
  const storage = type === "session_storage" ? window.sessionStorage : window.localStorage;
2370
2535
  storage.setItem(key, value);
2371
2536
  }, { type, key: action.key, value });
2372
2537
  if (action.reload === true) {
2373
2538
  await page.reload({ waitUntil: "domcontentloaded", timeout: 45000 });
2374
2539
  }
2375
- return { ...base, ok: true, key: action.key, value_length: value.length, reload: action.reload === true };
2540
+ return { ...base, ...setupScopeEvidence(scope), ok: true, key: action.key, value_length: value.length, reload: action.reload === true };
2376
2541
  }
2377
2542
  if (type === "clear_storage") {
2378
2543
  const storage = action.storage || "both";
2379
- await page.evaluate(({ storage }) => {
2544
+ const scope = await setupActionScope(action, timeout);
2545
+ if (!scope.ok) return setupScopeFailure(base, scope);
2546
+ await scope.context.evaluate(({ storage }) => {
2380
2547
  if (storage === "local" || storage === "both") window.localStorage.clear();
2381
2548
  if (storage === "session" || storage === "both") window.sessionStorage.clear();
2382
2549
  }, { storage });
2383
2550
  if (action.reload === true) {
2384
2551
  await page.reload({ waitUntil: "domcontentloaded", timeout: 45000 });
2385
2552
  }
2386
- return { ...base, ok: true, storage, reload: action.reload === true };
2553
+ return { ...base, ...setupScopeEvidence(scope), ok: true, storage, reload: action.reload === true };
2387
2554
  }
2388
2555
  if (type === "window_call") {
2389
2556
  const path = String(action.path || action.function_path || action.functionPath || "");
2390
2557
  const args = Array.isArray(action.args) ? action.args : [];
2391
2558
  if (!path) return { ...base, path, reason: "missing_path" };
2392
- const result = await page.evaluate(async ({ path, args }) => {
2559
+ const scope = await setupActionScope(action, timeout);
2560
+ if (!scope.ok) return setupScopeFailure(base, scope);
2561
+ const result = await scope.context.evaluate(async ({ path, args }) => {
2393
2562
  const toJsonValue = (value) => {
2394
2563
  if (value === null || value === undefined) return null;
2395
2564
  if (typeof value === "string" || typeof value === "boolean") return value;
@@ -2429,6 +2598,7 @@ async function executeSetupAction(action, ordinal) {
2429
2598
  const expectationMet = !hasExpectation || setupValuesEqual(result.returned, expected);
2430
2599
  return {
2431
2600
  ...base,
2601
+ ...setupScopeEvidence(scope),
2432
2602
  ok: Boolean(result.ok && expectationMet),
2433
2603
  path,
2434
2604
  arg_count: args.length,
@@ -2459,13 +2629,16 @@ async function executeSetupAction(action, ordinal) {
2459
2629
  : action.expect;
2460
2630
  if (!path) return { ...base, path, reason: "missing_path" };
2461
2631
  if (!hasExpected) return { ...base, path, reason: "missing_expected_value" };
2632
+ const scope = await setupActionScope(action, timeout);
2633
+ if (!scope.ok) return setupScopeFailure(base, scope);
2462
2634
  const startedAt = Date.now();
2463
2635
  let result = null;
2464
2636
  while (Date.now() - startedAt <= timeout) {
2465
- result = await setupReadWindowValue(path);
2637
+ result = await setupReadWindowValue(scope.context, path);
2466
2638
  if (result.ok && setupValuesEqual(result.value, expected)) {
2467
2639
  return {
2468
2640
  ...base,
2641
+ ...setupScopeEvidence(scope),
2469
2642
  ok: true,
2470
2643
  path,
2471
2644
  value: setupJsonValue(result.value),
@@ -2477,6 +2650,7 @@ async function executeSetupAction(action, ordinal) {
2477
2650
  }
2478
2651
  return {
2479
2652
  ...base,
2653
+ ...setupScopeEvidence(scope),
2480
2654
  path,
2481
2655
  reason: result?.ok ? "unexpected_value" : result?.reason || "path_not_found",
2482
2656
  missing_part: result?.missing_part || undefined,
@@ -2493,11 +2667,13 @@ async function executeSetupAction(action, ordinal) {
2493
2667
  const hasExpected = expected !== undefined;
2494
2668
  if (!path) return { ...base, path, reason: "missing_path" };
2495
2669
  if (!hasExpected && minValue === undefined && maxValue === undefined) return { ...base, path, reason: "missing_number_expectation" };
2670
+ const scope = await setupActionScope(action, timeout);
2671
+ if (!scope.ok) return setupScopeFailure(base, scope);
2496
2672
  const startedAt = Date.now();
2497
2673
  let result = null;
2498
2674
  let lastReason = "path_not_found";
2499
2675
  while (Date.now() - startedAt <= timeout) {
2500
- result = await setupReadWindowValue(path);
2676
+ result = await setupReadWindowValue(scope.context, path);
2501
2677
  if (result.ok) {
2502
2678
  const actual = setupFiniteNumber(result.value);
2503
2679
  if (actual === undefined) {
@@ -2511,6 +2687,7 @@ async function executeSetupAction(action, ordinal) {
2511
2687
  } else {
2512
2688
  return {
2513
2689
  ...base,
2690
+ ...setupScopeEvidence(scope),
2514
2691
  ok: true,
2515
2692
  path,
2516
2693
  value: actual,
@@ -2527,6 +2704,7 @@ async function executeSetupAction(action, ordinal) {
2527
2704
  }
2528
2705
  return {
2529
2706
  ...base,
2707
+ ...setupScopeEvidence(scope),
2530
2708
  path,
2531
2709
  reason: lastReason,
2532
2710
  missing_part: result?.missing_part || undefined,
@@ -2538,7 +2716,9 @@ async function executeSetupAction(action, ordinal) {
2538
2716
  };
2539
2717
  }
2540
2718
  if (type === "click") {
2541
- const locator = page.locator(action.selector);
2719
+ const scope = await setupActionScope(action, timeout);
2720
+ if (!scope.ok) return setupScopeFailure(base, scope);
2721
+ const locator = scope.context.locator(action.selector);
2542
2722
  const count = await locator.count();
2543
2723
  if (!count) return { ...base, reason: "selector_not_found", count };
2544
2724
  let targetIndex = Number.isInteger(action.index) ? action.index : 0;
@@ -2570,33 +2750,39 @@ async function executeSetupAction(action, ordinal) {
2570
2750
  ? { timeout, noWaitAfter: true, force: true }
2571
2751
  : { timeout, noWaitAfter: true };
2572
2752
  await locator.nth(targetIndex).click(clickOptions);
2573
- return { ...base, ok: true, count, target_index: targetIndex, text: matchedText, force: action.force === true || undefined };
2753
+ return { ...base, ...setupScopeEvidence(scope), ok: true, count, target_index: targetIndex, text: matchedText, force: action.force === true || undefined };
2574
2754
  }
2575
2755
  if (type === "fill" || type === "set_input_value") {
2576
- const locator = page.locator(action.selector);
2756
+ const scope = await setupActionScope(action, timeout);
2757
+ if (!scope.ok) return setupScopeFailure(base, scope);
2758
+ const locator = scope.context.locator(action.selector);
2577
2759
  const count = await locator.count();
2578
2760
  if (!count) return { ...base, reason: "selector_not_found", count };
2579
2761
  const targetIndex = Number.isInteger(action.index) ? action.index : 0;
2580
2762
  if (targetIndex < 0 || targetIndex >= count) return { ...base, reason: "index_out_of_range", count, target_index: targetIndex };
2581
2763
  const value = setupActionValue(action);
2582
2764
  await locator.nth(targetIndex).fill(value, { timeout });
2583
- return { ...base, ok: true, count, target_index: targetIndex, value_length: value.length };
2765
+ return { ...base, ...setupScopeEvidence(scope), ok: true, count, target_index: targetIndex, value_length: value.length };
2584
2766
  }
2585
2767
  if (type === "assert_selector_count") {
2586
- const locator = page.locator(action.selector);
2768
+ const scope = await setupActionScope(action, timeout);
2769
+ if (!scope.ok) return setupScopeFailure(base, scope);
2770
+ const locator = scope.context.locator(action.selector);
2587
2771
  const expectedCount = setupNumber(action.expected_count, -1);
2588
2772
  if (!Number.isInteger(expectedCount) || expectedCount < 0) return { ...base, reason: "invalid_expected_count", expected_count: action.expected_count };
2589
2773
  const startedAt = Date.now();
2590
2774
  let count = 0;
2591
2775
  while (Date.now() - startedAt <= timeout) {
2592
2776
  count = await locator.count().catch(() => 0);
2593
- if (count === expectedCount) return { ...base, ok: true, count, expected_count: expectedCount, timeout_ms: timeout };
2777
+ if (count === expectedCount) return { ...base, ...setupScopeEvidence(scope), ok: true, count, expected_count: expectedCount, timeout_ms: timeout };
2594
2778
  await page.waitForTimeout(100);
2595
2779
  }
2596
- return { ...base, reason: "selector_count_mismatch", count, expected_count: expectedCount, timeout_ms: timeout };
2780
+ return { ...base, ...setupScopeEvidence(scope), reason: "selector_count_mismatch", count, expected_count: expectedCount, timeout_ms: timeout };
2597
2781
  }
2598
2782
  if (type === "wait_for_text") {
2599
- const locator = page.locator(action.selector);
2783
+ const scope = await setupActionScope(action, timeout);
2784
+ if (!scope.ok) return setupScopeFailure(base, scope);
2785
+ const locator = scope.context.locator(action.selector);
2600
2786
  const startedAt = Date.now();
2601
2787
  let lastText = "";
2602
2788
  while (Date.now() - startedAt <= timeout) {
@@ -2605,15 +2791,17 @@ async function executeSetupAction(action, ordinal) {
2605
2791
  const text = await setupLocatorText(locator, index);
2606
2792
  lastText = text || lastText;
2607
2793
  if (setupTextMatches(text, action)) {
2608
- return { ...base, ok: true, text: compactSetupResultText(text), target_index: index, timeout_ms: timeout };
2794
+ return { ...base, ...setupScopeEvidence(scope), ok: true, text: compactSetupResultText(text), target_index: index, timeout_ms: timeout };
2609
2795
  }
2610
2796
  }
2611
2797
  await page.waitForTimeout(100);
2612
2798
  }
2613
- return { ...base, reason: "text_not_found", text: compactSetupResultText(lastText), timeout_ms: timeout };
2799
+ return { ...base, ...setupScopeEvidence(scope), reason: "text_not_found", text: compactSetupResultText(lastText), timeout_ms: timeout };
2614
2800
  }
2615
2801
  if (type === "assert_text_visible" || type === "assert_text_absent") {
2616
- const locator = page.locator(action.selector);
2802
+ const scope = await setupActionScope(action, timeout);
2803
+ if (!scope.ok) return setupScopeFailure(base, scope);
2804
+ const locator = scope.context.locator(action.selector);
2617
2805
  const startedAt = Date.now();
2618
2806
  let lastText = "";
2619
2807
  let matchedText = "";
@@ -2632,7 +2820,7 @@ async function executeSetupAction(action, ordinal) {
2632
2820
  if (type === "assert_text_visible") {
2633
2821
  const visible = await setupLocatorVisible(locator, index);
2634
2822
  if (visible) {
2635
- return { ...base, ok: true, count, text: compactSetupResultText(text), target_index: index, timeout_ms: timeout };
2823
+ return { ...base, ...setupScopeEvidence(scope), ok: true, count, text: compactSetupResultText(text), target_index: index, timeout_ms: timeout };
2636
2824
  }
2637
2825
  hiddenMatch = true;
2638
2826
  break;
@@ -2641,17 +2829,17 @@ async function executeSetupAction(action, ordinal) {
2641
2829
  }
2642
2830
  }
2643
2831
  if (type === "assert_text_absent" && !matched) {
2644
- return { ...base, ok: true, count, timeout_ms: timeout };
2832
+ return { ...base, ...setupScopeEvidence(scope), ok: true, count, timeout_ms: timeout };
2645
2833
  }
2646
2834
  await page.waitForTimeout(100);
2647
2835
  }
2648
2836
  if (type === "assert_text_visible") {
2649
2837
  if (hiddenMatch) {
2650
- return { ...base, reason: "matching_element_not_visible", text: compactSetupResultText(matchedText), timeout_ms: timeout };
2838
+ return { ...base, ...setupScopeEvidence(scope), reason: "matching_element_not_visible", text: compactSetupResultText(matchedText), timeout_ms: timeout };
2651
2839
  }
2652
- return { ...base, reason: "text_not_found", text: compactSetupResultText(lastText), timeout_ms: timeout };
2840
+ return { ...base, ...setupScopeEvidence(scope), reason: "text_not_found", text: compactSetupResultText(lastText), timeout_ms: timeout };
2653
2841
  }
2654
- return { ...base, reason: "text_still_present", text: compactSetupResultText(matchedText || lastText), timeout_ms: timeout };
2842
+ return { ...base, ...setupScopeEvidence(scope), reason: "text_still_present", text: compactSetupResultText(matchedText || lastText), timeout_ms: timeout };
2655
2843
  }
2656
2844
  return { ...base, reason: "unsupported_action" };
2657
2845
  } catch (error) {
@@ -3254,7 +3442,7 @@ async function captureViewport(viewport) {
3254
3442
  if ((check.type === "text_visible" || check.type === "text_absent") && (check.text || check.pattern)) {
3255
3443
  text_matches[textKey(check)] = textMatches(dom.body_text || dom.body_text_sample || "", check);
3256
3444
  }
3257
- if ((check.type === "frame_text_visible" || check.type === "frame_no_horizontal_overflow") && check.selector) {
3445
+ if ((check.type === "frame_text_visible" || check.type === "frame_url_equals" || check.type === "frame_url_matches" || check.type === "frame_no_horizontal_overflow") && check.selector) {
3258
3446
  selectors[check.selector] = selectors[check.selector] || await selectorStats(check.selector);
3259
3447
  frames[check.selector] = frames[check.selector] || await frameEvidence(check.selector);
3260
3448
  }