@reticlehq/browser 3.6.0 → 3.8.0

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 (129) hide show
  1. package/dist/actions/action-report.d.ts +64 -0
  2. package/dist/actions/action-report.js +87 -0
  3. package/dist/actions/actions-dom.d.ts +5 -0
  4. package/dist/actions/actions-dom.js +32 -4
  5. package/dist/actions/actions-press.d.ts +1 -0
  6. package/dist/actions/actions-press.js +25 -1
  7. package/dist/actions/actions.d.ts +2 -37
  8. package/dist/actions/actions.js +56 -80
  9. package/dist/actions/appeared-text.js +5 -2
  10. package/dist/actions/click-target.d.ts +1 -0
  11. package/dist/actions/click-target.js +14 -0
  12. package/dist/actions/danger-context.d.ts +4 -1
  13. package/dist/actions/danger-context.js +45 -4
  14. package/dist/actions/value-input.d.ts +1 -0
  15. package/dist/actions/value-input.js +11 -0
  16. package/dist/activate.d.ts +14 -0
  17. package/dist/activate.js +43 -0
  18. package/dist/busy.d.ts +3 -0
  19. package/dist/busy.js +42 -0
  20. package/dist/commands/commands.js +49 -17
  21. package/dist/connect-options.d.ts +12 -1
  22. package/dist/connect-options.js +18 -1
  23. package/dist/dom/a11y.d.ts +2 -0
  24. package/dist/dom/a11y.js +353 -80
  25. package/dist/dom/addressing/coverage-markers.d.ts +6 -0
  26. package/dist/dom/addressing/coverage-markers.js +13 -0
  27. package/dist/dom/chart.d.ts +3 -2
  28. package/dist/dom/chart.js +3 -2
  29. package/dist/dom/dom-ignore.d.ts +8 -0
  30. package/dist/dom/dom-ignore.js +57 -2
  31. package/dist/dom/layout-audit.d.ts +15 -0
  32. package/dist/dom/layout-audit.js +208 -0
  33. package/dist/dom/paint-context.d.ts +15 -0
  34. package/dist/dom/paint-context.js +78 -0
  35. package/dist/dom/query.d.ts +2 -1
  36. package/dist/dom/query.js +163 -38
  37. package/dist/dom/render-page.d.ts +12 -0
  38. package/dist/dom/render-page.js +135 -0
  39. package/dist/dom/shadow-registry.d.ts +1 -0
  40. package/dist/dom/shadow-registry.js +27 -0
  41. package/dist/dom/snapshot.d.ts +5 -0
  42. package/dist/dom/snapshot.js +135 -36
  43. package/dist/dom/stable-selector.d.ts +1 -0
  44. package/dist/dom/stable-selector.js +19 -0
  45. package/dist/dom/text-match.d.ts +12 -0
  46. package/dist/dom/text-match.js +115 -0
  47. package/dist/dom/theme.d.ts +1 -0
  48. package/dist/dom/theme.js +21 -9
  49. package/dist/dom/virtualized.js +5 -1
  50. package/dist/index.d.ts +2 -1
  51. package/dist/index.js +5 -0
  52. package/dist/observers/blind-spots.js +3 -2
  53. package/dist/observers/canvas.d.ts +18 -0
  54. package/dist/observers/canvas.js +76 -0
  55. package/dist/observers/dom.js +96 -32
  56. package/dist/observers/field.d.ts +11 -0
  57. package/dist/observers/field.js +116 -30
  58. package/dist/observers/install-all.js +4 -0
  59. package/dist/observers/media.d.ts +2 -0
  60. package/dist/observers/media.js +32 -0
  61. package/dist/observers/net-detail/network-body.d.ts +2 -0
  62. package/dist/observers/net-detail/network-body.js +8 -1
  63. package/dist/observers/network.js +82 -13
  64. package/dist/observers/perf.js +53 -1
  65. package/dist/observers/route.d.ts +2 -0
  66. package/dist/observers/route.js +39 -9
  67. package/dist/presenter/carousel/carousel.js +2 -7
  68. package/dist/presenter/carousel/panel-slides.d.ts +2 -4
  69. package/dist/presenter/carousel/panel-slides.js +17 -15
  70. package/dist/presenter/chrome/log-memory.d.ts +1 -0
  71. package/dist/presenter/chrome/presenter-log.d.ts +7 -0
  72. package/dist/presenter/chrome/presenter-log.js +48 -8
  73. package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
  74. package/dist/presenter/chrome/presenter-morph.js +34 -0
  75. package/dist/presenter/chrome/presenter-report-copy.d.ts +12 -4
  76. package/dist/presenter/chrome/presenter-report-copy.js +12 -4
  77. package/dist/presenter/hud-telemetry.js +55 -4
  78. package/dist/presenter/presenter-account-styles.js +3 -3
  79. package/dist/presenter/presenter-account.d.ts +27 -5
  80. package/dist/presenter/presenter-account.js +83 -6
  81. package/dist/presenter/presenter-agent-link.d.ts +9 -0
  82. package/dist/presenter/presenter-agent-link.js +62 -0
  83. package/dist/presenter/presenter-chat-views.d.ts +25 -4
  84. package/dist/presenter/presenter-chat-views.js +339 -72
  85. package/dist/presenter/presenter-config.d.ts +3 -0
  86. package/dist/presenter/presenter-config.js +2 -0
  87. package/dist/presenter/presenter-controls.d.ts +0 -1
  88. package/dist/presenter/presenter-controls.js +13 -91
  89. package/dist/presenter/presenter-design-tokens.js +1 -1
  90. package/dist/presenter/presenter-frame.d.ts +3 -2
  91. package/dist/presenter/presenter-frame.js +31 -24
  92. package/dist/presenter/presenter-harness-row.d.ts +56 -0
  93. package/dist/presenter/presenter-harness-row.js +170 -0
  94. package/dist/presenter/presenter-personas.d.ts +35 -0
  95. package/dist/presenter/presenter-personas.js +95 -0
  96. package/dist/presenter/presenter-plan.js +25 -24
  97. package/dist/presenter/presenter-report-styles.js +23 -22
  98. package/dist/presenter/presenter-report.js +29 -13
  99. package/dist/presenter/presenter-run-state.js +2 -1
  100. package/dist/presenter/presenter-settings-styles.js +8 -4
  101. package/dist/presenter/presenter-settings.d.ts +4 -4
  102. package/dist/presenter/presenter-settings.js +66 -28
  103. package/dist/presenter/presenter-shell-styles.js +12 -3
  104. package/dist/presenter/presenter-shell.d.ts +11 -3
  105. package/dist/presenter/presenter-shell.js +60 -22
  106. package/dist/presenter/presenter-styles.js +0 -2
  107. package/dist/presenter/presenter-workspace.js +10 -3
  108. package/dist/presenter/presenter.d.ts +3 -2
  109. package/dist/presenter/presenter.js +107 -25
  110. package/dist/registry/declared-platform.js +2 -0
  111. package/dist/registry/store-adapters.d.ts +5 -2
  112. package/dist/registry/store-adapters.js +40 -10
  113. package/dist/reticle-inject.js +771 -703
  114. package/dist/reticle.d.ts +4 -2
  115. package/dist/reticle.js +72 -8
  116. package/dist/security/page-location.d.ts +2 -0
  117. package/dist/security/page-location.js +7 -0
  118. package/dist/security/serialization.js +1 -1
  119. package/dist/storage-keys.d.ts +4 -2
  120. package/dist/storage-keys.js +8 -3
  121. package/dist/timers/native/native-timers.d.ts +1 -0
  122. package/dist/timers/native/native-timers.js +5 -0
  123. package/dist/transport/transport.d.ts +1 -0
  124. package/dist/transport/transport.js +23 -3
  125. package/dist/transport/unreachable-message.d.ts +1 -1
  126. package/dist/transport/unreachable-message.js +3 -5
  127. package/package.json +6 -2
  128. package/dist/presenter/carousel/offer-card.d.ts +0 -21
  129. package/dist/presenter/carousel/offer-card.js +0 -56
