@riddledc/riddle-proof 0.7.196 → 0.7.198

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
@@ -406,7 +406,7 @@ when body matching overrides sequence order.
406
406
  `target.setup_actions` is optional. Use it when the meaningful proof surface
407
407
  appears only after a picker, tab, login stub, storage seed, form fill,
408
408
  transport control, or other bounded interaction. Supported setup actions are
409
- `click`, `tap`, `drag`, `press`, `key_down`, `key_up`, `fill`, `set_input_value`, `set_range_value`,
409
+ `click`, `tap`, `tap_until`, `drag`, `press`, `key_down`, `key_up`, `fill`, `set_input_value`, `set_range_value`,
410
410
  `deterministic_runtime`, `canvas_signature`, `assert_text_visible`, `assert_text_absent`,
411
411
  `assert_selector_count`, `assert_window_value`, `assert_window_number`,
412
412
  `local_storage`, `session_storage`, `clear_storage`, `clear_console`,
@@ -436,6 +436,14 @@ It requires `selector`, defaults to a touch tap at the target center, and
436
436
  accepts `x` / `y` or `from_x` / `from_y` plus `coordinate_mode: "ratio"` for
437
437
  element-relative coordinates. Set `pointer_type` to `mouse`, `touch`, or `pen`
438
438
  when the proof must distinguish input modality.
439
+ Use `tap_until` for gameplay loops where the proof should tap a visible target
440
+ until a browser-state predicate is satisfied. It accepts the same selector,
441
+ coordinate, and pointer options as `tap`, plus `until_path`,
442
+ `until_expected_value`, `max_taps` / `max_calls` from 1 to 100, and optional
443
+ `interval_ms`. The action stops early when the predicate matches and records one
444
+ compact receipt with `tap_count`, final `until_value`, and input dispatch
445
+ details, so long canvas interaction loops do not need dozens of repeated setup
446
+ actions.
439
447
  Use `set_range_value` for HTML range inputs and React-controlled sliders. It
440
448
  accepts aliases such as `set-slider-value`, requires `selector` plus `value`,
441
449
  uses the native input value setter, dispatches bubbling `input` and `change`
@@ -531,7 +539,9 @@ actions stay visible. Click actions with `click_count` greater than `1` are
531
539
  included in clicked-target evidence and rolled up as `click_count_action_total`
532
540
  and `click_count_value_total`. Repeated selector runs such as long gameplay
533
541
  button loops are also grouped as compact `same-selector` click-sequence
534
- receipts with click totals and ordinals. Setup receipt sampling favors both
542
+ receipts with click totals and ordinals. `tap_until` actions are summarized as
543
+ one compact receipt with total taps and the final predicate value, which is the
544
+ preferred shape for long canvas gameplay loops. Setup receipt sampling favors both
535
545
  first and last per-viewport receipts before filling remaining space, so late
536
546
  lifecycle phases such as terminal or restart remain visible in compact
537
547
  summaries.
@@ -45,6 +45,7 @@ var RIDDLE_PROOF_PROFILE_CHECK_TYPES = [
45
45
  var RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES = [
46
46
  "click",
47
47
  "tap",
48
+ "tap_until",
48
49
  "drag",
49
50
  "press",
50
51
  "key_down",
@@ -615,6 +616,28 @@ function profileSetupTapReceipts(results) {
615
616
  reason: result.reason ?? result.error ?? null
616
617
  }));
617
618
  }
