@reticlehq/browser 3.7.0 → 3.8.1

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.
Files changed (104) hide show
  1. package/dist/actions/action-report.d.ts +10 -0
  2. package/dist/actions/action-report.js +59 -14
  3. package/dist/actions/actions-dom.d.ts +4 -0
  4. package/dist/actions/actions-dom.js +20 -4
  5. package/dist/actions/actions.d.ts +1 -0
  6. package/dist/actions/actions.js +94 -46
  7. package/dist/actions/appeared-text.js +2 -2
  8. package/dist/actions/click-target.d.ts +1 -0
  9. package/dist/actions/click-target.js +14 -0
  10. package/dist/actions/danger-context.d.ts +2 -1
  11. package/dist/actions/danger-context.js +43 -17
  12. package/dist/actions/scroll.d.ts +2 -0
  13. package/dist/actions/scroll.js +10 -2
  14. package/dist/activate.d.ts +14 -0
  15. package/dist/activate.js +43 -0
  16. package/dist/busy.d.ts +3 -0
  17. package/dist/busy.js +42 -0
  18. package/dist/commands/commands.js +45 -17
  19. package/dist/connect-options.d.ts +12 -1
  20. package/dist/connect-options.js +18 -1
  21. package/dist/dom/a11y.d.ts +0 -2
  22. package/dist/dom/a11y.js +103 -363
  23. package/dist/dom/chart.d.ts +3 -2
  24. package/dist/dom/chart.js +3 -2
  25. package/dist/dom/dom-ignore.d.ts +6 -0
  26. package/dist/dom/dom-ignore.js +60 -5
  27. package/dist/dom/layout-audit.d.ts +15 -0
  28. package/dist/dom/layout-audit.js +208 -0
  29. package/dist/dom/paint-context.d.ts +15 -0
  30. package/dist/dom/paint-context.js +78 -0
  31. package/dist/dom/query-finders.d.ts +6 -0
  32. package/dist/dom/query-finders.js +216 -0
  33. package/dist/dom/query.d.ts +1 -1
  34. package/dist/dom/query.js +154 -227
  35. package/dist/dom/shadow-registry.d.ts +1 -0
  36. package/dist/dom/shadow-registry.js +27 -0
  37. package/dist/dom/snapshot.d.ts +5 -0
  38. package/dist/dom/snapshot.js +87 -35
  39. package/dist/dom/stable-selector.d.ts +1 -0
  40. package/dist/dom/stable-selector.js +19 -0
  41. package/dist/dom/text-match.d.ts +12 -0
  42. package/dist/dom/text-match.js +115 -0
  43. package/dist/dom/theme.d.ts +1 -0
  44. package/dist/dom/theme.js +21 -9
  45. package/dist/dom/virtualized.js +5 -1
  46. package/dist/dom/visibility.d.ts +7 -0
  47. package/dist/dom/visibility.js +322 -0
  48. package/dist/index.d.ts +4 -2
  49. package/dist/index.js +7 -1
  50. package/dist/observers/blind-spots.js +3 -2
  51. package/dist/observers/canvas.d.ts +18 -0
  52. package/dist/observers/canvas.js +76 -0
  53. package/dist/observers/dom.js +68 -19
  54. package/dist/observers/field.d.ts +11 -0
  55. package/dist/observers/field.js +117 -13
  56. package/dist/observers/install-all.js +4 -0
  57. package/dist/observers/media.d.ts +2 -0
  58. package/dist/observers/media.js +32 -0
  59. package/dist/observers/net-detail/network-body.d.ts +2 -0
  60. package/dist/observers/net-detail/network-body.js +36 -9
  61. package/dist/observers/net-detail/network-event-stream.d.ts +5 -0
  62. package/dist/observers/net-detail/network-event-stream.js +55 -0
  63. package/dist/observers/net-detail/network-frames.d.ts +2 -0
  64. package/dist/observers/net-detail/network-frames.js +39 -0
  65. package/dist/observers/net-detail/network-response-body.d.ts +3 -0
  66. package/dist/observers/net-detail/network-response-body.js +97 -0
  67. package/dist/observers/network.js +111 -101
  68. package/dist/observers/route.d.ts +2 -0
  69. package/dist/observers/route.js +33 -4
  70. package/dist/presenter/chrome/presenter-report-copy.d.ts +0 -5
  71. package/dist/presenter/chrome/presenter-report-copy.js +0 -14
  72. package/dist/presenter/hud-telemetry.js +1 -1
  73. package/dist/presenter/presenter-chat-views.d.ts +3 -4
  74. package/dist/presenter/presenter-chat-views.js +47 -20
  75. package/dist/presenter/presenter-config.d.ts +1 -0
  76. package/dist/presenter/presenter-config.js +1 -0
  77. package/dist/presenter/presenter-frame.d.ts +3 -2
  78. package/dist/presenter/presenter-frame.js +24 -15
  79. package/dist/presenter/presenter-harness-row.d.ts +4 -15
  80. package/dist/presenter/presenter-harness-row.js +11 -50
  81. package/dist/presenter/presenter-report-styles.js +0 -17
  82. package/dist/presenter/presenter-report.d.ts +1 -2
  83. package/dist/presenter/presenter-report.js +4 -54
  84. package/dist/presenter/presenter-run-state.js +2 -1
  85. package/dist/presenter/presenter-settings.d.ts +2 -3
  86. package/dist/presenter/presenter-settings.js +14 -17
  87. package/dist/presenter/presenter-shell.d.ts +6 -4
  88. package/dist/presenter/presenter-shell.js +37 -7
  89. package/dist/presenter/presenter.js +32 -11
  90. package/dist/registry/store-adapters.d.ts +5 -2
  91. package/dist/registry/store-adapters.js +40 -10
  92. package/dist/reticle-inject.js +700 -645
  93. package/dist/reticle.d.ts +4 -2
  94. package/dist/reticle.js +36 -6
  95. package/dist/security/page-location.d.ts +2 -0
  96. package/dist/security/page-location.js +7 -0
  97. package/dist/security/serialization.js +1 -1
  98. package/dist/storage-keys.d.ts +1 -0
  99. package/dist/storage-keys.js +1 -0
  100. package/dist/timers/native/native-timers.d.ts +1 -0
  101. package/dist/timers/native/native-timers.js +5 -0
  102. package/dist/transport/transport.d.ts +1 -0
  103. package/dist/transport/transport.js +18 -0
  104. package/package.json +6 -2