@@ -0,0 +1,64 @@
1
+ import { type SettleReason } from '@reticlehq/core';
2
+ export interface ActionEffect {
3
+ dispatched: true;
4
+ targetMatched: boolean;
5
+ visible: boolean;
6
+ enabled: boolean;
7
+ defaultPrevented: boolean;
8
+ focusMoved: string | null;
9
+ valueChanged: boolean;
10
+ autofillSafe?: true;
11
+ domMutatedWithin: number;
12
+ appeared?: string;
13
+ occluded: boolean;
14
+ occludedBy: string | null;
15
+ scrolledIntoView: boolean;
16
+ occluderName?: string;
17
+ noSize?: true;
18
+ offViewport?: true;
19
+ heldMs?: number;
20
+ alreadyAtValue?: true;
21
+ canvases?: number;
22
+ }
23
+ export declare function visibleCanvases(doc: Document): number;
24
+ export interface ActionResult {
25
+ ok: true;
26
+ ref: string;
27
+ action: string;
28
+ dispatched: boolean;
29
+ settled: boolean;
30
+ settleReason: SettleReason | null;
31
+ effect: ActionEffect;
32
+ testid?: string;
33
+ testidOf?: number;
34
+ component?: string;
35
+ role?: string;
36
+ name?: string;
37
+ nth?: number;
38
+ of?: number;
39
+ source?: {
40
+ file: string;
41
+ line: number;
42
+ column?: number;
43
+ };
44
+ secret?: true;
45
+ warning?: string;
46
+ reticleUi?: true;
47
+ }
48
+ export interface CapturedAnchor {
49
+ testid?: string;
50
+ testidOf?: number;
51
+ component?: string;
52
+ source?: {
53
+ file: string;
54
+ line: number;
55
+ column?: number;
56
+ };
57
+ role?: string;
58
+ name?: string;
59
+ nth?: number;
60
+ of?: number;
61
+ secret?: true;
62
+ }
63
+ export declare function anchorOf(el: Element): CapturedAnchor;
64
+ export declare const result: (ref: string, action: string, effect: ActionEffect, settled: boolean, settleReason: SettleReason | null, anchor: CapturedAnchor, warning?: string) => ActionResult;
@@ -0,0 +1,87 @@
1
+ import { readTestId } from '../dom/addressing/testid-attr.js';
2
+ import { QueryBy } from '@reticlehq/core';
3
+ import { elementsMatching } from '../dom/query.js';
4
+ import { getAccessibleName, getRole, isSensitiveField } from '../dom/a11y.js';
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
+ const box = canvas.getBoundingClientRect();
16
+ const sized = box.width > 0 && box.height > 0;
17
+ const onScreen = box.bottom > 0 && box.right > 0 && box.top < height && box.left < width;
18
+ if (sized && onScreen)
19
+ count += 1;
20
+ }
21
+ return count;
22
+ }
23
+ export function anchorOf(el) {
24
+ const testid = readTestId(el) ?? undefined;
25
+ const info = identifyComponent(el);
26
+ const out = {};
27
+ if (testid !== undefined) {
28
+ out.testid = testid;
29
+ const shared = elementsMatching({ by: QueryBy.TESTID, value: testid }).length;
30
+ if (1 < shared)
31
+ out.testidOf = shared;
32
+ }
33
+ const component = info?.componentStack[0];
34
+ if (component !== undefined)
35
+ out.component = component;
36
+ if (info?.source !== undefined)
37
+ out.source = info.source;
38
+ const role = getRole(el);
39
+ const name = getAccessibleName(el);
40
+ if (role.length > 0)
41
+ out.role = role;
42
+ if (name.length > 0)
43
+ out.name = name;
44
+ if (isSensitiveField(el))
45
+ out.secret = true;
46
+ if (role.length > 0 && name.length > 0) {
47
+ const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
48
+ const at = same.indexOf(el);
49
+ if (1 < same.length && 0 <= at) {
50
+ out.nth = at;
51
+ out.of = same.length;
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+ export const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
57
+ const base = {
58
+ ok: true,
59
+ ref,
60
+ action,
61
+ dispatched: true,
62
+ settled,
63
+ settleReason,
64
+ effect,
65
+ };
66
+ if (anchor.testid !== undefined)
67
+ base.testid = anchor.testid;
68
+ if (anchor.testidOf !== undefined)
69
+ base.testidOf = anchor.testidOf;
70
+ if (anchor.component !== undefined)
71
+ base.component = anchor.component;
72
+ if (anchor.source !== undefined)
73
+ base.source = anchor.source;
74
+ if (anchor.role !== undefined)
75
+ base.role = anchor.role;
76
+ if (anchor.name !== undefined)
77
+ base.name = anchor.name;
78
+ if (anchor.nth !== undefined)
79
+ base.nth = anchor.nth;
80
+ if (anchor.of !== undefined)
81
+ base.of = anchor.of;
82
+ if (anchor.secret !== undefined)
83
+ base.secret = anchor.secret;
84
+ if (warning !== undefined)
85
+ base.warning = warning;
86
+ return base;
87
+ };
@@ -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;
@@ -26,4 +30,5 @@ export declare function fireTapSequence(el: ActionTarget, hold: {
26
30
  prevented: boolean;
27
31
  heldMs: number;
28
32
  }>;
33
+ export declare function assertNotLeaving(el: Element): void;
29
34
  export {};
@@ -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 })));
@@ -231,3 +247,15 @@ function makeTouchEvent(type, touches) {
231
247
  }