619
+ function profileSetupTapUntilReceipts(results) {
620
+ return results.filter((result) => profileSetupResultAction(result) === "tap_until").map((result) => ({
621
+ ordinal: result.ordinal ?? null,
622
+ ok: result.ok !== false,
623
+ selector: result.selector ?? null,
624
+ frame_selector: result.frame_selector ?? null,
625
+ pointer_type: result.pointer_type ?? null,
626
+ input_dispatch: result.input_dispatch ?? null,
627
+ coordinate_mode: result.coordinate_mode ?? null,
628
+ x: result.x ?? null,
629
+ y: result.y ?? null,
630
+ duration_ms: result.duration_ms ?? null,
631
+ until_path: result.until_path ?? null,
632
+ until_value: result.until_value ?? null,
633
+ until_expected_value: result.until_expected_value ?? null,
634
+ tap_count: result.tap_count ?? null,
635
+ max_taps: result.max_taps ?? result.max_calls ?? null,
636
+ interval_ms: result.interval_ms ?? null,
637
+ timeout_ms: result.timeout_ms ?? null,
638
+ reason: result.reason ?? result.error ?? null
639
+ }));
640
+ }
618
641
  function profileSetupKeyboardReceipts(results) {
619
642
  return results.filter((result) => ["press", "key_down", "key_up"].includes(profileSetupResultAction(result))).map((result) => ({
620
643
  action: profileSetupResultAction(result),
@@ -790,6 +813,9 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountByViewpo
790
813
  const sampledDragReceipts = sampleProfileSetupSummaryItems(dragReceipts, 8);
791
814
  const tapReceipts = profileSetupTapReceipts(results);
792
815
  const sampledTapReceipts = sampleProfileSetupSummaryItems(tapReceipts, 8);
816
+ const tapUntilReceipts = profileSetupTapUntilReceipts(results);
817
+ const tapUntilTapCounts = tapUntilReceipts.map((result) => typeof result.tap_count === "number" && Number.isFinite(result.tap_count) ? result.tap_count : void 0).filter((value) => value !== void 0);
818
+ const sampledTapUntilReceipts = sampleProfileSetupSummaryItems(tapUntilReceipts, 8);
793
819
  const keyboardReceipts = profileSetupKeyboardReceipts(results);
794
820
  const sampledKeyboardReceipts = sampleProfileSetupSummaryItems(keyboardReceipts, 8);
795
821
  const canvasSignatureReceipts = profileSetupCanvasSignatureReceipts(results);
@@ -801,6 +827,9 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountByViewpo
801
827
  selector: result.selector ?? null,
802
828
  frame_selector: result.frame_selector ?? null,
803
829
  text: compactProfileSetupSummaryText(result.text),
830
+ ...result.fallback_to_tap === true ? { fallback_to_tap: true } : {},
831
+ ...result.input_dispatch ? { input_dispatch: result.input_dispatch } : {},
832
+ ...result.click_error ? { click_error: compactProfileSetupSummaryText(result.click_error) } : {},
804
833
  ...clickCount ? { click_count: clickCount } : {}
805
834
  };
806
835
  });
@@ -859,6 +888,10 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountByViewpo
859
888
  tap_total: tapReceipts.length,
860
889
  tap_truncated: tapReceipts.length > sampledTapReceipts.length,
861
890
  tap: sampledTapReceipts,
891
+ tap_until_total: tapUntilReceipts.length,
892
+ tap_until_tap_total: tapUntilTapCounts.reduce((sum, value) => sum + value, 0),
893
+ tap_until_truncated: tapUntilReceipts.length > sampledTapUntilReceipts.length,
894
+ tap_until: sampledTapUntilReceipts,
862
895
  keyboard_total: keyboardReceipts.length,
863
896
  keyboard_truncated: keyboardReceipts.length > sampledKeyboardReceipts.length,
864
897
  keyboard: sampledKeyboardReceipts,
@@ -920,7 +953,7 @@ function isSupportedCheckType(value) {
920
953
  }
921
954
  function normalizeSetupActionType(value, index) {
922
955
  const normalizedInput = String(value || "").trim().replace(/-/g, "_");
923
- const normalized = normalizedInput === "clear_browser_storage" ? "clear_storage" : normalizedInput === "reset_console" || normalizedInput === "clear_browser_console" || normalizedInput === "reset_browser_console" ? "clear_console" : normalizedInput === "pointer_drag" || normalizedInput === "mouse_drag" || normalizedInput === "drag_to" ? "drag" : normalizedInput === "pointer_tap" || normalizedInput === "touch_tap" || normalizedInput === "canvas_tap" ? "tap" : normalizedInput === "keyboard_press" || normalizedInput === "key_press" ? "press" : normalizedInput === "keyboard_down" || normalizedInput === "key_down" || normalizedInput === "keydown" || normalizedInput === "press_down" ? "key_down" : normalizedInput === "keyboard_up" || normalizedInput === "key_up" || normalizedInput === "keyup" || normalizedInput === "press_up" || normalizedInput === "release_key" || normalizedInput === "key_release" ? "key_up" : normalizedInput === "set_slider_value" || normalizedInput === "slider_value" || normalizedInput === "set_slider" || normalizedInput === "set_range" || normalizedInput === "range_value" || normalizedInput === "range_input" || normalizedInput === "set_range_input" ? "set_range_value" : normalizedInput === "deterministic_runtime" || normalizedInput === "mock_runtime" || normalizedInput === "mock_random" || normalizedInput === "mock_random_queue" || normalizedInput === "seed_random_queue" || normalizedInput === "set_random_queue" || normalizedInput === "mock_clock" || normalizedInput === "set_mock_clock" || normalizedInput === "set_runtime_determinism" || normalizedInput === "runtime_determinism" ? "deterministic_runtime" : normalizedInput === "canvas_hash" || normalizedInput === "capture_canvas_hash" || normalizedInput === "capture_canvas_signature" || normalizedInput === "canvas_state_signature" ? "canvas_signature" : normalizedInput === "capture_screenshot" || normalizedInput === "save_screenshot" || normalizedInput === "setup_screenshot" ? "screenshot" : normalizedInput === "accept_dialog" || normalizedInput === "accept_dialogs" || normalizedInput === "confirm_dialog" || normalizedInput === "set_dialog_response" ? "dialog_response" : normalizedInput === "dismiss_dialog" || normalizedInput === "dismiss_dialogs" || normalizedInput === "cancel_dialog" ? "dialog_response" : normalizedInput === "window_call_until" || normalizedInput === "call_until" || normalizedInput === "window_call_repeat_until" || normalizedInput === "repeat_window_call_until" ? "window_call_until" : normalizedInput === "window_evaluate" || normalizedInput === "browser_eval" || normalizedInput === "browser_evaluate" || normalizedInput === "evaluate_script" || normalizedInput === "profile_script" ? "window_eval" : normalizedInput;
956
+ const normalized = normalizedInput === "clear_browser_storage" ? "clear_storage" : normalizedInput === "reset_console" || normalizedInput === "clear_browser_console" || normalizedInput === "reset_browser_console" ? "clear_console" : normalizedInput === "pointer_drag" || normalizedInput === "mouse_drag" || normalizedInput === "drag_to" ? "drag" : normalizedInput === "pointer_tap" || normalizedInput === "touch_tap" || normalizedInput === "canvas_tap" ? "tap" : normalizedInput === "tap_until" || normalizedInput === "pointer_tap_until" || normalizedInput === "touch_tap_until" || normalizedInput === "canvas_tap_until" || normalizedInput === "tap_repeat_until" || normalizedInput === "repeat_tap_until" ? "tap_until" : normalizedInput === "keyboard_press" || normalizedInput === "key_press" ? "press" : normalizedInput === "keyboard_down" || normalizedInput === "key_down" || normalizedInput === "keydown" || normalizedInput === "press_down" ? "key_down" : normalizedInput === "keyboard_up" || normalizedInput === "key_up" || normalizedInput === "keyup" || normalizedInput === "press_up" || normalizedInput === "release_key" || normalizedInput === "key_release" ? "key_up" : normalizedInput === "set_slider_value" || normalizedInput === "slider_value" || normalizedInput === "set_slider" || normalizedInput === "set_range" || normalizedInput === "range_value" || normalizedInput === "range_input" || normalizedInput === "set_range_input" ? "set_range_value" : normalizedInput === "deterministic_runtime" || normalizedInput === "mock_runtime" || normalizedInput === "mock_random" || normalizedInput === "mock_random_queue" || normalizedInput === "seed_random_queue" || normalizedInput === "set_random_queue" || normalizedInput === "mock_clock" || normalizedInput === "set_mock_clock" || normalizedInput === "set_runtime_determinism" || normalizedInput === "runtime_determinism" ? "deterministic_runtime" : normalizedInput === "canvas_hash" || normalizedInput === "capture_canvas_hash" || normalizedInput === "capture_canvas_signature" || normalizedInput === "canvas_state_signature" ? "canvas_signature" : normalizedInput === "capture_screenshot" || normalizedInput === "save_screenshot" || normalizedInput === "setup_screenshot" ? "screenshot" : normalizedInput === "accept_dialog" || normalizedInput === "accept_dialogs" || normalizedInput === "confirm_dialog" || normalizedInput === "set_dialog_response" ? "dialog_response" : normalizedInput === "dismiss_dialog" || normalizedInput === "dismiss_dialogs" || normalizedInput === "cancel_dialog" ? "dialog_response" : normalizedInput === "window_call_until" || normalizedInput === "call_until" || normalizedInput === "window_call_repeat_until" || normalizedInput === "repeat_window_call_until" ? "window_call_until" : normalizedInput === "window_evaluate" || normalizedInput === "browser_eval" || normalizedInput === "browser_evaluate" || normalizedInput === "evaluate_script" || normalizedInput === "profile_script" ? "window_eval" : normalizedInput;
924
957
  if (RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES.includes(normalized)) {
925
958
  return normalized;
926
959
  }
@@ -1002,8 +1035,8 @@ function normalizeSetupActionCoordinateMode(value, index) {
1002
1035
  }
1003
1036
  function normalizeSetupActionPointerType(value, type, index) {
1004
1037
  if (value === void 0 || value === null || value === "") return void 0;
1005
- if (type !== "drag" && type !== "tap") {
1006
- throw new Error(`target.setup_actions[${index}].pointer_type is only supported for drag/tap actions.`);
1038
+ if (type !== "drag" && type !== "tap" && type !== "tap_until") {
1039
+ throw new Error(`target.setup_actions[${index}].pointer_type is only supported for drag/tap/tap_until actions.`);
1007
1040
  }
1008
1041
  const normalized = String(value).trim().replace(/-/g, "_").toLowerCase();
1009
1042
  if (normalized === "mouse") return "mouse";
@@ -1082,11 +1115,11 @@ function normalizeSetupAction(input, index) {
1082
1115
  if (frameIndex !== void 0 && (!Number.isInteger(frameIndex) || frameIndex < 0)) {
1083
1116
  throw new Error(`target.setup_actions[${index}].frame_index must be a non-negative integer.`);
1084
1117
  }
1085
- if ((type === "click" || type === "tap" || type === "drag" || type === "fill" || type === "set_input_value" || type === "set_range_value" || type === "canvas_signature" || type === "wait_for_selector" || type === "wait_for_text" || type === "assert_text_visible" || type === "assert_text_absent" || type === "assert_selector_count") && !selector) {
1118
+ if ((type === "click" || type === "tap" || type === "tap_until" || type === "drag" || type === "fill" || type === "set_input_value" || type === "set_range_value" || type === "canvas_signature" || type === "wait_for_selector" || type === "wait_for_text" || type === "assert_text_visible" || type === "assert_text_absent" || type === "assert_selector_count") && !selector) {
1086
1119
  throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
1087
1120
  }
1088
- const fromX = type === "click" || type === "tap" ? numberValue(valueFromOwn(input, "from_x", "fromX", "x", "click_x", "clickX", "start_x", "startX", "x1")) : numberValue(valueFromOwn(input, "from_x", "fromX", "start_x", "startX", "x1"));
1089
- const fromY = type === "click" || type === "tap" ? numberValue(valueFromOwn(input, "from_y", "fromY", "y", "click_y", "clickY", "start_y", "startY", "y1")) : numberValue(valueFromOwn(input, "from_y", "fromY", "start_y", "startY", "y1"));
1121
+ const fromX = type === "click" || type === "tap" || type === "tap_until" ? numberValue(valueFromOwn(input, "from_x", "fromX", "x", "click_x", "clickX", "start_x", "startX", "x1")) : numberValue(valueFromOwn(input, "from_x", "fromX", "start_x", "startX", "x1"));
1122
+ const fromY = type === "click" || type === "tap" || type === "tap_until" ? numberValue(valueFromOwn(input, "from_y", "fromY", "y", "click_y", "clickY", "start_y", "startY", "y1")) : numberValue(valueFromOwn(input, "from_y", "fromY", "start_y", "startY", "y1"));
1090
1123
  const toX = numberValue(valueFromOwn(input, "to_x", "toX", "end_x", "endX", "x2"));
1091
1124
  const toY = numberValue(valueFromOwn(input, "to_y", "toY", "end_y", "endY", "y2"));
1092
1125
  const coordinateMode = normalizeSetupActionCoordinateMode(valueFromOwn(input, "coordinate_mode", "coordinateMode", "coords", "units"), index);
@@ -1111,18 +1144,18 @@ function normalizeSetupAction(input, index) {
1111
1144
  }
1112
1145
  }
1113
1146
  }
1114
- if (type === "tap") {
1147
+ if (type === "tap" || type === "tap_until") {
1115
1148
  const hasTapCoordinate = fromX !== void 0 || fromY !== void 0;
1116
1149
  if (hasTapCoordinate && (fromX === void 0 || fromY === void 0)) {
1117
- throw new Error(`target.setup_actions[${index}] tap coordinates require both x and y.`);
1150
+ throw new Error(`target.setup_actions[${index}] ${type} coordinates require both x and y.`);
1118
1151
  }
1119
1152
  if (hasTapCoordinate && fromX !== void 0 && fromY !== void 0) {
1120
1153
  const tapCoordinates = [fromX, fromY];
1121
1154
  if (coordinateMode === "ratio" && tapCoordinates.some((value2) => value2 < 0 || value2 > 1)) {
1122
- throw new Error(`target.setup_actions[${index}] tap ratio coordinates must be between 0 and 1.`);
1155
+ throw new Error(`target.setup_actions[${index}] ${type} ratio coordinates must be between 0 and 1.`);
1123
1156
  }
1124
1157
  if ((coordinateMode === void 0 || coordinateMode === "pixels") && tapCoordinates.some((value2) => value2 < 0)) {
1125
- throw new Error(`target.setup_actions[${index}] tap pixel coordinates must be non-negative.`);
1158
+ throw new Error(`target.setup_actions[${index}] ${type} pixel coordinates must be non-negative.`);
1126
1159
  }
1127
1160
  }
1128
1161
  }
@@ -1228,7 +1261,7 @@ function normalizeSetupAction(input, index) {
1228
1261
  const captureReturn = input.capture_return === false || input.captureReturn === false || input.include_return === false || input.includeReturn === false || input.omit_return === true || input.omitReturn === true ? false : void 0;
1229
1262
  const untilPath = stringFromOwn(input, "until_path", "untilPath", "until_state_path", "untilStatePath", "until_window_path", "untilWindowPath", "until");
1230
1263
  const hasUntilExpectedValue = hasOwn(input, "until_expected_value") || hasOwn(input, "untilExpectedValue") || hasOwn(input, "until_expected") || hasOwn(input, "untilExpected") || hasOwn(input, "until_value") || hasOwn(input, "untilValue") || hasOwn(input, "expected_value") || hasOwn(input, "expectedValue") || hasOwn(input, "expected");
1231
- if (type === "window_call_until") {
1264
+ if (type === "window_call_until" || type === "tap_until") {
1232
1265
  if (!untilPath) {
1233
1266
  throw new Error(`target.setup_actions[${index}] ${type} requires until_path.`);
1234
1267
  }
@@ -1236,12 +1269,12 @@ function normalizeSetupAction(input, index) {
1236
1269
  throw new Error(`target.setup_actions[${index}] ${type} requires until_expected_value.`);
1237
1270
  }
1238
1271
  }
1239
- const maxCalls = numberValue(valueFromOwn(input, "max_calls", "maxCalls", "max_attempts", "maxAttempts", "attempts"));
1240
- if (type === "window_call_until" && (maxCalls === void 0 || !Number.isInteger(maxCalls) || maxCalls < 1 || maxCalls > 100)) {
1272
+ const maxCalls = numberValue(valueFromOwn(input, "max_calls", "maxCalls", "max_attempts", "maxAttempts", "attempts", "max_taps", "maxTaps", "tap_limit", "tapLimit"));
1273
+ if ((type === "window_call_until" || type === "tap_until") && (maxCalls === void 0 || !Number.isInteger(maxCalls) || maxCalls < 1 || maxCalls > 100)) {
1241
1274
  throw new Error(`target.setup_actions[${index}].max_calls must be an integer from 1 to 100.`);
1242
1275
  }
1243
1276
  const intervalMs = numberValue(valueFromOwn(input, "interval_ms", "intervalMs", "poll_ms", "pollMs", "call_interval_ms", "callIntervalMs"));
1244
- if (type === "window_call_until" && intervalMs !== void 0 && (!Number.isInteger(intervalMs) || intervalMs < 0 || intervalMs > 5e3)) {
1277
+ if ((type === "window_call_until" || type === "tap_until") && intervalMs !== void 0 && (!Number.isInteger(intervalMs) || intervalMs < 0 || intervalMs > 5e3)) {
1245
1278
  throw new Error(`target.setup_actions[${index}].interval_ms must be an integer from 0 to 5000.`);
1246
1279
  }
1247
1280
  const steps = numberValue(input.steps);
@@ -4507,6 +4540,30 @@ function profileSetupTapReceipts(results) {
4507
4540
  reason: result.reason || result.error || null,
4508
4541
  }));
4509
4542
  }
4543
+ function profileSetupTapUntilReceipts(results) {
4544
+ return (results || [])
4545
+ .filter((result) => result && profileSetupResultAction(result) === "tap_until")
4546
+ .map((result) => ({
4547
+ ordinal: result.ordinal ?? null,
4548
+ ok: result.ok !== false,
4549
+ selector: result.selector ?? null,
4550
+ frame_selector: result.frame_selector ?? null,
4551
+ pointer_type: result.pointer_type ?? null,
4552
+ input_dispatch: result.input_dispatch ?? null,
4553
+ coordinate_mode: result.coordinate_mode ?? null,
4554
+ x: result.x ?? null,
4555
+ y: result.y ?? null,
4556
+ duration_ms: result.duration_ms ?? null,
4557
+ until_path: result.until_path ?? null,
4558
+ until_value: result.until_value ?? null,
4559
+ until_expected_value: result.until_expected_value ?? null,
4560
+ tap_count: result.tap_count ?? null,
4561
+ max_taps: result.max_taps ?? result.max_calls ?? null,
4562
+ interval_ms: result.interval_ms ?? null,
4563
+ timeout_ms: result.timeout_ms ?? null,
4564
+ reason: result.reason || result.error || null,
4565
+ }));
4566
+ }
4510
4567
  function profileSetupKeyboardReceipts(results) {
4511
4568
  return (results || [])
4512
4569
  .filter((result) => result && ["press", "key_down", "key_up"].includes(profileSetupResultAction(result)))
@@ -4709,6 +4766,11 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountsByViewp
4709
4766
  const sampledDragReceipts = sampleProfileSetupSummaryItems(dragReceipts, 8);
4710
4767
  const tapReceipts = profileSetupTapReceipts(results);
4711
4768
  const sampledTapReceipts = sampleProfileSetupSummaryItems(tapReceipts, 8);
4769
+ const tapUntilReceipts = profileSetupTapUntilReceipts(results);
4770
+ const tapUntilTapCounts = tapUntilReceipts
4771
+ .map((result) => typeof result.tap_count === "number" && Number.isFinite(result.tap_count) ? result.tap_count : undefined)
4772
+ .filter((value) => value !== undefined);
4773
+ const sampledTapUntilReceipts = sampleProfileSetupSummaryItems(tapUntilReceipts, 8);
4712
4774
  const keyboardReceipts = profileSetupKeyboardReceipts(results);
4713
4775
  const sampledKeyboardReceipts = sampleProfileSetupSummaryItems(keyboardReceipts, 8);
4714
4776
  const canvasSignatureReceipts = profileSetupCanvasSignatureReceipts(results);
@@ -4722,6 +4784,9 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountsByViewp
4722
4784
  selector: result.selector ?? null,
4723
4785
  frame_selector: result.frame_selector ?? null,
4724
4786
  text: compactProfileSetupSummaryText(result.text),
4787
+ ...(result.fallback_to_tap === true ? { fallback_to_tap: true } : {}),
4788
+ ...(result.input_dispatch ? { input_dispatch: result.input_dispatch } : {}),
4789
+ ...(result.click_error ? { click_error: compactProfileSetupSummaryText(result.click_error) } : {}),
4725
4790
  ...(clickCount ? { click_count: clickCount } : {}),
4726
4791
  };
4727
4792
  });
@@ -4788,6 +4853,10 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountsByViewp
4788
4853
  tap_total: tapReceipts.length,
4789
4854
  tap_truncated: tapReceipts.length > sampledTapReceipts.length,
4790
4855
  tap: sampledTapReceipts,
4856
+ tap_until_total: tapUntilReceipts.length,
4857
+ tap_until_tap_total: tapUntilTapCounts.reduce((sum, value) => sum + value, 0),
4858
+ tap_until_truncated: tapUntilReceipts.length > sampledTapUntilReceipts.length,
4859
+ tap_until: sampledTapUntilReceipts,
4791
4860
  keyboard_total: keyboardReceipts.length,
4792
4861
  keyboard_truncated: keyboardReceipts.length > sampledKeyboardReceipts.length,
4793
4862
  keyboard: sampledKeyboardReceipts,
@@ -5734,6 +5803,108 @@ async function waitForAnyVisibleSelector(context, selector, timeout) {
5734
5803
  }
5735
5804
  throw new Error("No visible match for selector " + selector + ": " + lastReason);
5736
5805
  }
5806
+ async function dispatchSetupTapPoint(point, pointerType, durationMs) {
5807
+ if (pointerType === "touch" || pointerType === "pen") {
5808
+ const client = await page.context().newCDPSession(page);
5809
+ try {
5810
+ if (pointerType === "touch") {
5811
+ const touchPoint = {
5812
+ x: point.x,
5813
+ y: point.y,
5814
+ radiusX: 1,
5815
+ radiusY: 1,
5816
+ force: 1,
5817
+ id: 11,
5818
+ };
5819
+ await client.send("Input.dispatchTouchEvent", {
5820
+ type: "touchStart",
5821
+ touchPoints: [touchPoint],
5822
+ });
5823
+ if (durationMs) await page.waitForTimeout(durationMs);
5824
+ await client.send("Input.dispatchTouchEvent", {
5825
+ type: "touchEnd",
5826
+ touchPoints: [],
5827
+ });
5828
+ } else {
5829
+ await client.send("Input.dispatchMouseEvent", {
5830
+ type: "mousePressed",
5831
+ x: point.x,
5832
+ y: point.y,
5833
+ button: "left",
5834
+ buttons: 1,
5835
+ clickCount: 1,
5836
+ pointerType: "pen",
5837
+ });
5838
+ if (durationMs) await page.waitForTimeout(durationMs);
5839
+ await client.send("Input.dispatchMouseEvent", {
5840
+ type: "mouseReleased",
5841
+ x: point.x,
5842
+ y: point.y,
5843
+ button: "left",
5844
+ buttons: 0,
5845
+ clickCount: 1,
5846
+ pointerType: "pen",
5847
+ });
5848
+ }
5849
+ } finally {
5850
+ await client.detach().catch(() => {});
5851
+ }
5852
+ } else {
5853
+ await page.mouse.click(point.x, point.y);
5854
+ }
5855
+ }
5856
+ async function resolveSetupTapTarget(action, base, scope, timeout) {
5857
+ const locator = scope.context.locator(action.selector);
5858
+ const count = await locator.count();
5859
+ if (!count) return { result: { ...base, ...setupScopeEvidence(scope), reason: "selector_not_found", count } };
5860
+ const targetIndex = Number.isInteger(action.index) ? action.index : 0;
5861
+ if (targetIndex < 0 || targetIndex >= count) return { result: { ...base, ...setupScopeEvidence(scope), reason: "index_out_of_range", count, target_index: targetIndex } };
5862
+ const target = locator.nth(targetIndex);
5863
+ await target.waitFor({ state: "visible", timeout });
5864
+ const box = await target.boundingBox();
5865
+ if (!box) return { result: { ...base, ...setupScopeEvidence(scope), reason: "bounding_box_unavailable", count, target_index: targetIndex } };
5866
+ const fromX = setupFiniteNumber(action.from_x ?? action.fromX ?? action.x ?? action.click_x ?? action.clickX);
5867
+ const fromY = setupFiniteNumber(action.from_y ?? action.fromY ?? action.y ?? action.click_y ?? action.clickY);
5868
+ const hasTapPosition = fromX !== undefined || fromY !== undefined;
5869
+ if (hasTapPosition && (fromX === undefined || fromY === undefined)) return { result: { ...base, ...setupScopeEvidence(scope), reason: "missing_tap_coordinates", count, target_index: targetIndex } };
5870
+ const mode = String(action.coordinate_mode || action.coordinateMode || (hasTapPosition ? "pixels" : "ratio")).trim();
5871
+ if (hasTapPosition && mode === "ratio" && [fromX, fromY].some((value) => value < 0 || value > 1)) return { result: { ...base, ...setupScopeEvidence(scope), reason: "invalid_ratio_coordinates", count, target_index: targetIndex } };
5872
+ if (hasTapPosition && mode !== "ratio" && [fromX, fromY].some((value) => value < 0)) return { result: { ...base, ...setupScopeEvidence(scope), reason: "invalid_pixel_coordinates", count, target_index: targetIndex } };
5873
+ const coordinate = (value, size) => mode === "ratio" ? value * size : value;
5874
+ const localX = hasTapPosition && fromX !== undefined ? fromX : 0.5;
5875
+ const localY = hasTapPosition && fromY !== undefined ? fromY : 0.5;
5876
+ const point = {
5877
+ x: box.x + coordinate(localX, box.width),
5878
+ y: box.y + coordinate(localY, box.height),
5879
+ };
5880
+ const durationMs = setupNumber(action.duration_ms ?? action.durationMs, 0);
5881
+ const pointerType = String(action.pointer_type || action.pointerType || "touch").trim().toLowerCase();
5882
+ return {
5883
+ target: {
5884
+ count,
5885
+ targetIndex,
5886
+ point,
5887
+ mode,
5888
+ fromX,
5889
+ fromY,
5890
+ hasTapPosition,
5891
+ pointerType,
5892
+ durationMs,
5893
+ },
5894
+ };
5895
+ }
5896
+ function setupTapTargetEvidence(tapTarget) {
5897
+ return {
5898
+ count: tapTarget.count,
5899
+ target_index: tapTarget.targetIndex,
5900
+ coordinate_mode: tapTarget.hasTapPosition ? tapTarget.mode : undefined,
5901
+ x: tapTarget.hasTapPosition ? tapTarget.fromX : undefined,
5902
+ y: tapTarget.hasTapPosition ? tapTarget.fromY : undefined,
5903
+ pointer_type: tapTarget.pointerType,
5904
+ input_dispatch: tapTarget.pointerType === "touch" || tapTarget.pointerType === "pen" ? "cdp" : "playwright_mouse",
5905
+ duration_ms: tapTarget.durationMs || undefined,
5906
+ };
5907
+ }
5737
5908
  function setupHasOwn(action, key) {
5738
5909
  return Boolean(action) && Object.keys(action).includes(key);
5739
5910
  }
@@ -6336,91 +6507,108 @@ async function executeSetupAction(action, ordinal, viewport) {
6336
6507
  if (type === "tap") {
6337
6508
  const scope = await setupActionScope(action, timeout);
6338
6509
  if (!scope.ok) return setupScopeFailure(base, scope);
6339
- const locator = scope.context.locator(action.selector);
6340
- const count = await locator.count();
6341
- if (!count) return { ...base, ...setupScopeEvidence(scope), reason: "selector_not_found", count };
6342
- const targetIndex = Number.isInteger(action.index) ? action.index : 0;
6343
- if (targetIndex < 0 || targetIndex >= count) return { ...base, ...setupScopeEvidence(scope), reason: "index_out_of_range", count, target_index: targetIndex };
6344
- const target = locator.nth(targetIndex);
6345
- await target.waitFor({ state: "visible", timeout });
6346
- const box = await target.boundingBox();
6347
- if (!box) return { ...base, ...setupScopeEvidence(scope), reason: "bounding_box_unavailable", count, target_index: targetIndex };
6348
- const fromX = setupFiniteNumber(action.from_x ?? action.fromX ?? action.x ?? action.click_x ?? action.clickX);
6349
- const fromY = setupFiniteNumber(action.from_y ?? action.fromY ?? action.y ?? action.click_y ?? action.clickY);
6350
- const hasTapPosition = fromX !== undefined || fromY !== undefined;
6351
- if (hasTapPosition && (fromX === undefined || fromY === undefined)) return { ...base, ...setupScopeEvidence(scope), reason: "missing_tap_coordinates", count, target_index: targetIndex };
6352
- const mode = String(action.coordinate_mode || action.coordinateMode || (hasTapPosition ? "pixels" : "ratio")).trim();
6353
- if (hasTapPosition && mode === "ratio" && [fromX, fromY].some((value) => value < 0 || value > 1)) return { ...base, ...setupScopeEvidence(scope), reason: "invalid_ratio_coordinates", count, target_index: targetIndex };
6354
- if (hasTapPosition && mode !== "ratio" && [fromX, fromY].some((value) => value < 0)) return { ...base, ...setupScopeEvidence(scope), reason: "invalid_pixel_coordinates", count, target_index: targetIndex };
6355
- const coordinate = (value, size) => mode === "ratio" ? value * size : value;
6356
- const localX = hasTapPosition && fromX !== undefined ? fromX : 0.5;
6357
- const localY = hasTapPosition && fromY !== undefined ? fromY : 0.5;
6358
- const point = {
6359
- x: box.x + coordinate(localX, box.width),
6360
- y: box.y + coordinate(localY, box.height),
6510
+ const prepared = await resolveSetupTapTarget(action, base, scope, timeout);
6511
+ if (prepared.result) return prepared.result;
6512
+ await dispatchSetupTapPoint(prepared.target.point, prepared.target.pointerType, prepared.target.durationMs);
6513
+ return {
6514
+ ...base,
6515
+ ...setupScopeEvidence(scope),
6516
+ ok: true,
6517
+ ...setupTapTargetEvidence(prepared.target),
6361
6518
  };
6362
- const durationMs = setupNumber(action.duration_ms ?? action.durationMs, 0);
6363
- const pointerType = String(action.pointer_type || action.pointerType || "touch").trim().toLowerCase();
6364
- if (pointerType === "touch" || pointerType === "pen") {
6365
- const client = await page.context().newCDPSession(page);
6366
- try {
6367
- if (pointerType === "touch") {
6368
- const touchPoint = {
6369
- x: point.x,
6370
- y: point.y,
6371
- radiusX: 1,
6372
- radiusY: 1,
6373
- force: 1,
6374
- id: 11,
6375
- };
6376
- await client.send("Input.dispatchTouchEvent", {
6377
- type: "touchStart",
6378
- touchPoints: [touchPoint],
6379
- });
6380
- if (durationMs) await page.waitForTimeout(durationMs);
6381
- await client.send("Input.dispatchTouchEvent", {
6382
- type: "touchEnd",
6383
- touchPoints: [],
6384
- });
6385
- } else {
6386
- await client.send("Input.dispatchMouseEvent", {
6387
- type: "mousePressed",
6388
- x: point.x,
6389
- y: point.y,
6390
- button: "left",
6391
- buttons: 1,
6392
- clickCount: 1,
6393
- pointerType: "pen",
6394
- });
6395
- if (durationMs) await page.waitForTimeout(durationMs);
6396
- await client.send("Input.dispatchMouseEvent", {
6397
- type: "mouseReleased",
6398
- x: point.x,
6399
- y: point.y,
6400
- button: "left",
6401
- buttons: 0,
6402
- clickCount: 1,
6403
- pointerType: "pen",
6404
- });
6405
- }
6406
- } finally {
6407
- await client.detach().catch(() => {});
6519
+ }
6520
+ if (type === "tap_until") {
6521
+ const untilPath = String(action.until_path || action.untilPath || action.until_state_path || action.untilStatePath || action.until_window_path || action.untilWindowPath || action.until || "");
6522
+ const hasUntilExpected = setupHasOwn(action, "until_expected_value")
6523
+ || setupHasOwn(action, "untilExpectedValue")
6524
+ || setupHasOwn(action, "until_expected")
6525
+ || setupHasOwn(action, "untilExpected")
6526
+ || setupHasOwn(action, "until_value")
6527
+ || setupHasOwn(action, "untilValue")
6528
+ || setupHasOwn(action, "expected_value")
6529
+ || setupHasOwn(action, "expectedValue")
6530
+ || setupHasOwn(action, "expected");
6531
+ const untilExpected = setupHasOwn(action, "until_expected_value")
6532
+ ? action.until_expected_value
6533
+ : setupHasOwn(action, "untilExpectedValue")
6534
+ ? action.untilExpectedValue
6535
+ : setupHasOwn(action, "until_expected")
6536
+ ? action.until_expected
6537
+ : setupHasOwn(action, "untilExpected")
6538
+ ? action.untilExpected
6539
+ : setupHasOwn(action, "until_value")
6540
+ ? action.until_value
6541
+ : setupHasOwn(action, "untilValue")
6542
+ ? action.untilValue
6543
+ : setupHasOwn(action, "expected_value")
6544
+ ? action.expected_value
6545
+ : setupHasOwn(action, "expectedValue")
6546
+ ? action.expectedValue
6547
+ : action.expected;
6548
+ if (!untilPath) return { ...base, reason: "missing_until_path" };
6549
+ if (!hasUntilExpected) return { ...base, until_path: untilPath, reason: "missing_until_expected_value" };
6550
+ const maxTaps = Math.min(100, Math.max(1, Math.floor(setupNumber(action.max_taps ?? action.maxTaps ?? action.tap_limit ?? action.tapLimit ?? action.max_calls ?? action.maxCalls ?? action.max_attempts ?? action.maxAttempts ?? action.attempts, 1) || 1)));
6551
+ const intervalMs = Math.min(5000, Math.max(0, Math.floor(setupNumber(action.interval_ms ?? action.intervalMs ?? action.poll_ms ?? action.pollMs ?? action.tap_interval_ms ?? action.tapIntervalMs, 100) || 0)));
6552
+ const scope = await setupActionScope(action, timeout);
6553
+ if (!scope.ok) return setupScopeFailure(base, scope);
6554
+ const prepared = await resolveSetupTapTarget(action, base, scope, timeout);
6555
+ if (prepared.result) return prepared.result;
6556
+ const startedAt = Date.now();
6557
+ let tapCount = 0;
6558
+ let lastPredicateResult = await setupReadWindowValue(scope.context, untilPath);
6559
+ const targetEvidence = setupTapTargetEvidence(prepared.target);
6560
+ if (lastPredicateResult.ok && setupValuesEqual(lastPredicateResult.value, untilExpected)) {
6561
+ return {
6562
+ ...base,
6563
+ ...setupScopeEvidence(scope),
6564
+ ok: true,
6565
+ ...targetEvidence,
6566
+ until_path: untilPath,
6567
+ until_value: setupJsonValue(lastPredicateResult.value),
6568
+ until_expected_value: setupJsonValue(untilExpected),
6569
+ tap_count: tapCount,
6570
+ max_taps: maxTaps,
6571
+ max_calls: maxTaps,
6572
+ interval_ms: intervalMs,
6573
+ timeout_ms: timeout,
6574
+ };
6575
+ }
6576
+ while (tapCount < maxTaps && Date.now() - startedAt <= timeout) {
6577
+ await dispatchSetupTapPoint(prepared.target.point, prepared.target.pointerType, prepared.target.durationMs);
6578
+ tapCount += 1;
6579
+ lastPredicateResult = await setupReadWindowValue(scope.context, untilPath);
6580
+ if (lastPredicateResult.ok && setupValuesEqual(lastPredicateResult.value, untilExpected)) {
6581
+ return {
6582
+ ...base,
6583
+ ...setupScopeEvidence(scope),
6584
+ ok: true,
6585
+ ...targetEvidence,
6586
+ until_path: untilPath,
6587
+ until_value: setupJsonValue(lastPredicateResult.value),
6588
+ until_expected_value: setupJsonValue(untilExpected),
6589
+ tap_count: tapCount,
6590
+ max_taps: maxTaps,
6591
+ max_calls: maxTaps,
6592
+ interval_ms: intervalMs,
6593
+ timeout_ms: timeout,
6594
+ };
6408
6595
  }
6409
- } else {
6410
- await page.mouse.click(point.x, point.y);
6596
+ if (tapCount < maxTaps && intervalMs) await page.waitForTimeout(intervalMs);
6411
6597
  }
6412
6598
  return {
6413
6599
  ...base,
6414
6600
  ...setupScopeEvidence(scope),
6415
- ok: true,
6416
- count,
6417
- target_index: targetIndex,
6418
- coordinate_mode: hasTapPosition ? mode : undefined,
6419
- x: hasTapPosition ? fromX : undefined,
6420
- y: hasTapPosition ? fromY : undefined,
6421
- pointer_type: pointerType,
6422
- input_dispatch: pointerType === "touch" || pointerType === "pen" ? "cdp" : "playwright_mouse",
6423
- duration_ms: durationMs || undefined,
6601
+ ...targetEvidence,
6602
+ until_path: untilPath,
6603
+ until_value: setupJsonValue(lastPredicateResult?.value),
6604
+ until_expected_value: setupJsonValue(untilExpected),
6605
+ tap_count: tapCount,
6606
+ max_taps: maxTaps,
6607
+ max_calls: maxTaps,
6608
+ interval_ms: intervalMs,
6609
+ timeout_ms: timeout,
6610
+ reason: Date.now() - startedAt > timeout ? "timeout" : "until_condition_not_met",
6611
+ missing_part: lastPredicateResult?.missing_part || undefined,
6424
6612
  };
6425
6613
  }
6426
6614
  if (type === "drag") {