@@ -13,9 +13,14 @@ export interface ActionEffect {
13
13
  occluded: boolean;
14
14
  occludedBy: string | null;
15
15
  scrolledIntoView: boolean;
16
+ occluderName?: string;
17
+ noSize?: true;
18
+ offViewport?: true;
16
19
  heldMs?: number;
17
20
  alreadyAtValue?: true;
21
+ canvases?: number;
18
22
  }
23
+ export declare function visibleCanvases(doc: Document): number;
19
24
  export interface ActionResult {
20
25
  ok: true;
21
26
  ref: string;
@@ -25,6 +30,7 @@ export interface ActionResult {
25
30
  settleReason: SettleReason | null;
26
31
  effect: ActionEffect;
27
32
  testid?: string;
33
+ testidOf?: number;
28
34
  component?: string;
29
35
  role?: string;
30
36
  name?: string;
@@ -35,10 +41,13 @@ export interface ActionResult {
35
41
  line: number;
36
42
  column?: number;
37
43
  };
44
+ secret?: true;
38
45
  warning?: string;
46
+ reticleUi?: true;
39
47
  }
40
48
  export interface CapturedAnchor {
41
49
  testid?: string;
50
+ testidOf?: number;
42
51
  component?: string;
43
52
  source?: {
44
53
  file: string;
@@ -49,6 +58,7 @@ export interface CapturedAnchor {
49
58
  name?: string;
50
59
  nth?: number;
51
60
  of?: number;
61
+ secret?: true;
52
62
  }
53
63
  export declare function anchorOf(el: Element): CapturedAnchor;
54
64
  export declare const result: (ref: string, action: string, effect: ActionEffect, settled: boolean, settleReason: SettleReason | null, anchor: CapturedAnchor, warning?: string) => ActionResult;
@@ -1,25 +1,56 @@
1
1
  import { readTestId } from '../dom/addressing/testid-attr.js';
2
2
  import { QueryBy } from '@reticlehq/core';
3
3
  import { elementsMatching } from '../dom/query.js';
4
- import { getAccessibleName, getRole } from '../dom/a11y.js';
4
+ import { getAccessibleName, getRole, isSensitiveField } from '../dom/a11y.js';
5
5
  import { identifyComponent } from '../registry/stores/adapters.js';
6
+ import { isReticleUi } from '../dom/dom-ignore.js';
7
+ const CANVAS_SELECTOR = 'canvas';
8
+ export function visibleCanvases(doc) {
9
+ const width = doc.defaultView?.innerWidth ?? 0;
10
+ const height = doc.defaultView?.innerHeight ?? 0;
11
+ let count = 0;
12
+ for (const canvas of doc.querySelectorAll(CANVAS_SELECTOR)) {
13
+ if (isReticleUi(canvas)) {
14
+ continue;
15
+ }
16
+ const box = canvas.getBoundingClientRect();
17
+ const sized = box.width > 0 && box.height > 0;
18
+ const onScreen = box.bottom > 0 && box.right > 0 && box.top < height && box.left < width;
19
+ if (sized && onScreen) {
20
+ count += 1;
21
+ }
22
+ }
23
+ return count;
24
+ }
6
25
  export function anchorOf(el) {
7
26
  const testid = readTestId(el) ?? undefined;
8
27
  const info = identifyComponent(el);
9
28
  const out = {};
10
- if (testid !== undefined)
29
+ if (testid !== undefined) {
11
30
  out.testid = testid;
31
+ const shared = elementsMatching({ by: QueryBy.TESTID, value: testid }).length;
32
+ if (1 < shared) {
33
+ out.testidOf = shared;
34
+ }
35
+ }
12
36
  const component = info?.componentStack[0];
13
- if (component !== undefined)
37
+ if (component !== undefined) {
14
38
  out.component = component;
15
- if (info?.source !== undefined)
39
+ }
40
+ if (info?.source !== undefined) {
16
41
  out.source = info.source;
42
+ }
17
43
  const role = getRole(el);
18
44
  const name = getAccessibleName(el);
19
- if (role.length > 0)
45
+ if (role.length > 0) {
20
46
  out.role = role;
21
- if (name.length > 0)
47
+ }
48
+ if (name.length > 0) {
22
49
  out.name = name;
50
+ }
51
+ if (isSensitiveField(el)) {
52
+ out.secret = true;
53
+ }
23
54
  if (role.length > 0 && name.length > 0) {
24
55
  const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
25
56
  const at = same.indexOf(el);
@@ -40,21 +71,35 @@ export const result = (ref, action, effect, settled, settleReason, anchor, warni
40
71
  settleReason,
41
72
  effect,
42
73
  };
43
- if (anchor.testid !== undefined)
74
+ if (anchor.testid !== undefined) {
44
75
  base.testid = anchor.testid;
45
- if (anchor.component !== undefined)
76
+ }
77
+ if (anchor.testidOf !== undefined) {
78
+ base.testidOf = anchor.testidOf;
79
+ }
80
+ if (anchor.component !== undefined) {
46
81
  base.component = anchor.component;
47
- if (anchor.source !== undefined)
82
+ }
83
+ if (anchor.source !== undefined) {
48
84
  base.source = anchor.source;
49
- if (anchor.role !== undefined)
85
+ }
86
+ if (anchor.role !== undefined) {
50
87
  base.role = anchor.role;
51
- if (anchor.name !== undefined)
88
+ }
89
+ if (anchor.name !== undefined) {
52
90
  base.name = anchor.name;
53
- if (anchor.nth !== undefined)
91
+ }
92
+ if (anchor.nth !== undefined) {
54
93
  base.nth = anchor.nth;
55
- if (anchor.of !== undefined)
94
+ }
95
+ if (anchor.of !== undefined) {
56
96
  base.of = anchor.of;
57
- if (warning !== undefined)
97
+ }
98
+ if (anchor.secret !== undefined) {
99
+ base.secret = anchor.secret;
100
+ }
101
+ if (warning !== undefined) {
58
102
  base.warning = warning;
103
+ }
59
104
  return base;
60
105
  };
@@ -3,6 +3,9 @@ interface ClickGeometry {
3
3
  occluded: boolean;
4
4
  occludedBy: string | null;
5
5
  scrolledIntoView: boolean;
6
+ occluderName?: string;
7
+ noSize?: true;
8
+ offViewport?: true;
6
9
  }
7
10
  export declare const NO_GEOMETRY: ClickGeometry;
8
11
  export declare function mouseEventFor(el: Element, type: string, init?: MouseEventInit): MouseEvent;
@@ -14,6 +17,7 @@ export declare function fireClickSequence(el: ActionTarget, hold?: {
14
17
  prevented: boolean;
15
18
  heldMs: number;
16
19
  }>;
20
+ export declare function isOffViewport(el: Element, rect: DOMRect): boolean;
17
21
  export declare function clickGeometry(el: ActionTarget): ClickGeometry;
18
22
  export declare function firePointer(el: Element, type: string, relatedTarget?: EventTarget | null): void;
19
23
  export declare function firePointerNonBubbling(el: Element, type: string, relatedTarget?: EventTarget | null): void;
@@ -1,4 +1,6 @@
1
1
  import { refs } from '../dom/addressing/refs.js';
2
+ import { readTestId } from '../dom/addressing/testid-attr.js';
3
+ import { getAccessibleName, getRole } from '../dom/a11y.js';
2
4
  import { hitTestOccluder } from '../dom/occlusion.js';
3
5
  import { nativeFrame } from '../timers/native/native-timers.js';
4
6
  import { asSyntheticInput } from './synthetic/synthetic-input.js';
@@ -58,7 +60,7 @@ export async function fireClickSequence(el, hold, detail) {
58
60
  function isMeasurable(rect) {
59
61
  return rect.width > 0 || rect.height > 0;
60
62
  }
61
- function isOffViewport(el, rect) {
63
+ export function isOffViewport(el, rect) {
62
64
  const win = el.ownerDocument.defaultView;
63
65
  if (null === win)
64
66
  return false;
@@ -70,21 +72,35 @@ function hitTest(el, rect) {
70
72
  const top = hitTestOccluder(el, rect);
71
73
  return null === top
72
74
  ? { occluded: false, occludedBy: null }
73
- : { occluded: true, occludedBy: refs.refFor(top) };
75
+ : { occluded: true, occludedBy: refs.refFor(top), occluderName: nameOf(top) };
76
+ }
77
+ function nameOf(el) {
78
+ const testid = readTestId(el);
79
+ if (testid !== null && testid.length > 0)
80
+ return `[data-testid="${testid}"]`;
81
+ const name = getAccessibleName(el).trim();
82
+ return name.length > 0 ? `${getRole(el)} "${name.slice(0, 40)}"` : el.tagName.toLowerCase();
83
+ }
84
+ function hasNoSize(el) {
85
+ if (typeof el.getClientRects !== 'function')
86
+ return false;
87
+ const rects = Array.from(el.getClientRects());
88
+ return rects.length > 0 && rects.every((r) => 0 === r.width || 0 === r.height);
74
89
  }
75
90
  export function clickGeometry(el) {
76
91
  if (typeof el.getBoundingClientRect !== 'function')
77
92
  return NO_GEOMETRY;
78
93
  let rect = el.getBoundingClientRect();
79
94
  if (!isMeasurable(rect))
80
- return NO_GEOMETRY;
95
+ return hasNoSize(el) ? { ...NO_GEOMETRY, noSize: true } : NO_GEOMETRY;
81
96
  let scrolledIntoView = false;
82
97
  if (isOffViewport(el, rect) && 'function' === typeof el.scrollIntoView) {
83
98
  el.scrollIntoView({ block: 'center', inline: 'center' });
84
99
  scrolledIntoView = true;
85
100
  rect = el.getBoundingClientRect();
86
101
  }
87
- return { ...hitTest(el, rect), scrolledIntoView };
102
+ const stillOff = scrolledIntoView && isOffViewport(el, rect);
103
+ return { ...hitTest(el, rect), scrolledIntoView, ...(stillOff ? { offViewport: true } : {}) };
88
104
  }
89
105
  export function firePointer(el, type, relatedTarget = null) {
90
106
  asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: true, cancelable: true, relatedTarget })));
@@ -22,6 +22,7 @@ interface SequenceStepResult {
22
22
  line: number;
23
23
  column?: number;
24
24
  };
25
+ secret?: true;
25
26
  warning?: string;
26
27
  }
27
28
  export declare function executeSequence(steps: ActionStep[]): Promise<{
@@ -1,25 +1,31 @@
1
1
  import { ActionType, ActionWarning, clampHoldMs, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
2
+ import { isHiddenNativeOption } from '../dom/query-finders.js';
3
+ import { isReticleUi } from '../dom/dom-ignore.js';
2
4
  import { asSyntheticInput } from './synthetic/synthetic-input.js';
3
5
  import { echoRef, refs } from '../dom/addressing/refs.js';
4
- import { dangerousActionContext, requiresDangerousConfirmation, assertOutwardAllowed, submitControlFor, } from './danger-context.js';
6
+ import { requiresDangerousConfirmation, assertOutwardAllowed, assertPaymentEntryAllowed, submitControlFor, } from './danger-context.js';
5
7
  import { assertEditable, assertNotRichText, isAutofillSafeTarget, setNativeValue, } from './value-input.js';
6
- import { getRole, isVisible, getStates } from '../dom/a11y.js';
8
+ import { getStates } from '../dom/a11y.js';
9
+ import { isVisible } from '../dom/visibility.js';
7
10
  import { elementHasHoverHandlers } from '../registry/stores/adapters.js';
8
11
  import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/realm.js';
9
- import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
12
+ import { asSettleBudget, nativeSetTimeout, settle } from '../timers/native/native-timers.js';
13
+ import { publishSettledField, watchActedField } from '../observers/field.js';
10
14
  import { AppearedText } from './appeared-text.js';
11
15
  import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
12
16
  import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
13
- import { anchorOf, result } from './action-report.js';
17
+ import { anchorOf, result, visibleCanvases, } from './action-report.js';
14
18
  function asString(value, fallback = '') {
15
19
  return 'string' === typeof value ? value : fallback;
16
20
  }
17
21
  function requireElement(ref) {
18
22
  const el = refs.resolve(ref);
19
- if (null === el)
23
+ if (null === el) {
20
24
  throw new Error(`ref '${echoRef(ref)}' no longer resolves to an element`);
21
- if (!isActionTarget(el))
25
+ }
26
+ if (!isActionTarget(el)) {
22
27
  throw new Error(`ref '${echoRef(ref)}' is not an HTML or SVG element`);
28
+ }
23
29
  return el;
24
30
  }
25
31
  const FILL_LIKE = new Set([
@@ -48,8 +54,9 @@ const GENERIC_ACTION_ARG_KEYS = new Set([
48
54
  function assertUploadArgs(args) {
49
55
  const keys = Object.keys(args);
50
56
  const dropped = keys.filter((k) => !UPLOAD_ARG_KEYS.has(k) && !GENERIC_ACTION_ARG_KEYS.has(k));
51
- if (0 === dropped.length && keys.some((k) => UPLOAD_ARG_KEYS.has(k)))
57
+ if (0 === dropped.length && keys.some((k) => UPLOAD_ARG_KEYS.has(k))) {
52
58
  return;
59
+ }
53
60
  const detail = 0 === dropped.length
54
61
  ? 'no file was described'
55
62
  : `upload does not read ${dropped.join(', ')}, so it would be dropped`;
@@ -67,27 +74,34 @@ function assertActionAllowed(el, action, args) {
67
74
  (action === ActionType.PRESS && 'Enter' === pressKey(args));
68
75
  const dragTarget = action === ActionType.DRAG ? refs.resolve(dragTargetRef(args)) : null;
69
76
  const submitter = action === ActionType.PRESS && 'Enter' === pressKey(args) ? submitControlFor(el) : null;
70
- if (canTrigger)
77
+ if (canTrigger) {
71
78
  assertOutwardAllowed([el, submitter, isActionTarget(dragTarget) ? dragTarget : null]);
72
- const sourceDangerous = requiresDangerousConfirmation(dangerousActionContext(el), getRole(el)) ||
73
- (submitter !== null &&
74
- requiresDangerousConfirmation(dangerousActionContext(submitter), getRole(submitter)));
75
- const targetDangerous = isActionTarget(dragTarget) &&
76
- requiresDangerousConfirmation(dangerousActionContext(dragTarget), getRole(dragTarget));
79
+ }
80
+ if (action === ActionType.FILL || action === ActionType.TYPE) {
81
+ assertPaymentEntryAllowed(el);
82
+ }
83
+ const sourceDangerous = requiresDangerousConfirmation(el) ||
84
+ (submitter !== null && requiresDangerousConfirmation(submitter));
85
+ const targetDangerous = isActionTarget(dragTarget) && requiresDangerousConfirmation(dragTarget);
77
86
  if (canTrigger &&
78
87
  (sourceDangerous || targetDangerous) &&
79
88
  args[DANGEROUS_ACTION_CONFIRM_ARG] !== true) {
80
89
  throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
81
90
  }
91
+ if (CLICK_LIKE.has(action) && el instanceof HTMLElement && isHiddenNativeOption(el)) {
92
+ throw new Error('cannot click a native <option> inside a visually-hidden <select> — open the dropdown and target the visible option');
93
+ }
82
94
  }
83
95
  function optionValueFor(options, named) {
84
- if (options.some((option) => option.value === named))
96
+ if (options.some((option) => option.value === named)) {
85
97
  return named;
98
+ }
86
99
  const byLabel = options.filter((option) => option.label.trim() === named.trim());
87
100
  return 1 === byLabel.length ? (byLabel[0]?.value ?? named) : named;
88
101
  }
89
102
  import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, } from './actions-dom.js';
90
103
  import { scrollFrom } from './scroll.js';
104
+ import { clickHitsNoControl } from './click-target.js';
91
105
  function enabledOf(el) {
92
106
  return !getStates(el).includes(ElementState.DISABLED);
93
107
  }
@@ -99,8 +113,9 @@ function valueOf(el) {
99
113
  }
100
114
  function activeRef(el) {
101
115
  const active = el.ownerDocument.activeElement;
102
- if (null === active || active === el.ownerDocument.body)
116
+ if (null === active || active === el.ownerDocument.body) {
103
117
  return null;
118
+ }
104
119
  return refs.refFor(active);
105
120
  }
106
121
  async function dispatchFor(el, action, args) {
@@ -136,8 +151,9 @@ async function dispatchOther(el, action, args) {
136
151
  firePointer(el, 'pointermove', from);
137
152
  const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true }));
138
153
  const holdMs = clampHoldMs(args['holdMs']);
139
- if (holdMs > 0)
154
+ if (holdMs > 0) {
140
155
  await sleep(holdMs);
156
+ }
141
157
  return !moved;
142
158
  }
143
159
  case ActionType.FOCUS:
@@ -161,8 +177,9 @@ async function dispatchOther(el, action, args) {
161
177
  'To empty a field on purpose use the `clear` action, or fill with an explicit "".');
162
178
  }
163
179
  assertEditable(el, 'fill');
164
- if (!isAutofillSafeTarget(el))
180
+ if (!isAutofillSafeTarget(el)) {
165
181
  el.focus();
182
+ }
166
183
  return setNativeValue(el, args['value']);
167
184
  }
168
185
  assertNotRichText(el, 'fill');
@@ -214,8 +231,9 @@ async function dispatchOther(el, action, args) {
214
231
  if ('radio' === el.type && action === ActionType.UNCHECK) {
215
232
  throw new Error('cannot uncheck a radio button — a real user could not; select another radio in the group');
216
233
  }
217
- if (alreadyAtCheckedState(el, action))
234
+ if (alreadyAtCheckedState(el, action)) {
218
235
  return false;
236
+ }
219
237
  const event = mouseEventFor(el, 'click', {
220
238
  bubbles: true,
221
239
  cancelable: true,
@@ -226,25 +244,29 @@ async function dispatchOther(el, action, args) {
226
244
  }
227
245
  case ActionType.SUBMIT: {
228
246
  const form = isForm(el) ? el : el.closest('form');
229
- if (null === form)
247
+ if (null === form) {
230
248
  throw new Error('no form to submit');
249
+ }
231
250
  form.requestSubmit();
232
251
  return false;
233
252
  }
234
253
  case ActionType.PRESS: {
235
254
  const mods = pressModifiers(args);
236
255
  const combo = pressKeys(args);
237
- if (combo.length > 0)
256
+ if (combo.length > 0) {
238
257
  return await pressCombo(el, combo, mods, clampHoldMs(args['holdMs']));
258
+ }
239
259
  const key = pressKey(args);
240
260
  const code = pressCode(args, key);
241
261
  const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
242
262
  closeModalOnEscape(el, key, down);
243
- if (down)
263
+ if (down) {
244
264
  dispatchKeypress(el, key, code, mods);
265
+ }
245
266
  const hold = clampHoldMs(args['holdMs']);
246
- if (hold > 0)
267
+ if (hold > 0) {
247
268
  await holdKey(el, key, code, mods, hold);
269
+ }
248
270
  asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
249
271
  return !down;
250
272
  }
@@ -270,8 +292,9 @@ async function dispatchOther(el, action, args) {
270
292
  if (true === args['__base64']) {
271
293
  const bin = atob(rawContent);
272
294
  const arr = new Uint8Array(bin.length);
273
- for (let i = 0; i < bin.length; i++)
295
+ for (let i = 0; i < bin.length; i++) {
274
296
  arr[i] = bin.charCodeAt(i);
297
+ }
275
298
  fileBody = arr.buffer.slice(0);
276
299
  }
277
300
  const file = new File([fileBody], asString(args['name'], 'file.txt'), {
@@ -286,8 +309,9 @@ async function dispatchOther(el, action, args) {
286
309
  }
287
310
  case ActionType.DRAG: {
288
311
  const toRef = dragTargetRef(args);
289
- if ('' === toRef)
312
+ if ('' === toRef) {
290
313
  return await dragElement(el, null, args['data']);
314
+ }
291
315
  const resolved = refs.resolve(toRef);
292
316
  if (!isActionTarget(resolved)) {
293
317
  throw new Error(`drag target '${echoRef(toRef)}' did not resolve to an element — pass a ref from ` +
@@ -299,26 +323,24 @@ async function dispatchOther(el, action, args) {
299
323
  throw new Error(`unknown action '${action}'`);
300
324
  }
301
325
  }
302
- function asSettleBudget(raw) {
303
- if ('number' !== typeof raw || !Number.isFinite(raw))
304
- return undefined;
305
- return Math.max(0, Math.min(raw, FRAME_BUDGET_MS_CEILING));
306
- }
307
- const FRAME_BUDGET_MS_CEILING = 200;
308
326
  export async function executeAction(ref, action, args = {}) {
309
327
  const named = 0 < ref.length;
310
328
  const el = isReflessDocumentPress(ref, action, args) ? focusedOrDocument() : requireElement(ref);
311
- if (named)
329
+ if (named) {
312
330
  assertActionAllowed(el, action, args);
313
- if (named)
331
+ }
332
+ if (named) {
314
333
  assertNotLeaving(el);
334
+ }
315
335
  const anchor = named ? anchorOf(el) : {};
316
336
  const visible = isVisible(el);
317
337
  const enabled = enabledOf(el);
318
338
  const prevFocus = activeRef(el);
319
339
  const valueBefore = valueOf(el);
340
+ const actedField = watchActedField(el);
320
341
  const geometry = CLICK_LIKE.has(action) ? clickGeometry(el) : NO_GEOMETRY;
321
342
  const alreadyAtValue = alreadyAtCheckedState(el, action);
343
+ const hitsNoControl = clickHitsNoControl(el, action);
322
344
  let mutated = 0;
323
345
  const said = new AppearedText();
324
346
  const obs = new MutationObserver((records) => {
@@ -346,9 +368,11 @@ export async function executeAction(ref, action, args = {}) {
346
368
  settled = outcome.settled;
347
369
  settleReason = outcome.settled ? null : SettleReason.TIMEOUT;
348
370
  obs.disconnect();
371
+ actedField?.stop();
349
372
  }
350
373
  assertAriaToggleLanded(el, action, alreadyAtValue);
351
374
  const valueAfter = valueOf(el);
375
+ publishSettledField(el, actedField);
352
376
  const nextFocus = activeRef(el);
353
377
  const effect = {
354
378
  dispatched: true,
@@ -364,17 +388,32 @@ export async function executeAction(ref, action, args = {}) {
364
388
  occluded: geometry.occluded,
365
389
  occludedBy: geometry.occludedBy,
366
390
  scrolledIntoView: geometry.scrolledIntoView,
391
+ ...(geometry.occluderName === undefined ? {} : { occluderName: geometry.occluderName }),
392
+ ...(true === geometry.noSize ? { noSize: true } : {}),
393
+ ...(true === geometry.offViewport ? { offViewport: true } : {}),
367
394
  ...(heldMs > 0 ? { heldMs } : {}),
368
395
  ...(alreadyAtValue ? { alreadyAtValue: true } : {}),
396
+ ...canvasesOnScreen(),
369
397
  };
370
- const warning = !named
371
- ? ActionWarning.GLOBAL_PRESS
372
- : geometry.occluded
373
- ? ActionWarning.CLICK_OCCLUDED
374
- : action === ActionType.HOVER && elementHasHoverHandlers(el)
375
- ? ActionWarning.HOVER_NATIVE_ENTER_LEAVE
376
- : undefined;
377
- return result(ref, action, effect, settled, settleReason, anchor, warning);
398
+ let warning;
399
+ if (!named) {
400
+ warning = ActionWarning.GLOBAL_PRESS;
401
+ }
402
+ else if (geometry.occluded) {
403
+ warning = ActionWarning.CLICK_OCCLUDED;
404
+ }
405
+ else if (action === ActionType.HOVER && elementHasHoverHandlers(el)) {
406
+ warning = ActionWarning.HOVER_NATIVE_ENTER_LEAVE;
407
+ }
408
+ else if (hitsNoControl) {
409
+ warning = ActionWarning.CLICK_NOT_A_CONTROL;
410
+ }
411
+ const done = result(ref, action, effect, settled, settleReason, anchor, warning);
412
+ return isReticleUi(el) ? { ...done, reticleUi: true } : done;
413
+ }
414
+ function canvasesOnScreen() {
415
+ const canvases = visibleCanvases(document);
416
+ return canvases > 0 ? { canvases } : {};
378
417
  }
379
418
  const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
380
419
  export async function executeSequence(steps) {
@@ -390,18 +429,27 @@ export async function executeSequence(steps) {
390
429
  settled: res.settled,
391
430
  settleReason: res.settleReason,
392
431
  };
393
- if (res.testid !== undefined)
432
+ if (res.testid !== undefined) {
394
433
  stepBase.testid = res.testid;
395
- if (res.component !== undefined)
434
+ }
435
+ if (res.component !== undefined) {
396
436
  stepBase.component = res.component;
397
- if (res.role !== undefined)
437
+ }
438
+ if (res.role !== undefined) {
398
439
  stepBase.role = res.role;
399
- if (res.name !== undefined)
440
+ }
441
+ if (res.name !== undefined) {
400
442
  stepBase.name = res.name;
401
- if (res.source !== undefined)
443
+ }
444
+ if (res.source !== undefined) {
402
445
  stepBase.source = res.source;
403
- if (res.warning !== undefined)
446
+ }
447
+ if (res.secret !== undefined) {
448
+ stepBase.secret = res.secret;
449
+ }
450
+ if (res.warning !== undefined) {
404
451
  stepBase.warning = res.warning;
452
+ }
405
453
  stepResults.push(stepBase);
406
454
  }
407
455
  return { ok: true, count: steps.length, effects, steps: stepResults };
@@ -1,4 +1,4 @@
1
- import { isIgnored } from '../dom/dom-ignore.js';
1
+ import { isWordsIgnored } from '../dom/dom-ignore.js';
2
2
  import { isSensitiveField } from '../dom/a11y.js';
3
3
  const APPEARED_MAX = 200;
4
4
  const JOIN = ' | ';
@@ -45,7 +45,7 @@ export class AppearedText {
45
45
  return this.#length > APPEARED_MAX;
46
46
  }
47
47
  #add(raw, owner) {
48
- if (null !== owner && isIgnored(owner))
48
+ if (null !== owner && isWordsIgnored(owner))
49
49
  return;
50
50
  if (null !== owner && isSensitiveField(owner))
51
51
  return;
@@ -0,0 +1 @@
1
+ export declare function clickHitsNoControl(el: Element, action: string): boolean;
@@ -0,0 +1,14 @@
1
+ import { ActionType } from '@reticlehq/core';
2
+ import { getRole } from '../dom/a11y.js';
3
+ import { parentAcrossShadowBoundary } from '../dom/shadow-registry.js';
4
+ import { INTERACTIVE } from '../dom/snapshot.js';
5
+ const CLICK_FORWARDING = 'label, summary, [onclick], [tabindex], [contenteditable]:not([contenteditable="false"])';
6
+ export function clickHitsNoControl(el, action) {
7
+ if (ActionType.CLICK !== action && ActionType.DBLCLICK !== action)
8
+ return false;
9
+ for (let node = el; node !== null; node = parentAcrossShadowBoundary(node)) {
10
+ if (INTERACTIVE.has(getRole(node)) || node.matches(CLICK_FORWARDING))
11
+ return false;
12
+ }
13
+ return 'pointer' !== getComputedStyle(el).cursor;
14
+ }
@@ -1,6 +1,7 @@
1
1
  import { type ActionTarget } from '../dom/realm.js';
2
2
  export declare function dangerousActionContext(el: ActionTarget): string;
3
3
  export declare function submitControlFor(el: ActionTarget): HTMLElement | null;
4
- export declare function requiresDangerousConfirmation(text: string, role?: string): boolean;
4
+ export declare function requiresDangerousConfirmation(el: ActionTarget): boolean;
5
5
  export declare function setOutwardPolicy(allowed: readonly string[] | undefined): void;
6
6
  export declare function assertOutwardAllowed(targets: readonly (ActionTarget | null)[]): void;
7
+ export declare function assertPaymentEntryAllowed(el: ActionTarget): void;