232
248
  return new Event(type, { bubbles: true, cancelable: true });
233
249
  }
250
+ const POPUP_ROLES = new Set(['menu', 'menubar', 'listbox', 'dialog', 'alertdialog', 'tooltip']);
251
+ const CLOSED_STATE = 'closed';
252
+ export function assertNotLeaving(el) {
253
+ for (let up = el.parentElement; up !== null; up = up.parentElement) {
254
+ if (CLOSED_STATE !== up.getAttribute('data-state'))
255
+ continue;
256
+ if (!POPUP_ROLES.has(up.getAttribute('role') ?? ''))
257
+ continue;
258
+ throw new Error(`'${up.getAttribute('role') ?? 'popup'}' holding this element is closing (data-state="closed"), ` +
259
+ 'so a person could not click it: look again once it has gone and act on what is open');
260
+ }
261
+ }
@@ -8,6 +8,7 @@ export interface ModifierFlags {
8
8
  }
9
9
  export declare function pressModifiers(args: Record<string, unknown>): ModifierFlags;
10
10
  export declare function pressCode(args: Record<string, unknown>, key: string): string;
11
+ export declare function dispatchKeypress(el: ActionTarget, key: string, code: string, mods: ModifierFlags, repeat?: boolean): void;
11
12
  export declare function focusedOrDocument(): HTMLElement;
