@riddledc/riddle-proof 0.7.159 → 0.7.161

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
@@ -398,10 +398,11 @@ when body matching overrides sequence order.
398
398
  appears only after a picker, tab, login stub, storage seed, form fill,
399
399
  transport control, or other bounded interaction. Supported setup actions are
400
400
  `click`, `drag`, `press`, `fill`, `set_input_value`, `set_range_value`,
401
- `assert_text_visible`, `assert_text_absent`, `assert_selector_count`, `assert_window_value`,
402
- `assert_window_number`, `local_storage`, `session_storage`, `clear_storage`,
403
- `clear_console`, `screenshot`, `wait`, `wait_for_selector`, `wait_for_text`,
404
- `window_eval`, `window_call`, and `window_call_until`;
401
+ `canvas_signature`, `assert_text_visible`, `assert_text_absent`,
402
+ `assert_selector_count`, `assert_window_value`, `assert_window_number`,
403
+ `local_storage`, `session_storage`, `clear_storage`, `clear_console`,
404
+ `screenshot`, `wait`, `wait_for_selector`, `wait_for_text`, `window_eval`,
405
+ `window_call`, and `window_call_until`;
405
406
  a failed setup action is recorded as a failed `setup_actions_succeeded` check so
406
407
  the profile cannot pass without reaching the intended state. Text-matched `click` actions prefer
407
408
  visible matching elements, which keeps responsive layouts from selecting hidden
@@ -422,6 +423,13 @@ events, and records the requested value plus the browser's actual normalized
422
423
  value, numeric value, `min`, `max`, and `step`. The action is intentionally
423
424
  strict: if the target is not an `input[type="range"]`, setup fails with
424
425
  `not_range_input` instead of silently treating the control like a text field.
426
+ Use `canvas_signature` for canvas-only proof surfaces. It requires `selector`,
427
+ reads the selected canvas with `toDataURL("image/png")`, records a sampled hash,
428
+ canvas dimensions, CSS dimensions, and data length, and can store the result
429
+ with `store_return_to` or `store_signature_to`. Add `compare_to` plus
430
+ `expect_changed: true` to assert that the current canvas signature differs from
431
+ a previously stored signature, for example menu -> active play or terminal ->
432
+ restart.
425
433
  Use `drag` for pointer-driven controls such as canvas launch areas, sliders, or
426
434
  drag-to-aim games. Provide `selector`, `from_x`, `from_y`, `to_x`, and `to_y`;
