@riddledc/riddle-proof 0.7.197 → 0.7.199

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,15 @@ 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`. Set `tap_burst_size` from 1 to 100 when gameplay needs several
444
+ fast taps before the next predicate check. The action stops early when the
445
+ predicate matches and records one compact receipt with `tap_count`,
446
+ `condition_check_count`, final `until_value`, and input dispatch details, so
447
+ long canvas interaction loops do not need dozens of repeated setup actions.
439
448
  Use `set_range_value` for HTML range inputs and React-controlled sliders. It
440
449
  accepts aliases such as `set-slider-value`, requires `selector` plus `value`,
441
450
  uses the native input value setter, dispatches bubbling `input` and `change`
@@ -531,10 +540,12 @@ actions stay visible. Click actions with `click_count` greater than `1` are
531
540
  included in clicked-target evidence and rolled up as `click_count_action_total`
532
541
  and `click_count_value_total`. Repeated selector runs such as long gameplay
533
542
  button loops are also grouped as compact `same-selector` click-sequence
534
- receipts with click totals and ordinals. Setup receipt sampling favors both
535
- first and last per-viewport receipts before filling remaining space, so late
536
- lifecycle phases such as terminal or restart remain visible in compact
537
- summaries.
543
+ receipts with click totals and ordinals. `tap_until` actions are summarized as
544
+ one compact receipt with total taps, optional burst size, predicate-check count,
545
+ and the final predicate value, which is the preferred shape for long canvas
546
+ gameplay loops. Setup receipt sampling favors both first and last per-viewport
547
+ receipts before filling remaining space, so late lifecycle phases such as
548
+ terminal or restart remain visible in compact summaries.
538
549
 
539
550
  `target.timeout_sec` is optional. Use it for known-heavy profile targets so the
540
551
  profile carries its own hosted Riddle worker budget; an explicit CLI `--timeout`
@@ -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,30 @@ 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
+ tap_burst_size: result.tap_burst_size ?? null,
637
+ condition_check_count: result.condition_check_count ?? null,
638
+ interval_ms: result.interval_ms ?? null,
639
+ timeout_ms: result.timeout_ms ?? null,
640
+ reason: result.reason ?? result.error ?? null
641
+ }));
642
+ }
618
643
  function profileSetupKeyboardReceipts(results) {
619
644
  return results.filter((result) => ["press", "key_down", "key_up"].includes(profileSetupResultAction(result))).map((result) => ({
620
645
  action: profileSetupResultAction(result),
@@ -790,6 +815,9 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountByViewpo
790
815
  const sampledDragReceipts = sampleProfileSetupSummaryItems(dragReceipts, 8);
791
816
  const tapReceipts = profileSetupTapReceipts(results);
792
817
  const sampledTapReceipts = sampleProfileSetupSummaryItems(tapReceipts, 8);
818
+ const tapUntilReceipts = profileSetupTapUntilReceipts(results);
819
+ 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);
820
+ const sampledTapUntilReceipts = sampleProfileSetupSummaryItems(tapUntilReceipts, 8);
793
821
  const keyboardReceipts = profileSetupKeyboardReceipts(results);
794
822
  const sampledKeyboardReceipts = sampleProfileSetupSummaryItems(keyboardReceipts, 8);
795
823
  const canvasSignatureReceipts = profileSetupCanvasSignatureReceipts(results);
@@ -862,6 +890,10 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountByViewpo
862
890
  tap_total: tapReceipts.length,
863
891
  tap_truncated: tapReceipts.length > sampledTapReceipts.length,
864
892
  tap: sampledTapReceipts,
893
+ tap_until_total: tapUntilReceipts.length,
894
+ tap_until_tap_total: tapUntilTapCounts.reduce((sum, value) => sum + value, 0),
895
+ tap_until_truncated: tapUntilReceipts.length > sampledTapUntilReceipts.length,
896
+ tap_until: sampledTapUntilReceipts,
865
897
  keyboard_total: keyboardReceipts.length,
866
898
  keyboard_truncated: keyboardReceipts.length > sampledKeyboardReceipts.length,
867
899
  keyboard: sampledKeyboardReceipts,
@@ -923,7 +955,7 @@ function isSupportedCheckType(value) {
923
955
  }
924
956
  function normalizeSetupActionType(value, index) {
925
957
  const normalizedInput = String(value || "").trim().replace(/-/g, "_");
926
- 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;
958
+ 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;
927
959
  if (RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES.includes(normalized)) {
928
960
  return normalized;
929
961
  }
@@ -1005,8 +1037,8 @@ function normalizeSetupActionCoordinateMode(value, index) {
1005
1037
  }
1006
1038
  function normalizeSetupActionPointerType(value, type, index) {
1007
1039
  if (value === void 0 || value === null || value === "") return void 0;
1008
- if (type !== "drag" && type !== "tap") {
1009
- throw new Error(`target.setup_actions[${index}].pointer_type is only supported for drag/tap actions.`);
1040
+ if (type !== "drag" && type !== "tap" && type !== "tap_until") {
1041
+ throw new Error(`target.setup_actions[${index}].pointer_type is only supported for drag/tap/tap_until actions.`);
1010
1042
  }
1011
1043
  const normalized = String(value).trim().replace(/-/g, "_").toLowerCase();
1012
1044
  if (normalized === "mouse") return "mouse";
@@ -1085,11 +1117,11 @@ function normalizeSetupAction(input, index) {
1085
1117
  if (frameIndex !== void 0 && (!Number.isInteger(frameIndex) || frameIndex < 0)) {
1086
1118
  throw new Error(`target.setup_actions[${index}].frame_index must be a non-negative integer.`);
1087
1119
  }
1088
- 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) {
1120
+ 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) {
1089
1121
  throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
1090
1122
  }
1091
- 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"));
1092
- 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"));
1123
+ 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"));
1124
+ 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"));
1093
1125
  const toX = numberValue(valueFromOwn(input, "to_x", "toX", "end_x", "endX", "x2"));
1094
1126
  const toY = numberValue(valueFromOwn(input, "to_y", "toY", "end_y", "endY", "y2"));
1095
1127
  const coordinateMode = normalizeSetupActionCoordinateMode(valueFromOwn(input, "coordinate_mode", "coordinateMode", "coords", "units"), index);
@@ -1114,18 +1146,18 @@ function normalizeSetupAction(input, index) {
1114
1146
  }
1115
1147
  }
1116
1148
  }
1117
- if (type === "tap") {
1149
+ if (type === "tap" || type === "tap_until") {
1118
1150
  const hasTapCoordinate = fromX !== void 0 || fromY !== void 0;
1119
1151
  if (hasTapCoordinate && (fromX === void 0 || fromY === void 0)) {
1120
- throw new Error(`target.setup_actions[${index}] tap coordinates require both x and y.`);
1152
+ throw new Error(`target.setup_actions[${index}] ${type} coordinates require both x and y.`);
1121
1153
  }
1122
1154
  if (hasTapCoordinate && fromX !== void 0 && fromY !== void 0) {
1123
1155
  const tapCoordinates = [fromX, fromY];
1124
1156
  if (coordinateMode === "ratio" && tapCoordinates.some((value2) => value2 < 0 || value2 > 1)) {
1125
- throw new Error(`target.setup_actions[${index}] tap ratio coordinates must be between 0 and 1.`);
1157
+ throw new Error(`target.setup_actions[${index}] ${type} ratio coordinates must be between 0 and 1.`);
1126
1158
  }
1127
1159
  if ((coordinateMode === void 0 || coordinateMode === "pixels") && tapCoordinates.some((value2) => value2 < 0)) {
1128
- throw new Error(`target.setup_actions[${index}] tap pixel coordinates must be non-negative.`);
1160
+ throw new Error(`target.setup_actions[${index}] ${type} pixel coordinates must be non-negative.`);
1129
1161
  }
1130
1162
  }
1131
1163
  }
@@ -1231,7 +1263,7 @@ function normalizeSetupAction(input, index) {
1231
1263
  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;
1232
1264
  const untilPath = stringFromOwn(input, "until_path", "untilPath", "until_state_path", "untilStatePath", "until_window_path", "untilWindowPath", "until");
1233
1265
  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");
1234
- if (type === "window_call_until") {
1266
+ if (type === "window_call_until" || type === "tap_until") {
1235
1267
  if (!untilPath) {
1236
1268
  throw new Error(`target.setup_actions[${index}] ${type} requires until_path.`);
1237
1269
  }
@@ -1239,12 +1271,16 @@ function normalizeSetupAction(input, index) {
1239
1271
  throw new Error(`target.setup_actions[${index}] ${type} requires until_expected_value.`);
1240
1272
  }
1241
1273
  }
1242
- const maxCalls = numberValue(valueFromOwn(input, "max_calls", "maxCalls", "max_attempts", "maxAttempts", "attempts"));
1243
- if (type === "window_call_until" && (maxCalls === void 0 || !Number.isInteger(maxCalls) || maxCalls < 1 || maxCalls > 100)) {
1274
+ const maxCalls = numberValue(valueFromOwn(input, "max_calls", "maxCalls", "max_attempts", "maxAttempts", "attempts", "max_taps", "maxTaps", "tap_limit", "tapLimit"));
1275
+ if ((type === "window_call_until" || type === "tap_until") && (maxCalls === void 0 || !Number.isInteger(maxCalls) || maxCalls < 1 || maxCalls > 100)) {
1244
1276
  throw new Error(`target.setup_actions[${index}].max_calls must be an integer from 1 to 100.`);
1245
1277
  }
1278
+ const tapBurstSize = type === "tap_until" ? numberValue(valueFromOwn(input, "tap_burst_size", "tapBurstSize", "burst_size", "burstSize", "check_every_taps", "checkEveryTaps", "predicate_interval_taps", "predicateIntervalTaps")) : void 0;
1279
+ if (type === "tap_until" && tapBurstSize !== void 0 && (!Number.isInteger(tapBurstSize) || tapBurstSize < 1 || tapBurstSize > 100)) {
1280
+ throw new Error(`target.setup_actions[${index}].tap_burst_size must be an integer from 1 to 100.`);
1281
+ }
1246
1282
  const intervalMs = numberValue(valueFromOwn(input, "interval_ms", "intervalMs", "poll_ms", "pollMs", "call_interval_ms", "callIntervalMs"));
1247
- if (type === "window_call_until" && intervalMs !== void 0 && (!Number.isInteger(intervalMs) || intervalMs < 0 || intervalMs > 5e3)) {
1283
+ if ((type === "window_call_until" || type === "tap_until") && intervalMs !== void 0 && (!Number.isInteger(intervalMs) || intervalMs < 0 || intervalMs > 5e3)) {
1248
1284
  throw new Error(`target.setup_actions[${index}].interval_ms must be an integer from 0 to 5000.`);
1249
1285
  }
1250
1286
  const steps = numberValue(input.steps);
@@ -1290,6 +1326,7 @@ function normalizeSetupAction(input, index) {
1290
1326
  until_path: untilPath,
1291
1327
  until_expected_value: hasUntilExpectedValue ? toJsonValue(valueFromOwn(input, "until_expected_value", "untilExpectedValue", "until_expected", "untilExpected", "until_value", "untilValue", "expected_value", "expectedValue", "expected")) : void 0,
1292
1328
  max_calls: maxCalls,
1329
+ tap_burst_size: tapBurstSize,
1293
1330
  interval_ms: intervalMs,
1294
1331
  expected_value: hasExpectedValue ? toJsonValue(rawExpectedValue) : void 0,
1295
1332
  min_value: minValue,
@@ -4510,6 +4547,32 @@ function profileSetupTapReceipts(results) {
4510
4547
  reason: result.reason || result.error || null,
4511
4548
  }));
4512
4549
  }
4550
+ function profileSetupTapUntilReceipts(results) {
4551
+ return (results || [])
4552
+ .filter((result) => result && profileSetupResultAction(result) === "tap_until")
4553
+ .map((result) => ({
4554
+ ordinal: result.ordinal ?? null,
4555
+ ok: result.ok !== false,
4556
+ selector: result.selector ?? null,
4557
+ frame_selector: result.frame_selector ?? null,
4558
+ pointer_type: result.pointer_type ?? null,
4559
+ input_dispatch: result.input_dispatch ?? null,
4560
+ coordinate_mode: result.coordinate_mode ?? null,
4561
+ x: result.x ?? null,
4562
+ y: result.y ?? null,
4563
+ duration_ms: result.duration_ms ?? null,
4564
+ until_path: result.until_path ?? null,
4565
+ until_value: result.until_value ?? null,
4566
+ until_expected_value: result.until_expected_value ?? null,
4567
+ tap_count: result.tap_count ?? null,
4568
+ max_taps: result.max_taps ?? result.max_calls ?? null,
4569
+ tap_burst_size: result.tap_burst_size ?? null,
4570
+ condition_check_count: result.condition_check_count ?? null,
4571
+ interval_ms: result.interval_ms ?? null,
4572
+ timeout_ms: result.timeout_ms ?? null,
4573
+ reason: result.reason || result.error || null,
4574
+ }));
4575
+ }
4513
4576
  function profileSetupKeyboardReceipts(results) {
4514
4577
  return (results || [])
4515
4578
  .filter((result) => result && ["press", "key_down", "key_up"].includes(profileSetupResultAction(result)))
@@ -4712,6 +4775,11 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountsByViewp
4712
4775
  const sampledDragReceipts = sampleProfileSetupSummaryItems(dragReceipts, 8);
4713
4776
  const tapReceipts = profileSetupTapReceipts(results);
4714
4777
  const sampledTapReceipts = sampleProfileSetupSummaryItems(tapReceipts, 8);
4778
+ const tapUntilReceipts = profileSetupTapUntilReceipts(results);
4779
+ const tapUntilTapCounts = tapUntilReceipts
4780
+ .map((result) => typeof result.tap_count === "number" && Number.isFinite(result.tap_count) ? result.tap_count : undefined)
4781
+ .filter((value) => value !== undefined);
4782
+ const sampledTapUntilReceipts = sampleProfileSetupSummaryItems(tapUntilReceipts, 8);
4715
4783
  const keyboardReceipts = profileSetupKeyboardReceipts(results);
4716
4784
  const sampledKeyboardReceipts = sampleProfileSetupSummaryItems(keyboardReceipts, 8);
4717
4785
  const canvasSignatureReceipts = profileSetupCanvasSignatureReceipts(results);
@@ -4794,6 +4862,10 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountsByViewp
4794
4862
  tap_total: tapReceipts.length,
4795
4863
  tap_truncated: tapReceipts.length > sampledTapReceipts.length,
4796
4864
  tap: sampledTapReceipts,
4865
+ tap_until_total: tapUntilReceipts.length,
4866
+ tap_until_tap_total: tapUntilTapCounts.reduce((sum, value) => sum + value, 0),
4867
+ tap_until_truncated: tapUntilReceipts.length > sampledTapUntilReceipts.length,
4868
+ tap_until: sampledTapUntilReceipts,
4797
4869
  keyboard_total: keyboardReceipts.length,
4798
4870
  keyboard_truncated: keyboardReceipts.length > sampledKeyboardReceipts.length,
4799
4871
  keyboard: sampledKeyboardReceipts,
@@ -5740,6 +5812,108 @@ async function waitForAnyVisibleSelector(context, selector, timeout) {
5740
5812
  }
5741
5813
  throw new Error("No visible match for selector " + selector + ": " + lastReason);
5742
5814
  }
5815
+ async function dispatchSetupTapPoint(point, pointerType, durationMs) {
5816
+ if (pointerType === "touch" || pointerType === "pen") {
5817
+ const client = await page.context().newCDPSession(page);
5818
+ try {
5819
+ if (pointerType === "touch") {
5820
+ const touchPoint = {
5821
+ x: point.x,
5822
+ y: point.y,
5823
+ radiusX: 1,
5824
+ radiusY: 1,
5825
+ force: 1,
5826
+ id: 11,
5827
+ };
5828
+ await client.send("Input.dispatchTouchEvent", {
5829
+ type: "touchStart",
5830
+ touchPoints: [touchPoint],
5831
+ });
5832
+ if (durationMs) await page.waitForTimeout(durationMs);
5833
+ await client.send("Input.dispatchTouchEvent", {
5834
+ type: "touchEnd",
5835
+ touchPoints: [],
5836
+ });
5837
+ } else {
5838
+ await client.send("Input.dispatchMouseEvent", {
5839
+ type: "mousePressed",
5840
+ x: point.x,
5841
+ y: point.y,
5842
+ button: "left",
5843
+ buttons: 1,
5844
+ clickCount: 1,
5845
+ pointerType: "pen",
5846
+ });
5847
+ if (durationMs) await page.waitForTimeout(durationMs);
5848
+ await client.send("Input.dispatchMouseEvent", {
5849
+ type: "mouseReleased",
5850
+ x: point.x,
5851
+ y: point.y,
5852
+ button: "left",
5853
+ buttons: 0,
5854
+ clickCount: 1,
5855
+ pointerType: "pen",
5856
+ });
5857
+ }
5858
+ } finally {
5859
+ await client.detach().catch(() => {});
5860
+ }
5861
+ } else {
5862
+ await page.mouse.click(point.x, point.y);
5863
+ }
5864
+ }
5865
+ async function resolveSetupTapTarget(action, base, scope, timeout) {
5866
+ const locator = scope.context.locator(action.selector);
5867
+ const count = await locator.count();
5868
+ if (!count) return { result: { ...base, ...setupScopeEvidence(scope), reason: "selector_not_found", count } };
5869
+ const targetIndex = Number.isInteger(action.index) ? action.index : 0;
5870
+ if (targetIndex < 0 || targetIndex >= count) return { result: { ...base, ...setupScopeEvidence(scope), reason: "index_out_of_range", count, target_index: targetIndex } };
5871
+ const target = locator.nth(targetIndex);
5872
+ await target.waitFor({ state: "visible", timeout });
5873
+ const box = await target.boundingBox();
5874
+ if (!box) return { result: { ...base, ...setupScopeEvidence(scope), reason: "bounding_box_unavailable", count, target_index: targetIndex } };
5875
+ const fromX = setupFiniteNumber(action.from_x ?? action.fromX ?? action.x ?? action.click_x ?? action.clickX);
5876
+ const fromY = setupFiniteNumber(action.from_y ?? action.fromY ?? action.y ?? action.click_y ?? action.clickY);
5877
+ const hasTapPosition = fromX !== undefined || fromY !== undefined;
5878
+ if (hasTapPosition && (fromX === undefined || fromY === undefined)) return { result: { ...base, ...setupScopeEvidence(scope), reason: "missing_tap_coordinates", count, target_index: targetIndex } };
5879
+ const mode = String(action.coordinate_mode || action.coordinateMode || (hasTapPosition ? "pixels" : "ratio")).trim();
5880
+ 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 } };
5881
+ if (hasTapPosition && mode !== "ratio" && [fromX, fromY].some((value) => value < 0)) return { result: { ...base, ...setupScopeEvidence(scope), reason: "invalid_pixel_coordinates", count, target_index: targetIndex } };
5882
+ const coordinate = (value, size) => mode === "ratio" ? value * size : value;
5883
+ const localX = hasTapPosition && fromX !== undefined ? fromX : 0.5;
5884
+ const localY = hasTapPosition && fromY !== undefined ? fromY : 0.5;
5885
+ const point = {
5886
+ x: box.x + coordinate(localX, box.width),
5887
+ y: box.y + coordinate(localY, box.height),
5888
+ };
5889
+ const durationMs = setupNumber(action.duration_ms ?? action.durationMs, 0);
5890
+ const pointerType = String(action.pointer_type || action.pointerType || "touch").trim().toLowerCase();
5891
+ return {
5892
+ target: {
5893
+ count,
5894
+ targetIndex,
5895
+ point,
5896
+ mode,
5897
+ fromX,
5898
+ fromY,
5899
+ hasTapPosition,
5900
+ pointerType,
5901
+ durationMs,
5902
+ },
5903
+ };
5904
+ }
5905
+ function setupTapTargetEvidence(tapTarget) {
5906
+ return {
5907
+ count: tapTarget.count,
5908
+ target_index: tapTarget.targetIndex,
5909
+ coordinate_mode: tapTarget.hasTapPosition ? tapTarget.mode : undefined,
5910
+ x: tapTarget.hasTapPosition ? tapTarget.fromX : undefined,
5911
+ y: tapTarget.hasTapPosition ? tapTarget.fromY : undefined,
5912
+ pointer_type: tapTarget.pointerType,
5913
+ input_dispatch: tapTarget.pointerType === "touch" || tapTarget.pointerType === "pen" ? "cdp" : "playwright_mouse",
5914
+ duration_ms: tapTarget.durationMs || undefined,
5915
+ };
5916
+ }
5743
5917
  function setupHasOwn(action, key) {
5744
5918
  return Boolean(action) && Object.keys(action).includes(key);
5745
5919
  }
@@ -6342,91 +6516,121 @@ async function executeSetupAction(action, ordinal, viewport) {
6342
6516
  if (type === "tap") {
6343
6517
  const scope = await setupActionScope(action, timeout);
6344
6518
  if (!scope.ok) return setupScopeFailure(base, scope);
6345
- const locator = scope.context.locator(action.selector);
6346
- const count = await locator.count();
6347
- if (!count) return { ...base, ...setupScopeEvidence(scope), reason: "selector_not_found", count };
6348
- const targetIndex = Number.isInteger(action.index) ? action.index : 0;
6349
- if (targetIndex < 0 || targetIndex >= count) return { ...base, ...setupScopeEvidence(scope), reason: "index_out_of_range", count, target_index: targetIndex };
6350
- const target = locator.nth(targetIndex);
6351
- await target.waitFor({ state: "visible", timeout });
6352
- const box = await target.boundingBox();
6353
- if (!box) return { ...base, ...setupScopeEvidence(scope), reason: "bounding_box_unavailable", count, target_index: targetIndex };
6354
- const fromX = setupFiniteNumber(action.from_x ?? action.fromX ?? action.x ?? action.click_x ?? action.clickX);
6355
- const fromY = setupFiniteNumber(action.from_y ?? action.fromY ?? action.y ?? action.click_y ?? action.clickY);
6356
- const hasTapPosition = fromX !== undefined || fromY !== undefined;
6357
- if (hasTapPosition && (fromX === undefined || fromY === undefined)) return { ...base, ...setupScopeEvidence(scope), reason: "missing_tap_coordinates", count, target_index: targetIndex };
6358
- const mode = String(action.coordinate_mode || action.coordinateMode || (hasTapPosition ? "pixels" : "ratio")).trim();
6359
- if (hasTapPosition && mode === "ratio" && [fromX, fromY].some((value) => value < 0 || value > 1)) return { ...base, ...setupScopeEvidence(scope), reason: "invalid_ratio_coordinates", count, target_index: targetIndex };
6360
- if (hasTapPosition && mode !== "ratio" && [fromX, fromY].some((value) => value < 0)) return { ...base, ...setupScopeEvidence(scope), reason: "invalid_pixel_coordinates", count, target_index: targetIndex };
6361
- const coordinate = (value, size) => mode === "ratio" ? value * size : value;
6362
- const localX = hasTapPosition && fromX !== undefined ? fromX : 0.5;
6363
- const localY = hasTapPosition && fromY !== undefined ? fromY : 0.5;
6364
- const point = {
6365
- x: box.x + coordinate(localX, box.width),
6366
- y: box.y + coordinate(localY, box.height),
6519
+ const prepared = await resolveSetupTapTarget(action, base, scope, timeout);
6520
+ if (prepared.result) return prepared.result;
6521
+ await dispatchSetupTapPoint(prepared.target.point, prepared.target.pointerType, prepared.target.durationMs);
6522
+ return {
6523
+ ...base,
6524
+ ...setupScopeEvidence(scope),
6525
+ ok: true,
6526
+ ...setupTapTargetEvidence(prepared.target),
6367
6527
  };
6368
- const durationMs = setupNumber(action.duration_ms ?? action.durationMs, 0);
6369
- const pointerType = String(action.pointer_type || action.pointerType || "touch").trim().toLowerCase();
6370
- if (pointerType === "touch" || pointerType === "pen") {
6371
- const client = await page.context().newCDPSession(page);
6372
- try {
6373
- if (pointerType === "touch") {
6374
- const touchPoint = {
6375
- x: point.x,
6376
- y: point.y,
6377
- radiusX: 1,
6378
- radiusY: 1,
6379
- force: 1,
6380
- id: 11,
6381
- };
6382
- await client.send("Input.dispatchTouchEvent", {
6383
- type: "touchStart",
6384
- touchPoints: [touchPoint],
6385
- });
6386
- if (durationMs) await page.waitForTimeout(durationMs);
6387
- await client.send("Input.dispatchTouchEvent", {
6388
- type: "touchEnd",
6389
- touchPoints: [],
6390
- });
6391
- } else {
6392
- await client.send("Input.dispatchMouseEvent", {
6393
- type: "mousePressed",
6394
- x: point.x,
6395
- y: point.y,
6396
- button: "left",
6397
- buttons: 1,
6398
- clickCount: 1,
6399
- pointerType: "pen",
6400
- });
6401
- if (durationMs) await page.waitForTimeout(durationMs);
6402
- await client.send("Input.dispatchMouseEvent", {
6403
- type: "mouseReleased",
6404
- x: point.x,
6405
- y: point.y,
6406
- button: "left",
6407
- buttons: 0,
6408
- clickCount: 1,
6409
- pointerType: "pen",
6410
- });
6411
- }
6412
- } finally {
6413
- await client.detach().catch(() => {});
6528
+ }
6529
+ if (type === "tap_until") {
6530
+ const untilPath = String(action.until_path || action.untilPath || action.until_state_path || action.untilStatePath || action.until_window_path || action.untilWindowPath || action.until || "");
6531
+ const hasUntilExpected = setupHasOwn(action, "until_expected_value")
6532
+ || setupHasOwn(action, "untilExpectedValue")
6533
+ || setupHasOwn(action, "until_expected")
6534
+ || setupHasOwn(action, "untilExpected")
6535
+ || setupHasOwn(action, "until_value")
6536
+ || setupHasOwn(action, "untilValue")
6537
+ || setupHasOwn(action, "expected_value")
6538
+ || setupHasOwn(action, "expectedValue")
6539
+ || setupHasOwn(action, "expected");
6540
+ const untilExpected = setupHasOwn(action, "until_expected_value")
6541
+ ? action.until_expected_value
6542
+ : setupHasOwn(action, "untilExpectedValue")
6543
+ ? action.untilExpectedValue
6544
+ : setupHasOwn(action, "until_expected")
6545
+ ? action.until_expected
6546
+ : setupHasOwn(action, "untilExpected")
6547
+ ? action.untilExpected
6548
+ : setupHasOwn(action, "until_value")
6549
+ ? action.until_value
6550
+ : setupHasOwn(action, "untilValue")
6551
+ ? action.untilValue
6552
+ : setupHasOwn(action, "expected_value")
6553
+ ? action.expected_value
6554
+ : setupHasOwn(action, "expectedValue")
6555
+ ? action.expectedValue
6556
+ : action.expected;
6557
+ if (!untilPath) return { ...base, reason: "missing_until_path" };
6558
+ if (!hasUntilExpected) return { ...base, until_path: untilPath, reason: "missing_until_expected_value" };
6559
+ 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)));
6560
+ const tapBurstSize = Math.min(maxTaps, Math.min(100, Math.max(1, Math.floor(setupNumber(action.tap_burst_size ?? action.tapBurstSize ?? action.burst_size ?? action.burstSize ?? action.check_every_taps ?? action.checkEveryTaps ?? action.predicate_interval_taps ?? action.predicateIntervalTaps, 1) || 1))));
6561
+ 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)));
6562
+ const scope = await setupActionScope(action, timeout);
6563
+ if (!scope.ok) return setupScopeFailure(base, scope);
6564
+ const prepared = await resolveSetupTapTarget(action, base, scope, timeout);
6565
+ if (prepared.result) return prepared.result;
6566
+ const startedAt = Date.now();
6567
+ let tapCount = 0;
6568
+ let conditionCheckCount = 1;
6569
+ let lastPredicateResult = await setupReadWindowValue(scope.context, untilPath);
6570
+ const targetEvidence = setupTapTargetEvidence(prepared.target);
6571
+ if (lastPredicateResult.ok && setupValuesEqual(lastPredicateResult.value, untilExpected)) {
6572
+ return {
6573
+ ...base,
6574
+ ...setupScopeEvidence(scope),
6575
+ ok: true,
6576
+ ...targetEvidence,
6577
+ until_path: untilPath,
6578
+ until_value: setupJsonValue(lastPredicateResult.value),
6579
+ until_expected_value: setupJsonValue(untilExpected),
6580
+ tap_count: tapCount,
6581
+ max_taps: maxTaps,
6582
+ max_calls: maxTaps,
6583
+ tap_burst_size: tapBurstSize,
6584
+ condition_check_count: conditionCheckCount,
6585
+ interval_ms: intervalMs,
6586
+ timeout_ms: timeout,
6587
+ };
6588
+ }
6589
+ while (tapCount < maxTaps && Date.now() - startedAt <= timeout) {
6590
+ const burstCount = Math.min(tapBurstSize, maxTaps - tapCount);
6591
+ for (let burstIndex = 0; burstIndex < burstCount && Date.now() - startedAt <= timeout; burstIndex += 1) {
6592
+ await dispatchSetupTapPoint(prepared.target.point, prepared.target.pointerType, prepared.target.durationMs);
6593
+ tapCount += 1;
6594
+ if (tapCount < maxTaps && burstIndex < burstCount - 1 && intervalMs) await page.waitForTimeout(intervalMs);
6414
6595
  }
6415
- } else {
6416
- await page.mouse.click(point.x, point.y);
6596
+ lastPredicateResult = await setupReadWindowValue(scope.context, untilPath);
6597
+ conditionCheckCount += 1;
6598
+ if (lastPredicateResult.ok && setupValuesEqual(lastPredicateResult.value, untilExpected)) {
6599
+ return {
6600
+ ...base,
6601
+ ...setupScopeEvidence(scope),
6602
+ ok: true,
6603
+ ...targetEvidence,
6604
+ until_path: untilPath,
6605
+ until_value: setupJsonValue(lastPredicateResult.value),
6606
+ until_expected_value: setupJsonValue(untilExpected),
6607
+ tap_count: tapCount,
6608
+ max_taps: maxTaps,
6609
+ max_calls: maxTaps,
6610
+ tap_burst_size: tapBurstSize,
6611
+ condition_check_count: conditionCheckCount,
6612
+ interval_ms: intervalMs,
6613
+ timeout_ms: timeout,
6614
+ };
6615
+ }
6616
+ if (tapCount < maxTaps && intervalMs) await page.waitForTimeout(intervalMs);
6417
6617
  }
6418
6618
  return {
6419
6619
  ...base,
6420
6620
  ...setupScopeEvidence(scope),
6421
- ok: true,
6422
- count,
6423
- target_index: targetIndex,
6424
- coordinate_mode: hasTapPosition ? mode : undefined,
6425
- x: hasTapPosition ? fromX : undefined,
6426
- y: hasTapPosition ? fromY : undefined,
6427
- pointer_type: pointerType,
6428
- input_dispatch: pointerType === "touch" || pointerType === "pen" ? "cdp" : "playwright_mouse",
6429
- duration_ms: durationMs || undefined,
6621
+ ...targetEvidence,
6622
+ until_path: untilPath,
6623
+ until_value: setupJsonValue(lastPredicateResult?.value),
6624
+ until_expected_value: setupJsonValue(untilExpected),
6625
+ tap_count: tapCount,
6626
+ max_taps: maxTaps,
6627
+ max_calls: maxTaps,
6628
+ tap_burst_size: tapBurstSize,
6629
+ condition_check_count: conditionCheckCount,
6630
+ interval_ms: intervalMs,
6631
+ timeout_ms: timeout,
6632
+ reason: Date.now() - startedAt > timeout ? "timeout" : "until_condition_not_met",
6633
+ missing_part: lastPredicateResult?.missing_part || undefined,
6430
6634
  };
6431
6635
  }
6432
6636
  if (type === "drag") {