12
13
  export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
13
14
  export declare function pressKeys(args: Record<string, unknown>): string[];
@@ -57,6 +57,26 @@ export function pressCode(args, key) {
57
57
  }
58
58
  return /^[A-Z][A-Za-z0-9]*$/.test(key) && KNOWN_NAMED_KEYS.has(key) ? key : '';
59
59
  }
60
+ export function dispatchKeypress(el, key, code, mods, repeat = false) {
61
+ if (ENTER_KEY !== key && 1 !== key.length)
62
+ return;
63
+ const event = new KeyboardEvent('keypress', {
64
+ key,
65
+ code,
66
+ bubbles: true,
67
+ cancelable: true,
68
+ repeat,
69
+ ...mods,
70
+ });
71
+ const legacy = ENTER_KEY === key ? ENTER_CHAR_CODE : key.charCodeAt(0);
72
+ for (const field of LEGACY_KEY_CODE_FIELDS) {
73
+ Object.defineProperty(event, field, { value: legacy });
74
+ }
75
+ asSyntheticInput(() => el.dispatchEvent(event));
76
+ }
77
+ const ENTER_KEY = 'Enter';
78
+ const ENTER_CHAR_CODE = 13;
79
+ const LEGACY_KEY_CODE_FIELDS = ['keyCode', 'charCode', 'which'];
60
80
  const KNOWN_NAMED_KEYS = new Set([
61
81
  'Enter',
62
82
  'Escape',
@@ -139,7 +159,7 @@ export async function holdKey(el, key, code, mods, ms) {
139
159
  if (ms > KEY_REPEAT_DELAY_MS)
140
160
  await sleep(KEY_REPEAT_DELAY_MS);
141
161
  while (Date.now() - started < ms) {
142
- asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
162
+ const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
143
163
  key,
144
164
  code,
145
165
  bubbles: true,
@@ -147,6 +167,8 @@ export async function holdKey(el, key, code, mods, ms) {
147
167
  repeat: true,
148
168
  ...mods,
149
169
  })));
170
+ if (down)
171
+ dispatchKeypress(el, key, code, mods, true);
150
172
  await sleep(Math.min(KEY_REPEAT_MS, Math.max(0, ms - (Date.now() - started))));
151
173
  }