427
435
  coordinates are element-relative pixels by default. Set `coordinate_mode:
@@ -489,7 +497,9 @@ sequences include `clicked_total` and `clicked_truncated`; the compact `clicked`
489
497
  list keeps the first and last clicked targets so later route switches and reset
490
498
  actions stay visible. Click actions with `click_count` greater than `1` are
491
499
  included in clicked-target evidence and rolled up as `click_count_action_total`
492
- and `click_count_value_total`.
500
+ and `click_count_value_total`. Setup receipt sampling favors both first and last
501
+ per-viewport receipts before filling remaining space, so late lifecycle phases
502
+ such as terminal or restart remain visible in compact summaries.
493
503
 
494
504
  `target.timeout_sec` is optional. Use it for known-heavy profile targets so the
495
505
  profile carries its own hosted Riddle worker budget; an explicit CLI `--timeout`
@@ -49,6 +49,7 @@ var RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES = [
49
49
  "fill",
50
50
  "set_input_value",
51
51
  "set_range_value",
52
+ "canvas_signature",
52
53
  "assert_text_visible",
53
54
  "assert_text_absent",
54
55
  "assert_selector_count",
@@ -550,6 +551,26 @@ function profileSetupRangeValueReceipts(results) {
550
551
  reason: result.reason ?? result.error ?? null
551
552
  }));
552
553
  }
554
+ function profileSetupCanvasSignatureReceipts(results) {
555
+ return results.filter((result) => profileSetupResultAction(result) === "canvas_signature").map((result) => ({
556
+ ordinal: result.ordinal ?? null,
557
+ ok: result.ok !== false,
558
+ selector: result.selector ?? null,
559
+ frame_selector: result.frame_selector ?? null,
560
+ label: result.label ?? null,
561
+ hash: result.hash ?? null,
562
+ data_length: result.data_length ?? null,
563
+ width: result.width ?? null,
564
+ height: result.height ?? null,
565
+ css_width: result.css_width ?? null,
566
+ css_height: result.css_height ?? null,
567
+ compare_to: result.compare_to ?? null,
568
+ previous_hash: result.previous_hash ?? null,
569
+ changed: result.changed ?? null,
570
+ return_stored_to: result.return_stored_to ?? null,
571
+ reason: result.reason ?? result.error ?? null
572
+ }));
573
+ }
553
574
  function sampleProfileSetupSummaryItems(items, limit) {
554
575
  if (items.length <= limit) return items;
555
576
  const firstCount = Math.floor(limit / 2);
@@ -594,6 +615,8 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountByViewpo
594
615
  const sampledWindowEvalReceipts = sampleProfileSetupSummaryItems(windowEvalReceipts, 8);
595
616
  const rangeValueReceipts = profileSetupRangeValueReceipts(results);
596
617
  const sampledRangeValueReceipts = sampleProfileSetupSummaryItems(rangeValueReceipts, 8);
618
+ const canvasSignatureReceipts = profileSetupCanvasSignatureReceipts(results);
619
+ const sampledCanvasSignatureReceipts = sampleProfileSetupSummaryItems(canvasSignatureReceipts, 8);
597
620
  const clickedItems = results.filter((result) => profileSetupResultAction(result) === "click" && result.ok !== false).map((result) => {
598
621
  const clickCount = typeof result.click_count === "number" && Number.isFinite(result.click_count) && result.click_count > 1 ? result.click_count : void 0;
599
622
  return {
@@ -645,6 +668,9 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountByViewpo
645
668
  set_range_value_total: rangeValueReceipts.length,
646
669
  set_range_value_truncated: rangeValueReceipts.length > sampledRangeValueReceipts.length,
647
670
  set_range_value: sampledRangeValueReceipts,
671
+ canvas_signature_total: canvasSignatureReceipts.length,
672
+ canvas_signature_truncated: canvasSignatureReceipts.length > sampledCanvasSignatureReceipts.length,
673
+ canvas_signature: sampledCanvasSignatureReceipts,
648
674
  clicked,
649
675
  text_samples,
650
676
  failed: failed.map((result) => ({
@@ -697,7 +723,7 @@ function isSupportedCheckType(value) {
697
723
  }
698
724
  function normalizeSetupActionType(value, index) {
699
725
  const normalizedInput = String(value || "").trim().replace(/-/g, "_");
700
- 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 === "keyboard_press" || normalizedInput === "key_press" ? "press" : 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 === "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;
726
+ 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 === "keyboard_press" || normalizedInput === "key_press" ? "press" : 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 === "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;
701
727
  if (RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES.includes(normalized)) {
702
728
  return normalized;
703
729
  }
@@ -826,7 +852,7 @@ function normalizeSetupAction(input, index) {
826
852
  if (frameIndex !== void 0 && (!Number.isInteger(frameIndex) || frameIndex < 0)) {
827
853
  throw new Error(`target.setup_actions[${index}].frame_index must be a non-negative integer.`);
828
854
  }
829
- if ((type === "click" || type === "drag" || type === "fill" || type === "set_input_value" || type === "set_range_value" || type === "wait_for_selector" || type === "wait_for_text" || type === "assert_text_visible" || type === "assert_text_absent" || type === "assert_selector_count") && !selector) {
855
+ if ((type === "click" || 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) {
830
856
  throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
831
857
  }
832
858
  const fromX = numberValue(valueFromOwn(input, "from_x", "fromX", "start_x", "startX", "x1"));
@@ -920,7 +946,11 @@ function normalizeSetupAction(input, index) {
920
946
  "assign_return_to",
921
947
  "assignReturnTo",
922
948
  "return_state_path",
923
- "returnStatePath"
949
+ "returnStatePath",
950
+ "store_signature_to",
951
+ "storeSignatureTo",
952
+ "signature_path",
953
+ "signaturePath"
924
954
  );
925
955
  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;
926
956
  const untilPath = stringFromOwn(input, "until_path", "untilPath", "until_state_path", "untilStatePath", "until_window_path", "untilWindowPath", "until");
@@ -972,6 +1002,8 @@ function normalizeSetupAction(input, index) {
972
1002
  store_return_to: storeReturnTo,
973
1003
  capture_return: captureReturn,
974
1004
  return_summary_fields: normalizeReturnSummaryFields(input, index),
1005
+ compare_to: stringFromOwn(input, "compare_to", "compareTo", "previous_signature_path", "previousSignaturePath", "previous_path", "previousPath", "changed_from", "changedFrom"),
1006
+ expect_changed: booleanValue(valueFromOwn(input, "expect_changed", "expectChanged", "should_change", "shouldChange", "changed")),
975
1007
  until_path: untilPath,
976
1008
  until_expected_value: hasUntilExpectedValue ? toJsonValue(valueFromOwn(input, "until_expected_value", "untilExpectedValue", "until_expected", "untilExpected", "until_value", "untilValue", "expected_value", "expectedValue", "expected")) : void 0,
977
1009
  max_calls: maxCalls,
@@ -4098,6 +4130,28 @@ function profileSetupRangeValueReceipts(results) {
4098
4130
  reason: result.reason || result.error || null,
4099
4131
  }));
4100
4132
  }
4133
+ function profileSetupCanvasSignatureReceipts(results) {
4134
+ return (results || [])
4135
+ .filter((result) => result && profileSetupResultAction(result) === "canvas_signature")
4136
+ .map((result) => ({
4137
+ ordinal: result.ordinal ?? null,
4138
+ ok: result.ok !== false,
4139
+ selector: result.selector ?? null,
4140
+ frame_selector: result.frame_selector ?? null,
4141
+ label: result.label ?? null,
4142
+ hash: result.hash ?? null,
4143
+ data_length: result.data_length ?? null,
4144
+ width: result.width ?? null,
4145
+ height: result.height ?? null,
4146
+ css_width: result.css_width ?? null,
4147
+ css_height: result.css_height ?? null,
4148
+ compare_to: result.compare_to ?? null,
4149
+ previous_hash: result.previous_hash ?? null,
4150
+ changed: result.changed ?? null,
4151
+ return_stored_to: result.return_stored_to ?? null,
4152
+ reason: result.reason || result.error || null,
4153
+ }));
4154
+ }
4101
4155
  function sampleProfileSetupSummaryItems(items, limit) {
4102
4156
  if ((items || []).length <= limit) return items || [];
4103
4157
  const firstCount = Math.floor(limit / 2);
@@ -4156,6 +4210,8 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountsByViewp
4156
4210
  const sampledWindowEvalReceipts = sampleProfileSetupSummaryItems(windowEvalReceipts, 8);
4157
4211
  const rangeValueReceipts = profileSetupRangeValueReceipts(results);
4158
4212
  const sampledRangeValueReceipts = sampleProfileSetupSummaryItems(rangeValueReceipts, 8);
4213
+ const canvasSignatureReceipts = profileSetupCanvasSignatureReceipts(results);
4214
+ const sampledCanvasSignatureReceipts = sampleProfileSetupSummaryItems(canvasSignatureReceipts, 8);
4159
4215
  const clickedItems = results
4160
4216
  .filter((result) => result && profileSetupResultAction(result) === "click" && result.ok !== false)
4161
4217
  .map((result) => {
@@ -4217,6 +4273,9 @@ function profileSetupSummary(viewports, actionCount, expectedActionCountsByViewp
4217
4273
  set_range_value_total: rangeValueReceipts.length,
4218
4274
  set_range_value_truncated: rangeValueReceipts.length > sampledRangeValueReceipts.length,
4219
4275
  set_range_value: sampledRangeValueReceipts,
4276
+ canvas_signature_total: canvasSignatureReceipts.length,
4277
+ canvas_signature_truncated: canvasSignatureReceipts.length > sampledCanvasSignatureReceipts.length,
4278
+ canvas_signature: sampledCanvasSignatureReceipts,
4220
4279
  clicked,
4221
4280
  text_samples: textSamples,
4222
4281
  failed: failed.map((result) => ({
@@ -5691,6 +5750,54 @@ async function executeSetupAction(action, ordinal, viewport) {
5691
5750
  const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5692
5751
  const rect = element.getBoundingClientRect();
5693
5752
  const pointerId = payload.pointerType === "touch" ? 11 : 12;
5753
+ const capturedPointers = new Set();
5754
+ const originalOwnSetPointerCapture = Object.getOwnPropertyDescriptor(element, "setPointerCapture");
5755
+ const originalOwnReleasePointerCapture = Object.getOwnPropertyDescriptor(element, "releasePointerCapture");
5756
+ const originalOwnHasPointerCapture = Object.getOwnPropertyDescriptor(element, "hasPointerCapture");
5757
+ const originalSetPointerCapture = typeof element.setPointerCapture === "function" ? element.setPointerCapture.bind(element) : undefined;
5758
+ const originalReleasePointerCapture = typeof element.releasePointerCapture === "function" ? element.releasePointerCapture.bind(element) : undefined;
5759
+ const originalHasPointerCapture = typeof element.hasPointerCapture === "function" ? element.hasPointerCapture.bind(element) : undefined;
5760
+ const restorePointerCapture = () => {
5761
+ if (originalOwnSetPointerCapture) Object.defineProperty(element, "setPointerCapture", originalOwnSetPointerCapture);
5762
+ else delete (element as any).setPointerCapture;
5763
+ if (originalOwnReleasePointerCapture) Object.defineProperty(element, "releasePointerCapture", originalOwnReleasePointerCapture);
5764
+ else delete (element as any).releasePointerCapture;
5765
+ if (originalOwnHasPointerCapture) Object.defineProperty(element, "hasPointerCapture", originalOwnHasPointerCapture);
5766
+ else delete (element as any).hasPointerCapture;
5767
+ };
5768
+ Object.defineProperty(element, "setPointerCapture", {
5769
+ configurable: true,
5770
+ value: (activePointerId) => {
5771
+ capturedPointers.add(activePointerId);
5772
+ try {
5773
+ return originalSetPointerCapture?.(activePointerId);
5774
+ } catch {
5775
+ return undefined;
5776
+ }
5777
+ },
5778
+ });
5779
+ Object.defineProperty(element, "releasePointerCapture", {
5780
+ configurable: true,
5781
+ value: (activePointerId) => {
5782
+ capturedPointers.delete(activePointerId);
5783
+ try {
5784
+ return originalReleasePointerCapture?.(activePointerId);
5785
+ } catch {
5786
+ return undefined;
5787
+ }
5788
+ },
5789
+ });
5790
+ Object.defineProperty(element, "hasPointerCapture", {
5791
+ configurable: true,
5792
+ value: (activePointerId) => {
5793
+ if (capturedPointers.has(activePointerId)) return true;
5794
+ try {
5795
+ return Boolean(originalHasPointerCapture?.(activePointerId));
5796
+ } catch {
5797
+ return false;
5798
+ }
5799
+ },
5800
+ });
5694
5801
  const point = (progress) => ({
5695
5802
  clientX: rect.left + payload.start.x + (payload.end.x - payload.start.x) * progress,
5696
5803
  clientY: rect.top + payload.start.y + (payload.end.y - payload.start.y) * progress,
@@ -5710,16 +5817,20 @@ async function executeSetupAction(action, ordinal, viewport) {
5710
5817
  clientY: coords.clientY,
5711
5818
  }));
5712
5819
  };
5713
- dispatch("pointerover", 0);
5714
- dispatch("pointerenter", 0);
5715
- dispatch("pointerdown", 0);
5716
- for (let step = 1; step <= payload.steps; step += 1) {
5717
- dispatch("pointermove", step / payload.steps);
5718
- if (payload.durationMs && payload.steps > 1) await wait(payload.durationMs / payload.steps);
5820
+ try {
5821
+ dispatch("pointerover", 0);
5822
+ dispatch("pointerenter", 0);
5823
+ dispatch("pointerdown", 0);
5824
+ for (let step = 1; step <= payload.steps; step += 1) {
5825
+ dispatch("pointermove", step / payload.steps);
5826
+ if (payload.durationMs && payload.steps > 1) await wait(payload.durationMs / payload.steps);
5827
+ }
5828
+ dispatch("pointerup", 1);
5829
+ dispatch("pointerout", 1);
5830
+ dispatch("pointerleave", 1);
5831
+ } finally {
5832
+ restorePointerCapture();
5719
5833
  }
5720
- dispatch("pointerup", 1);
5721
- dispatch("pointerout", 1);
5722
- dispatch("pointerleave", 1);
5723
5834
  }, {
5724
5835
  pointerType,
5725
5836
  start: localStart,
@@ -5759,6 +5870,7 @@ async function executeSetupAction(action, ordinal, viewport) {
5759
5870
  to_x: toX,
5760
5871
  to_y: toY,
5761
5872
  pointer_type: pointerType,
5873
+ pointer_capture_polyfill: pointerType === "touch" || pointerType === "pen" ? true : undefined,
5762
5874
  steps,
5763
5875
  duration_ms: durationMs || undefined,
5764
5876
  };
@@ -6232,6 +6344,156 @@ async function executeSetupAction(action, ordinal, viewport) {
6232
6344
  reason: rangeResult && rangeResult.ok === true ? undefined : rangeResult?.reason || "range_value_not_set",
6233
6345
  };
6234
6346
  }
6347
+ if (type === "canvas_signature") {
6348
+ const scope = await setupActionScope(action, timeout);
6349
+ if (!scope.ok) return setupScopeFailure(base, scope);
6350
+ const locator = scope.context.locator(action.selector);
6351
+ const count = await locator.count();
6352
+ if (!count) return { ...base, ...setupScopeEvidence(scope), reason: "selector_not_found", count };
6353
+ const targetIndex = Number.isInteger(action.index) ? action.index : 0;
6354
+ if (targetIndex < 0 || targetIndex >= count) return { ...base, ...setupScopeEvidence(scope), reason: "index_out_of_range", count, target_index: targetIndex };
6355
+ const target = locator.nth(targetIndex);
6356
+ await target.waitFor({ state: "visible", timeout });
6357
+ const storeReturnTo = String(action.store_return_to || action.storeReturnTo || action.save_return_to || action.saveReturnTo || action.store_signature_to || action.storeSignatureTo || action.signature_path || action.signaturePath || "").trim();
6358
+ const compareTo = String(action.compare_to || action.compareTo || action.previous_signature_path || action.previousSignaturePath || action.previous_path || action.previousPath || action.changed_from || action.changedFrom || "").trim();
6359
+ const expectChanged = action.expect_changed === true || action.expectChanged === true || action.should_change === true || action.shouldChange === true || action.changed === true
6360
+ ? true
6361
+ : action.expect_changed === false || action.expectChanged === false || action.should_change === false || action.shouldChange === false || action.changed === false
6362
+ ? false
6363
+ : undefined;
6364
+ const signatureResult = await target.evaluate((element, payload) => {
6365
+ const toJsonValue = (value) => {
6366
+ if (value === null || value === undefined) return null;
6367
+ if (typeof value === "string" || typeof value === "boolean") return value;
6368
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
6369
+ if (Array.isArray(value)) return value.map(toJsonValue);
6370
+ if (typeof value === "object") {
6371
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, toJsonValue(child)]));
6372
+ }
6373
+ return String(value);
6374
+ };
6375
+ const pathParts = (path) => String(path || "").split(".").map((part) => part.trim()).filter(Boolean);
6376
+ const readWindowPath = (path) => {
6377
+ const parts = pathParts(path);
6378
+ if (parts[0] === "window") parts.shift();
6379
+ if (!parts.length) return { ok: false, reason: "missing_path" };
6380
+ let current = window;
6381
+ for (const part of parts) {
6382
+ if (current === null || current === undefined) return { ok: false, reason: "path_not_found", missing_part: part };
6383
+ current = current[part];
6384
+ if (current === undefined) return { ok: false, reason: "path_not_found", missing_part: part };
6385
+ }
6386
+ return { ok: true, value: toJsonValue(current) };
6387
+ };
6388
+ const storeWindowValue = (path, value) => {
6389
+ const parts = pathParts(path);
6390
+ if (parts[0] === "window") parts.shift();
6391
+ if (!parts.length) return { ok: false, reason: "missing_store_path" };
6392
+ let target = window;
6393
+ for (let index = 0; index < parts.length - 1; index += 1) {
6394
+ const part = parts[index];
6395
+ if (target[part] === null || typeof target[part] !== "object") target[part] = {};
6396
+ target = target[part];
6397
+ }
6398
+ target[parts[parts.length - 1]] = value;
6399
+ return { ok: true, path: parts.join(".") };
6400
+ };
6401
+ const hashText = (text) => {
6402
+ let hash = 2166136261;
6403
+ const step = Math.max(1, Math.floor((text.length || 1) / 4000));
6404
+ for (let index = 0; index < text.length; index += step) {
6405
+ hash ^= text.charCodeAt(index);
6406
+ hash = Math.imul(hash, 16777619) >>> 0;
6407
+ }
6408
+ return String(hash);
6409
+ };
6410
+ const tag = String(element && element.tagName ? element.tagName : "").toLowerCase();
6411
+ if (tag !== "canvas") return { ok: false, reason: "not_canvas_element", tag };
6412
+ const rect = element.getBoundingClientRect();
6413
+ let data = "";
6414
+ try {
6415
+ data = element.toDataURL("image/png");
6416
+ } catch (error) {
6417
+ return {
6418
+ ok: false,
6419
+ reason: "canvas_read_failed",
6420
+ error: String(error && error.message ? error.message : error).slice(0, 1000),
6421
+ width: element.width || 0,
6422
+ height: element.height || 0,
6423
+ css_width: Math.round(rect.width || 0),
6424
+ css_height: Math.round(rect.height || 0),
6425
+ };
6426
+ }
6427
+ const result = {
6428
+ ok: Boolean(element.width > 0 && element.height > 0 && data.length > 0),
6429
+ reason: element.width > 0 && element.height > 0 && data.length > 0 ? undefined : "empty_canvas_signature",
6430
+ hash: hashText(data),
6431
+ data_length: data.length,
6432
+ width: element.width || 0,
6433
+ height: element.height || 0,
6434
+ css_width: Math.round(rect.width || 0),
6435
+ css_height: Math.round(rect.height || 0),
6436
+ compare_to: payload.compareTo || undefined,
6437
+ previous_hash: null,
6438
+ changed: null,
6439
+ };
6440
+ if (payload.compareTo) {
6441
+ const previous = readWindowPath(payload.compareTo);
6442
+ if (!previous.ok) {
6443
+ result.ok = false;
6444
+ result.reason = previous.reason || "compare_path_not_found";
6445
+ result.missing_part = previous.missing_part || undefined;
6446
+ } else {
6447
+ const previousValue = previous.value;
6448
+ const previousHash = previousValue && typeof previousValue === "object" && !Array.isArray(previousValue)
6449
+ ? previousValue.hash || previousValue.signature || previousValue.canvas_hash || null
6450
+ : typeof previousValue === "string"
6451
+ ? previousValue
6452
+ : null;
6453
+ result.previous_hash = previousHash === null || previousHash === undefined ? null : String(previousHash);
6454
+ result.changed = result.previous_hash === null ? null : result.previous_hash !== result.hash;
6455
+ if (payload.expectChanged === true && result.changed !== true) {
6456
+ result.ok = false;
6457
+ result.reason = "canvas_signature_unchanged";
6458
+ } else if (payload.expectChanged === false && result.changed !== false) {
6459
+ result.ok = false;
6460
+ result.reason = "canvas_signature_changed";
6461
+ }
6462
+ }
6463
+ }
6464
+ if (payload.storeReturnTo) {
6465
+ const stored = storeWindowValue(payload.storeReturnTo, result);
6466
+ if (!stored.ok) {
6467
+ return { ...result, ok: false, reason: "signature_store_failed", store_reason: stored.reason };
6468
+ }
6469
+ return { ...result, return_stored_to: stored.path };
6470
+ }
6471
+ return result;
6472
+ }, { compareTo, expectChanged, storeReturnTo });
6473
+ return {
6474
+ ...base,
6475
+ ...setupScopeEvidence(scope),
6476
+ ok: signatureResult && signatureResult.ok === true,
6477
+ count,
6478
+ target_index: targetIndex,
6479
+ label: action.label || action.name || undefined,
6480
+ hash: signatureResult?.hash,
6481
+ data_length: signatureResult?.data_length,
6482
+ width: signatureResult?.width,
6483
+ height: signatureResult?.height,
6484
+ css_width: signatureResult?.css_width,
6485
+ css_height: signatureResult?.css_height,
6486
+ compare_to: signatureResult?.compare_to || compareTo || undefined,
6487
+ previous_hash: signatureResult?.previous_hash,
6488
+ changed: signatureResult?.changed,
6489
+ return_stored_to: signatureResult?.return_stored_to || storeReturnTo || undefined,
6490
+ missing_part: signatureResult?.missing_part || undefined,
6491
+ store_reason: signatureResult?.store_reason || undefined,
6492
+ tag: signatureResult?.tag,
6493
+ reason: signatureResult && signatureResult.ok === true ? undefined : signatureResult?.reason || "canvas_signature_failed",
6494
+ error: signatureResult?.error || undefined,
6495
+ };
6496
+ }
6235
6497
  if (type === "assert_selector_count") {
6236
6498
  const scope = await setupActionScope(action, timeout);
6237
6499
  if (!scope.ok) return setupScopeFailure(base, scope);