152
174
  }
@@ -179,6 +201,8 @@ export async function pressCombo(el, keys, mods, holdMs) {
179
201
  const ok = dispatch('keydown', key);
180
202
  if (!ok)
181
203
  prevented = true;
204
+ if (ok)
205
+ dispatchKeypress(el, key, pressCode({}, key), held);
182
206
  closeModalOnEscape(el, key, ok);
183
207
  }
184
208
  if (holdMs > 0)
@@ -1,41 +1,5 @@
1
1
  import { SettleReason } from '@reticlehq/core';
2
- interface ActionEffect {
3
- dispatched: true;
4
- targetMatched: boolean;
5
- visible: boolean;
6
- enabled: boolean;
7
- defaultPrevented: boolean;
8
- focusMoved: string | null;
9
- valueChanged: boolean;
10
- domMutatedWithin: number;
11
- appeared?: string;
12
- occluded: boolean;
13
- occludedBy: string | null;
14
- scrolledIntoView: boolean;
15
- heldMs?: number;
16
- alreadyAtValue?: true;
17
- }
18
- interface ActionResult {
19
- ok: true;
20
- ref: string;
21
- action: string;
22
- dispatched: boolean;
23
- settled: boolean;
24
- settleReason: SettleReason | null;
25
- effect: ActionEffect;
26
- testid?: string;
27
- component?: string;
28
- role?: string;
29
- name?: string;
30
- nth?: number;
31
- of?: number;
32
- source?: {
33
- file: string;
34
- line: number;
35
- column?: number;
36
- };
37
- warning?: string;
38
- }
2
+ import { type ActionEffect, type ActionResult } from './action-report.js';
39
3
  export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
40
4
  export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
41
5
  export interface ActionStep {
@@ -58,6 +22,7 @@ interface SequenceStepResult {
58
22
  line: number;
59
23
  column?: number;
60
24
  };
25
+ secret?: true;
61
26
  warning?: string;
62
27
  }
63
28
  export declare function executeSequence(steps: ActionStep[]): Promise<{
@@ -1,17 +1,19 @@
1
- import { readTestId } from '../dom/addressing/testid-attr.js';
2
- import { ActionType, ActionWarning, clampHoldMs, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, QueryBy, SettleReason, } from '@reticlehq/core';
3
- import { elementsMatching } from '../dom/query.js';
1
+ import { ActionType, ActionWarning, clampHoldMs, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
2
+ import { isHiddenNativeOption } from '../dom/query.js';
3
+ import { isReticleUi } from '../dom/dom-ignore.js';
4
4
  import { asSyntheticInput } from './synthetic/synthetic-input.js';
5
5
  import { echoRef, refs } from '../dom/addressing/refs.js';
6
- import { dangerousActionContext, requiresDangerousConfirmation, submitControlFor, } from './danger-context.js';
7
- import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
8
- import { getAccessibleName, getRole, isVisible, getStates } from '../dom/a11y.js';
9
- import { elementHasHoverHandlers, identifyComponent } from '../registry/stores/adapters.js';
6
+ import { requiresDangerousConfirmation, assertOutwardAllowed, assertPaymentEntryAllowed, submitControlFor, } from './danger-context.js';
7
+ import { assertEditable, assertNotRichText, isAutofillSafeTarget, setNativeValue, } from './value-input.js';
8
+ import { isVisible, getStates } from '../dom/a11y.js';
9
+ import { elementHasHoverHandlers } from '../registry/stores/adapters.js';
10
10
  import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/realm.js';
11
- import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
11
+ import { asSettleBudget, nativeSetTimeout, settle } from '../timers/native/native-timers.js';
12
+ import { publishSettledField, watchActedField } from '../observers/field.js';
12
13
  import { AppearedText } from './appeared-text.js';
13
14
  import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
14
- import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
15
+ import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
16
+ import { anchorOf, result, visibleCanvases, } from './action-report.js';
15
17
  function asString(value, fallback = '') {
16
18
  return 'string' === typeof value ? value : fallback;
17
19
  }
@@ -23,61 +25,6 @@ function requireElement(ref) {
23
25
  throw new Error(`ref '${echoRef(ref)}' is not an HTML or SVG element`);
24
26
  return el;
25
27
  }
26
- function anchorOf(el) {
27
- const testid = readTestId(el) ?? undefined;
28
- const info = identifyComponent(el);
29
- const out = {};
30
- if (testid !== undefined)
31
- out.testid = testid;
32
- const component = info?.componentStack[0];
33
- if (component !== undefined)
34
- out.component = component;
35
- if (info?.source !== undefined)
36
- out.source = info.source;
37
- const role = getRole(el);
38
- const name = getAccessibleName(el);
39
- if (role.length > 0)
40
- out.role = role;
41
- if (name.length > 0)
42
- out.name = name;
43
- if (role.length > 0 && name.length > 0) {
44
- const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
45
- const at = same.indexOf(el);
46
- if (1 < same.length && 0 <= at) {
47
- out.nth = at;
48
- out.of = same.length;
49
- }
50
- }
51
- return out;
52
- }
53
- const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
54
- const base = {
55
- ok: true,
56
- ref,
57
- action,
58
- dispatched: true,
59
- settled,
60
- settleReason,
61
- effect,
62
- };
63
- if (anchor.testid !== undefined)
64
- base.testid = anchor.testid;
65
- if (anchor.component !== undefined)
66
- base.component = anchor.component;
67
- if (anchor.source !== undefined)
68
- base.source = anchor.source;
69
- if (anchor.role !== undefined)
70
- base.role = anchor.role;
71
- if (anchor.name !== undefined)
72
- base.name = anchor.name;
73
- if (anchor.nth !== undefined)
74
- base.nth = anchor.nth;
75
- if (anchor.of !== undefined)
76
- base.of = anchor.of;
77
- if (warning !== undefined)
78
- base.warning = warning;
79
- return base;
80
- };
81
28
  const FILL_LIKE = new Set([
82
29
  ActionType.FILL,
83
30
  ActionType.TYPE,
@@ -123,19 +70,31 @@ function assertActionAllowed(el, action, args) {
123
70
  (action === ActionType.PRESS && 'Enter' === pressKey(args));
124
71
  const dragTarget = action === ActionType.DRAG ? refs.resolve(dragTargetRef(args)) : null;
125
72
  const submitter = action === ActionType.PRESS && 'Enter' === pressKey(args) ? submitControlFor(el) : null;
126
- const sourceDangerous = requiresDangerousConfirmation(dangerousActionContext(el), getRole(el)) ||
127
- (submitter !== null &&
128
- requiresDangerousConfirmation(dangerousActionContext(submitter), getRole(submitter)));
129
- const targetDangerous = isActionTarget(dragTarget) &&
130
- requiresDangerousConfirmation(dangerousActionContext(dragTarget), getRole(dragTarget));
73
+ if (canTrigger)
74
+ assertOutwardAllowed([el, submitter, isActionTarget(dragTarget) ? dragTarget : null]);
75
+ if (action === ActionType.FILL || action === ActionType.TYPE)
76
+ assertPaymentEntryAllowed(el);
77
+ const sourceDangerous = requiresDangerousConfirmation(el) ||
78
+ (submitter !== null && requiresDangerousConfirmation(submitter));
79
+ const targetDangerous = isActionTarget(dragTarget) && requiresDangerousConfirmation(dragTarget);
131
80
  if (canTrigger &&
132
81
  (sourceDangerous || targetDangerous) &&
133
82
  args[DANGEROUS_ACTION_CONFIRM_ARG] !== true) {
134
83
  throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
135
84
  }
85
+ if (CLICK_LIKE.has(action) && el instanceof HTMLElement && isHiddenNativeOption(el)) {
86
+ throw new Error('cannot click a native <option> inside a visually-hidden <select> — open the dropdown and target the visible option');
87
+ }
136
88
  }
137
- import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, } from './actions-dom.js';
89
+ function optionValueFor(options, named) {
90
+ if (options.some((option) => option.value === named))
91
+ return named;
92
+ const byLabel = options.filter((option) => option.label.trim() === named.trim());
93
+ return 1 === byLabel.length ? (byLabel[0]?.value ?? named) : named;
94
+ }
95
+ import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, } from './actions-dom.js';
138
96
  import { scrollFrom } from './scroll.js';
97
+ import { clickHitsNoControl } from './click-target.js';
139
98
  function enabledOf(el) {
140
99
  return !getStates(el).includes(ElementState.DISABLED);
141
100
  }
@@ -209,7 +168,8 @@ async function dispatchOther(el, action, args) {
209
168
  'To empty a field on purpose use the `clear` action, or fill with an explicit "".');
210
169
  }
211
170
  assertEditable(el, 'fill');
212
- el.focus();
171
+ if (!isAutofillSafeTarget(el))
172
+ el.focus();
213
173
  return setNativeValue(el, args['value']);
214
174
  }
215
175
  assertNotRichText(el, 'fill');
@@ -235,8 +195,8 @@ async function dispatchOther(el, action, args) {
235
195
  if (typeof args['value'] !== 'string') {
236
196
  throw new Error("select requires a string `value` — pass it nested, as args: { value: '…' }.");
237
197
  }
238
- const wanted = args['value'];
239
198
  const options = Array.from(el.options);
199
+ const wanted = optionValueFor(options, args['value']);
240
200
  if (!options.some((option) => option.value === wanted)) {
241
201
  const available = options.map((option) => `${option.value} (${option.label})`).join(', ');
242
202
  throw new Error(`no <option> with value '${echoRef(wanted)}' — available: ${0 === options.length ? '(none)' : available}`);
@@ -287,6 +247,8 @@ async function dispatchOther(el, action, args) {
287
247
  const code = pressCode(args, key);
288
248
  const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
289
249
  closeModalOnEscape(el, key, down);
250
+ if (down)
251
+ dispatchKeypress(el, key, code, mods);
290
252
  const hold = clampHoldMs(args['holdMs']);
291
253
  if (hold > 0)
292
254
  await holdKey(el, key, code, mods, hold);
@@ -344,24 +306,22 @@ async function dispatchOther(el, action, args) {
344
306
  throw new Error(`unknown action '${action}'`);
345
307
  }
346
308
  }
347
- function asSettleBudget(raw) {
348
- if ('number' !== typeof raw || !Number.isFinite(raw))
349
- return undefined;
350
- return Math.max(0, Math.min(raw, FRAME_BUDGET_MS_CEILING));
351
- }
352
- const FRAME_BUDGET_MS_CEILING = 200;
353
309
  export async function executeAction(ref, action, args = {}) {
354
310
  const named = 0 < ref.length;
355
311
  const el = isReflessDocumentPress(ref, action, args) ? focusedOrDocument() : requireElement(ref);
356
312
  if (named)
357
313
  assertActionAllowed(el, action, args);
314
+ if (named)
315
+ assertNotLeaving(el);
358
316
  const anchor = named ? anchorOf(el) : {};
359
317
  const visible = isVisible(el);
360
318
  const enabled = enabledOf(el);
361
319
  const prevFocus = activeRef(el);
362
320
  const valueBefore = valueOf(el);
321
+ const actedField = watchActedField(el);
363
322
  const geometry = CLICK_LIKE.has(action) ? clickGeometry(el) : NO_GEOMETRY;
364
323
  const alreadyAtValue = alreadyAtCheckedState(el, action);
324
+ const hitsNoControl = clickHitsNoControl(el, action);
365
325
  let mutated = 0;
366
326
  const said = new AppearedText();
367
327
  const obs = new MutationObserver((records) => {
@@ -389,9 +349,11 @@ export async function executeAction(ref, action, args = {}) {
389
349
  settled = outcome.settled;
390
350
  settleReason = outcome.settled ? null : SettleReason.TIMEOUT;
391
351
  obs.disconnect();
352
+ actedField?.stop();
392
353
  }
393
354
  assertAriaToggleLanded(el, action, alreadyAtValue);
394
355
  const valueAfter = valueOf(el);
356
+ publishSettledField(el, actedField);
395
357
  const nextFocus = activeRef(el);
396
358
  const effect = {
397
359
  dispatched: true,
@@ -401,13 +363,18 @@ export async function executeAction(ref, action, args = {}) {
401
363
  defaultPrevented,
402
364
  focusMoved: prevFocus !== nextFocus ? `${prevFocus ?? 'null'}->${nextFocus ?? 'null'}` : null,
403
365
  valueChanged: isFillLike(action) ? valueBefore !== valueAfter : false,
366
+ ...(ActionType.FILL === action && isAutofillSafeTarget(el) ? { autofillSafe: true } : {}),
404
367
  domMutatedWithin: mutated,
405
368
  ...said.effect(valueAfter),
406
369
  occluded: geometry.occluded,
407
370
  occludedBy: geometry.occludedBy,
408
371
  scrolledIntoView: geometry.scrolledIntoView,
372
+ ...(geometry.occluderName === undefined ? {} : { occluderName: geometry.occluderName }),
373
+ ...(true === geometry.noSize ? { noSize: true } : {}),
374
+ ...(true === geometry.offViewport ? { offViewport: true } : {}),
409
375
  ...(heldMs > 0 ? { heldMs } : {}),
410
376
  ...(alreadyAtValue ? { alreadyAtValue: true } : {}),
377
+ ...canvasesOnScreen(),
411
378
  };
412
379
  const warning = !named
413
380
  ? ActionWarning.GLOBAL_PRESS
@@ -415,8 +382,15 @@ export async function executeAction(ref, action, args = {}) {
415
382
  ? ActionWarning.CLICK_OCCLUDED
416
383
  : action === ActionType.HOVER && elementHasHoverHandlers(el)
417
384
  ? ActionWarning.HOVER_NATIVE_ENTER_LEAVE
418
- : undefined;
419
- return result(ref, action, effect, settled, settleReason, anchor, warning);
385
+ : hitsNoControl
386
+ ? ActionWarning.CLICK_NOT_A_CONTROL
387
+ : undefined;
388
+ const done = result(ref, action, effect, settled, settleReason, anchor, warning);
389
+ return isReticleUi(el) ? { ...done, reticleUi: true } : done;
390
+ }
391
+ function canvasesOnScreen() {
392
+ const canvases = visibleCanvases(document);
393
+ return canvases > 0 ? { canvases } : {};
420
394
  }
421
395
  const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
422
396
  export async function executeSequence(steps) {
@@ -442,6 +416,8 @@ export async function executeSequence(steps) {
442
416
  stepBase.name = res.name;
443
417
  if (res.source !== undefined)
444
418
  stepBase.source = res.source;
419
+ if (res.secret !== undefined)
420
+ stepBase.secret = res.secret;
445
421
  if (res.warning !== undefined)
446
422
  stepBase.warning = res.warning;
447
423
  stepResults.push(stepBase);
@@ -1,4 +1,5 @@
1
- import { isIgnored } from '../dom/dom-ignore.js';
1
+ import { isWordsIgnored } from '../dom/dom-ignore.js';
2
+ import { isSensitiveField } from '../dom/a11y.js';
2
3
  const APPEARED_MAX = 200;
3
4
  const JOIN = ' | ';
4
5
  const TEXT_NODE = 3;
@@ -44,7 +45,9 @@ export class AppearedText {
44
45
  return this.#length > APPEARED_MAX;
45
46
  }
46
47
  #add(raw, owner) {
47
- if (null !== owner && isIgnored(owner))
48
+ if (null !== owner && isWordsIgnored(owner))
49
+ return;
50
+ if (null !== owner && isSensitiveField(owner))
48
51
  return;
49
52
  const text = (raw ?? '').replace(/\s+/g, ' ').trim();
50
53
  if (0 === text.length)
@@ -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,4 +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
+ export declare function setOutwardPolicy(allowed: readonly string[] | undefined): void;
6
+ export declare function assertOutwardAllowed(targets: readonly (ActionTarget | null)[]): void;
7
+ export declare function assertPaymentEntryAllowed(el: ActionTarget): void;