@reticlehq/browser 2.14.0 → 3.2.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 (133) hide show
  1. package/dist/actions/actions-dom.d.ts +9 -0
  2. package/dist/actions/actions-dom.js +81 -28
  3. package/dist/actions/actions-press.d.ts +4 -0
  4. package/dist/actions/actions-press.js +46 -0
  5. package/dist/actions/actions.d.ts +1 -0
  6. package/dist/actions/actions.js +50 -13
  7. package/dist/actions/appeared-text.js +6 -0
  8. package/dist/actions/scroll.d.ts +8 -1
  9. package/dist/actions/scroll.js +28 -10
  10. package/dist/commands/commands.js +10 -3
  11. package/dist/dom/a11y.js +2 -2
  12. package/dist/dom/desktop-capture.js +1 -1
  13. package/dist/dom/dom-ignore.js +1 -1
  14. package/dist/dom/occlusion.js +31 -5
  15. package/dist/dom/paint-context.d.ts +6 -0
  16. package/dist/dom/paint-context.js +38 -0
  17. package/dist/dom/query.js +24 -7
  18. package/dist/dom/snapshot.js +1 -1
  19. package/dist/edit-epoch.d.ts +1 -1
  20. package/dist/edit-epoch.js +1 -1
  21. package/dist/index.d.ts +4 -5
  22. package/dist/index.js +3 -5
  23. package/dist/observers/animation.js +1 -1
  24. package/dist/observers/blind-spots.js +1 -1
  25. package/dist/observers/departure.js +16 -8
  26. package/dist/observers/dialogs.js +1 -1
  27. package/dist/observers/dom.js +28 -8
  28. package/dist/observers/download.js +1 -1
  29. package/dist/observers/health.js +1 -1
  30. package/dist/observers/{network-body.js → net-detail/network-body.js} +2 -2
  31. package/dist/observers/network-stream.js +2 -2
  32. package/dist/observers/network.d.ts +1 -1
  33. package/dist/observers/network.js +51 -10
  34. package/dist/observers/scroll.js +2 -2
  35. package/dist/presenter/chrome/layers.d.ts +2 -0
  36. package/dist/presenter/chrome/layers.js +2 -0
  37. package/dist/presenter/chrome/log-kinds.d.ts +12 -0
  38. package/dist/presenter/chrome/log-kinds.js +7 -0
  39. package/dist/presenter/{presenter-effects.js → chrome/presenter-effects.js} +1 -1
  40. package/dist/presenter/{presenter-log.d.ts → chrome/presenter-log.d.ts} +3 -12
  41. package/dist/presenter/{presenter-log.js → chrome/presenter-log.js} +4 -9
  42. package/dist/presenter/chrome/presenter-safe-html.d.ts +2 -0
  43. package/dist/presenter/chrome/presenter-safe-html.js +16 -0
  44. package/dist/presenter/{presenter-tally.js → chrome/presenter-tally.js} +1 -1
  45. package/dist/presenter/{presenter-heroicons-data.d.ts → icons/presenter-heroicons-data.d.ts} +1 -0
  46. package/dist/presenter/{presenter-heroicons-data.js → icons/presenter-heroicons-data.js} +1 -0
  47. package/dist/presenter/{presenter-icons.d.ts → icons/presenter-icons.d.ts} +1 -0
  48. package/dist/presenter/{presenter-icons.js → icons/presenter-icons.js} +1 -0
  49. package/dist/presenter/presenter-account-styles.d.ts +1 -0
  50. package/dist/presenter/presenter-account-styles.js +99 -0
  51. package/dist/presenter/presenter-account.d.ts +40 -0
  52. package/dist/presenter/presenter-account.js +178 -0
  53. package/dist/presenter/presenter-config.d.ts +9 -2
  54. package/dist/presenter/presenter-config.js +3 -1
  55. package/dist/presenter/presenter-controls.d.ts +1 -6
  56. package/dist/presenter/presenter-controls.js +7 -2
  57. package/dist/presenter/presenter-dock-layout.js +1 -1
  58. package/dist/presenter/presenter-drag.d.ts +1 -19
  59. package/dist/presenter/presenter-drag.js +3 -50
  60. package/dist/presenter/presenter-glow.js +1 -1
  61. package/dist/presenter/presenter-hud-position.d.ts +18 -0
  62. package/dist/presenter/presenter-hud-position.js +50 -0
  63. package/dist/presenter/presenter-offer.d.ts +25 -0
  64. package/dist/presenter/presenter-offer.js +87 -0
  65. package/dist/presenter/presenter-report-styles.js +20 -1
  66. package/dist/presenter/presenter-report.d.ts +2 -1
  67. package/dist/presenter/presenter-report.js +31 -23
  68. package/dist/presenter/presenter-run-state.d.ts +1 -1
  69. package/dist/presenter/presenter-run-state.js +1 -1
  70. package/dist/presenter/presenter-settings-styles.js +2 -1
  71. package/dist/presenter/presenter-settings.d.ts +8 -0
  72. package/dist/presenter/presenter-settings.js +58 -3
  73. package/dist/presenter/presenter-shell-styles.js +33 -15
  74. package/dist/presenter/presenter-shell.d.ts +8 -0
  75. package/dist/presenter/presenter-shell.js +47 -4
  76. package/dist/presenter/presenter-styles.d.ts +2 -1
  77. package/dist/presenter/presenter-styles.js +8 -3
  78. package/dist/presenter/presenter-workspace.d.ts +4 -0
  79. package/dist/presenter/presenter-workspace.js +29 -2
  80. package/dist/presenter/presenter.d.ts +3 -3
  81. package/dist/presenter/presenter.js +28 -14
  82. package/dist/presenter/tour/tour-view.d.ts +33 -0
  83. package/dist/presenter/tour/tour-view.js +192 -0
  84. package/dist/presenter/tour/tour.d.ts +25 -0
  85. package/dist/presenter/tour/tour.js +264 -0
  86. package/dist/recorder/recorder.js +6 -0
  87. package/dist/registry/declared-channels.d.ts +2 -0
  88. package/dist/registry/declared-channels.js +14 -0
  89. package/dist/registry/declared-platform.d.ts +4 -0
  90. package/dist/registry/declared-platform.js +34 -0
  91. package/dist/registry/emitter.js +1 -1
  92. package/dist/registry/store-adapters.js +1 -1
  93. package/dist/registry/stores.js +1 -1
  94. package/dist/reticle-presenter-helpers.js +10 -6
  95. package/dist/reticle-singleton.d.ts +2 -0
  96. package/dist/reticle-singleton.js +3 -0
  97. package/dist/reticle.js +99 -35
  98. package/dist/review/annotator-styles.js +1 -1
  99. package/dist/review/annotator.js +2 -2
  100. package/dist/review/mark-anchor.js +3 -3
  101. package/dist/transport/transport.js +2 -2
  102. package/package.json +5 -4
  103. package/dist/presenter/presenter-test-helpers.d.ts +0 -14
  104. package/dist/presenter/presenter-test-helpers.js +0 -39
  105. /package/dist/actions/{synthetic-input.d.ts → synthetic/synthetic-input.d.ts} +0 -0
  106. /package/dist/actions/{synthetic-input.js → synthetic/synthetic-input.js} +0 -0
  107. /package/dist/dom/{auto-anchor.d.ts → addressing/auto-anchor.d.ts} +0 -0
  108. /package/dist/dom/{auto-anchor.js → addressing/auto-anchor.js} +0 -0
  109. /package/dist/dom/{refs.d.ts → addressing/refs.d.ts} +0 -0
  110. /package/dist/dom/{refs.js → addressing/refs.js} +0 -0
  111. /package/dist/dom/{source.d.ts → addressing/source.d.ts} +0 -0
  112. /package/dist/dom/{source.js → addressing/source.js} +0 -0
  113. /package/dist/observers/{network-body.d.ts → net-detail/network-body.d.ts} +0 -0
  114. /package/dist/observers/{network-redact.d.ts → net-detail/network-redact.d.ts} +0 -0
  115. /package/dist/observers/{network-redact.js → net-detail/network-redact.js} +0 -0
  116. /package/dist/presenter/{overlay.d.ts → chrome/overlay.d.ts} +0 -0
  117. /package/dist/presenter/{overlay.js → chrome/overlay.js} +0 -0
  118. /package/dist/presenter/{presenter-brand.d.ts → chrome/presenter-brand.d.ts} +0 -0
  119. /package/dist/presenter/{presenter-brand.js → chrome/presenter-brand.js} +0 -0
  120. /package/dist/presenter/{presenter-effects.d.ts → chrome/presenter-effects.d.ts} +0 -0
  121. /package/dist/presenter/{presenter-hud-chrome.d.ts → chrome/presenter-hud-chrome.d.ts} +0 -0
  122. /package/dist/presenter/{presenter-hud-chrome.js → chrome/presenter-hud-chrome.js} +0 -0
  123. /package/dist/presenter/{presenter-report-copy.d.ts → chrome/presenter-report-copy.d.ts} +0 -0
  124. /package/dist/presenter/{presenter-report-copy.js → chrome/presenter-report-copy.js} +0 -0
  125. /package/dist/presenter/{presenter-tally.d.ts → chrome/presenter-tally.d.ts} +0 -0
  126. /package/dist/presenter/{presenter-verbs.d.ts → chrome/presenter-verbs.d.ts} +0 -0
  127. /package/dist/presenter/{presenter-verbs.js → chrome/presenter-verbs.js} +0 -0
  128. /package/dist/registry/{adapters.d.ts → stores/adapters.d.ts} +0 -0
  129. /package/dist/registry/{adapters.js → stores/adapters.js} +0 -0
  130. /package/dist/timers/{native-console.d.ts → console/native-console.d.ts} +0 -0
  131. /package/dist/timers/{native-console.js → console/native-console.js} +0 -0
  132. /package/dist/timers/{native-timers.d.ts → native/native-timers.d.ts} +0 -0
  133. /package/dist/timers/{native-timers.js → native/native-timers.js} +0 -0
@@ -4,6 +4,7 @@ interface ClickGeometry {
4
4
  scrolledIntoView: boolean;
5
5
  }
6
6
  export declare const NO_GEOMETRY: ClickGeometry;
7
+ export declare function mouseEventFor(el: Element, type: string, init?: MouseEventInit): MouseEvent;
7
8
  export declare function fireClickSequence(el: HTMLElement, hold?: {
8
9
  ms: number;
9
10
  sleep: (ms: number) => Promise<void>;
@@ -16,4 +17,12 @@ export declare function clickGeometry(el: HTMLElement): ClickGeometry;
16
17
  export declare function firePointer(el: Element, type: string, relatedTarget?: EventTarget | null): void;
17
18
  export declare function firePointerNonBubbling(el: Element, type: string, relatedTarget?: EventTarget | null): void;
18
19
  export declare function dragElement(source: HTMLElement, target: HTMLElement | null, data: unknown): Promise<boolean>;
20
+ export declare function fireTapSequence(el: HTMLElement, hold: {
21
+ ms: number;
22
+ sleep: (ms: number) => Promise<void>;
23
+ now: () => number;
24
+ } | undefined): Promise<{
25
+ prevented: boolean;
26
+ heldMs: number;
27
+ }>;
19
28
  export {};
@@ -1,17 +1,26 @@
1
- import { refs } from '../dom/refs.js';
1
+ import { refs } from '../dom/addressing/refs.js';
2
2
  import { hitTestOccluder } from '../dom/occlusion.js';
3
- import { nativeFrame } from '../timers/native-timers.js';
4
- import { asSyntheticInput } from './synthetic-input.js';
3
+ import { nativeFrame } from '../timers/native/native-timers.js';
4
+ import { asSyntheticInput } from './synthetic/synthetic-input.js';
5
5
  export const NO_GEOMETRY = {
6
6
  occluded: false,
7
7
  occludedBy: null,
8
8
  scrolledIntoView: false,
9
9
  };
10
+ export function mouseEventFor(el, type, init = {}) {
11
+ return new MouseEvent(type, { ...init, view: el.ownerDocument.defaultView });
12
+ }
13
+ function pointerEventFor(el, type, init) {
14
+ const full = { ...init, view: el.ownerDocument.defaultView };
15
+ return 'function' === typeof PointerEvent
16
+ ? new PointerEvent(type, full)
17
+ : new MouseEvent(type, full);
18
+ }
10
19
  export async function fireClickSequence(el, hold) {
11
20
  const doc = el.ownerDocument;
12
21
  const from = doc.activeElement ?? doc.body;
13
22
  firePointer(el, 'pointerdown', from);
14
- asSyntheticInput(() => el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true })));
23
+ asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mousedown', { bubbles: true, cancelable: true })));
15
24
  if (el.tabIndex >= 0 && 'function' === typeof el.focus)
16
25
  el.focus();
17
26
  let heldMs = 0;
@@ -21,8 +30,8 @@ export async function fireClickSequence(el, hold) {
21
30
  heldMs = hold.now() - startedAt;
22
31
  }
23
32
  firePointer(el, 'pointerup', from);
24
- asSyntheticInput(() => el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true })));
25
- const notPrevented = asSyntheticInput(() => el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })));
33
+ asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mouseup', { bubbles: true, cancelable: true })));
34
+ const notPrevented = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click', { bubbles: true, cancelable: true })));
26
35
  return { prevented: !notPrevented, heldMs };
27
36
  }
28
37
  function isMeasurable(rect) {
@@ -57,24 +66,10 @@ export function clickGeometry(el) {
57
66
  return { ...hitTest(el, rect), scrolledIntoView };
58
67
  }
59
68
  export function firePointer(el, type, relatedTarget = null) {
60
- asSyntheticInput(() => {
61
- if ('function' === typeof PointerEvent) {
62
- el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
63
- }
64
- else {
65
- el.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
66
- }
67
- });
69
+ asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: true, cancelable: true, relatedTarget })));
68
70
  }
69
71
  export function firePointerNonBubbling(el, type, relatedTarget = null) {
70
- asSyntheticInput(() => {
71
- if ('function' === typeof PointerEvent) {
72
- el.dispatchEvent(new PointerEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
73
- }
74
- else {
75
- el.dispatchEvent(new MouseEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
76
- }
77
- });
72
+ asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: false, cancelable: true, relatedTarget })));
78
73
  }
79
74
  function makeDataTransfer(data) {
80
75
  if (typeof DataTransfer !== 'function')
@@ -117,12 +112,9 @@ export async function dragElement(source, target, data) {
117
112
  button: 0,
118
113
  ...(related !== undefined ? { relatedTarget: related } : {}),
119
114
  };
120
- if ('function' === typeof PointerEvent && type.startsWith('pointer')) {
121
- el.dispatchEvent(new PointerEvent(type, { ...init, pointerId: 1, isPrimary: true }));
122
- }
123
- else {
124
- el.dispatchEvent(new MouseEvent(type, init));
125
- }
115
+ el.dispatchEvent(type.startsWith('pointer')
116
+ ? pointerEventFor(el, type, { ...init, pointerId: 1, isPrimary: true })
117
+ : mouseEventFor(el, type, init));
126
118
  };
127
119
  const sourceRect = source.getBoundingClientRect();
128
120
  const destRect = target !== null ? dest.getBoundingClientRect() : null;
@@ -176,3 +168,64 @@ export async function dragElement(source, target, data) {
176
168
  }
177
169
  return dropPrevented;
178
170
  }
171
+ export async function fireTapSequence(el, hold) {
172
+ const touches = touchListFor(el);
173
+ firePointerTouch(el, 'pointerdown');
174
+ asSyntheticInput(() => el.dispatchEvent(makeTouchEvent('touchstart', touches)));
175
+ if (el.tabIndex >= 0 && 'function' === typeof el.focus)
176
+ el.focus();
177
+ let heldMs = 0;
178
+ if (hold !== undefined && hold.ms > 0) {
179
+ const started = hold.now();
180
+ await hold.sleep(hold.ms);
181
+ heldMs = hold.now() - started;
182
+ }
183
+ asSyntheticInput(() => el.dispatchEvent(makeTouchEvent('touchend', [])));
184
+ firePointerTouch(el, 'pointerup');
185
+ const clicked = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click', { bubbles: true, cancelable: true })));
186
+ return { prevented: !clicked, heldMs };
187
+ }
188
+ function firePointerTouch(el, type) {
189
+ asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, {
190
+ bubbles: true,
191
+ cancelable: true,
192
+ pointerType: 'touch',
193
+ isPrimary: true,
194
+ width: 23,
195
+ height: 23,
196
+ })));
197
+ }
198
+ function touchListFor(el) {
199
+ if ('function' !== typeof Touch || 'function' !== typeof el.getBoundingClientRect)
200
+ return [];
201
+ const rect = el.getBoundingClientRect();
202
+ try {
203
+ return [
204
+ new Touch({
205
+ identifier: 0,
206
+ target: el,
207
+ clientX: rect.left + rect.width / 2,
208
+ clientY: rect.top + rect.height / 2,
209
+ }),
210
+ ];
211
+ }
212
+ catch {
213
+ return [];
214
+ }
215
+ }
216
+ function makeTouchEvent(type, touches) {
217
+ if ('function' === typeof TouchEvent) {
218
+ try {
219
+ return new TouchEvent(type, {
220
+ bubbles: true,
221
+ cancelable: true,
222
+ touches,
223
+ targetTouches: touches,
224
+ changedTouches: touches,
225
+ });
226
+ }
227
+ catch {
228
+ }
229
+ }
230
+ return new Event(type, { bubbles: true, cancelable: true });
231
+ }
@@ -8,3 +8,7 @@ export declare function pressModifiers(args: Record<string, unknown>): {
8
8
  export declare function pressCode(args: Record<string, unknown>, key: string): string;
9
9
  export declare function focusedOrDocument(): HTMLElement;
10
10
  export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
11
+ export declare function pressKeys(args: Record<string, unknown>): string[];
12
+ export declare function holdKey(el: HTMLElement, key: string, code: string, mods: Record<string, boolean>, ms: number): Promise<void>;
13
+ export declare function pressCombo(el: HTMLElement, keys: readonly string[], mods: Record<string, boolean>, holdMs: number): Promise<boolean>;
14
+ export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
@@ -1,4 +1,7 @@
1
1
  import { ActionType, isGlobalPress, pressKeyFromArgs } from '@reticlehq/core';
2
+ import { asSyntheticInput } from './synthetic/synthetic-input.js';
3
+ import { nativeSetTimeout } from '../timers/native/native-timers.js';
4
+ const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
2
5
  import { isHtmlElement } from '../dom/realm.js';
3
6
  function asString(value, fallback = '') {
4
7
  return 'string' === typeof value ? value : fallback;
@@ -75,3 +78,46 @@ export function focusedOrDocument() {
75
78
  export function isReflessDocumentPress(ref, action, args) {
76
79
  return 0 === ref.length && isGlobalPress(args) && ActionType.PRESS === action;
77
80
  }
81
+ export function pressKeys(args) {
82
+ const raw = args['keys'];
83
+ if (!Array.isArray(raw))
84
+ return [];
85
+ return raw.map((k) => asString(k)).filter((k) => k.length > 0);
86
+ }
87
+ const KEY_REPEAT_MS = 33;
88
+ const KEY_REPEAT_DELAY_MS = 500;
89
+ export async function holdKey(el, key, code, mods, ms) {
90
+ const started = Date.now();
91
+ if (ms > KEY_REPEAT_DELAY_MS)
92
+ await sleep(KEY_REPEAT_DELAY_MS);
93
+ while (Date.now() - started < ms) {
94
+ asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', {
95
+ key,
96
+ code,
97
+ bubbles: true,
98
+ cancelable: true,
99
+ repeat: true,
100
+ ...mods,
101
+ })));
102
+ await sleep(Math.min(KEY_REPEAT_MS, Math.max(0, ms - (Date.now() - started))));
103
+ }
104
+ }
105
+ export async function pressCombo(el, keys, mods, holdMs) {
106
+ let prevented = false;
107
+ for (const key of keys) {
108
+ const code = pressCode({}, key);
109
+ const ok = asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
110
+ if (!ok)
111
+ prevented = true;
112
+ }
113
+ if (holdMs > 0)
114
+ await sleep(holdMs);
115
+ for (const key of [...keys].reverse()) {
116
+ const code = pressCode({}, key);
117
+ asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
118
+ }
119
+ return prevented;
120
+ }
121
+ export const ZOOM_NEEDS_REAL_BROWSER_MSG = 'cannot zoom from inside the page — CSS zoom changes how it LOOKS without changing the layout ' +
122
+ 'viewport, visualViewport or media queries, so a layout that breaks at 200% would be reported as ' +
123
+ 'checked and passing. Drive a real browser (reticle drive <url>, or reticle_lease) and zoom there.';
@@ -35,6 +35,7 @@ interface ActionResult {
35
35
  warning?: string;
36
36
  }
37
37
  export declare const MAX_HOLD_MS = 30000;
38
+ export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
38
39
  export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
39
40
  export declare function dispatchWebMcp(tool: string, params: Record<string, unknown>, confirmDangerous?: boolean): Promise<unknown>;
40
41
  export interface ActionStep {
@@ -1,14 +1,14 @@
1
1
  import { ActionType, ActionWarning, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
2
- import { asSyntheticInput } from './synthetic-input.js';
3
- import { echoRef, refs } from '../dom/refs.js';
2
+ import { asSyntheticInput } from './synthetic/synthetic-input.js';
3
+ import { echoRef, refs } from '../dom/addressing/refs.js';
4
4
  import { dangerousActionContext, requiresDangerousConfirmation, submitControlFor, } from './danger-context.js';
5
5
  import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
6
6
  import { getAccessibleName, getRole, isVisible, getStates } from '../dom/a11y.js';
7
- import { elementHasHoverHandlers, identifyComponent } from '../registry/adapters.js';
7
+ import { elementHasHoverHandlers, identifyComponent } from '../registry/stores/adapters.js';
8
8
  import { isForm, isHtmlElement, isInput, isSelect, isTextArea } from '../dom/realm.js';
9
- import { nativeSetTimeout, settle } from '../timers/native-timers.js';
9
+ import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
10
10
  import { AppearedText } from './appeared-text.js';
11
- import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, } from './actions-press.js';
11
+ import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, } from './actions-press.js';
12
12
  function asString(value, fallback = '') {
13
13
  return 'string' === typeof value ? value : fallback;
14
14
  }
@@ -124,7 +124,8 @@ function assertActionAllowed(el, action, args) {
124
124
  throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
125
125
  }
126
126
  }
127
- import { NO_GEOMETRY, fireClickSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, } from './actions-dom.js';
127
+ import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, } from './actions-dom.js';
128
+ import { scrollFrom } from './scroll.js';
128
129
  function enabledOf(el) {
129
130
  return !getStates(el).includes(ElementState.DISABLED);
130
131
  }
@@ -151,21 +152,31 @@ async function dispatchFor(el, action, args) {
151
152
  const hold = clampHold(args['holdMs']);
152
153
  return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
153
154
  }
155
+ if (ActionType.TAP === action) {
156
+ const hold = clampHold(args['holdMs']);
157
+ return await fireTapSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
158
+ }
154
159
  return { prevented: await dispatchOther(el, action, args), heldMs: 0 };
155
160
  }
161
+ export const ZOOM_NEEDS_REAL_BROWSER_MSG = 'cannot zoom from inside the page — CSS zoom changes how it LOOKS without changing the layout ' +
162
+ 'viewport, visualViewport or media queries, so a layout that breaks at 200% would be reported as ' +
163
+ 'checked and passing. Drive a real browser (reticle drive <url>, or reticle_lease) and zoom there.';
164
+ function asFiniteNumber(raw) {
165
+ return 'number' === typeof raw && Number.isFinite(raw) ? raw : undefined;
166
+ }
156
167
  async function dispatchOther(el, action, args) {
157
168
  switch (action) {
158
169
  case ActionType.DBLCLICK:
159
- return !asSyntheticInput(() => el.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true })));
170
+ return !asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'dblclick', { bubbles: true, cancelable: true })));
160
171
  case ActionType.HOVER: {
161
172
  const doc = el.ownerDocument;
162
173
  const from = doc.activeElement ?? doc.body;
163
174
  firePointer(el, 'pointerover', from);
164
- el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, relatedTarget: from }));
175
+ el.dispatchEvent(mouseEventFor(el, 'mouseover', { bubbles: true, relatedTarget: from }));
165
176
  firePointerNonBubbling(el, 'pointerenter', from);
166
- el.dispatchEvent(new MouseEvent('mouseenter', { relatedTarget: from }));
177
+ el.dispatchEvent(mouseEventFor(el, 'mouseenter', { relatedTarget: from }));
167
178
  firePointer(el, 'pointermove', from);
168
- const moved = el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true }));
179
+ const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true }));
169
180
  const holdMs = clampHold(args['holdMs']);
170
181
  if (holdMs > 0)
171
182
  await sleep(holdMs);
@@ -241,7 +252,11 @@ async function dispatchOther(el, action, args) {
241
252
  }
242
253
  if (alreadyAtCheckedState(el, action))
243
254
  return false;
244
- const event = new MouseEvent('click', { bubbles: true, cancelable: true, composed: true });
255
+ const event = mouseEventFor(el, 'click', {
256
+ bubbles: true,
257
+ cancelable: true,
258
+ composed: true,
259
+ });
245
260
  const notPrevented = asSyntheticInput(() => el.dispatchEvent(event));
246
261
  return !notPrevented || event.defaultPrevented;
247
262
  }
@@ -253,16 +268,31 @@ async function dispatchOther(el, action, args) {
253
268
  return false;
254
269
  }
255
270
  case ActionType.PRESS: {
271
+ const mods = pressModifiers(args);
272
+ const combo = pressKeys(args);
273
+ if (combo.length > 0)
274
+ return await pressCombo(el, combo, mods, clampHold(args['holdMs']));
256
275
  const key = pressKey(args);
257
276
  const code = pressCode(args, key);
258
- const mods = pressModifiers(args);
259
277
  const down = asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
278
+ const hold = clampHold(args['holdMs']);
279
+ if (hold > 0)
280
+ await holdKey(el, key, code, mods, hold);
260
281
  asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
261
282
  return !down;
262
283
  }
263
284
  case ActionType.SCROLL_INTO_VIEW:
264
285
  el.scrollIntoView();
265
286
  return false;
287
+ case ActionType.ZOOM:
288
+ throw new Error(ZOOM_NEEDS_REAL_BROWSER_MSG);
289
+ case ActionType.SCROLL: {
290
+ const dy = asFiniteNumber(args['dy']);
291
+ const dx = asFiniteNumber(args['dx']);
292
+ const fraction = asFiniteNumber(args['fraction']);
293
+ scrollFrom(el, dy, fraction, dx);
294
+ return false;
295
+ }
266
296
  case ActionType.UPLOAD: {
267
297
  if (!isInput(el) || el.type !== 'file') {
268
298
  throw new Error('upload target must be a <input type="file">');
@@ -302,6 +332,12 @@ async function dispatchOther(el, action, args) {
302
332
  throw new Error(`unknown action '${action}'`);
303
333
  }
304
334
  }
335
+ function asSettleBudget(raw) {
336
+ if ('number' !== typeof raw || !Number.isFinite(raw))
337
+ return undefined;
338
+ return Math.max(0, Math.min(raw, FRAME_BUDGET_MS_CEILING));
339
+ }
340
+ const FRAME_BUDGET_MS_CEILING = 200;
305
341
  export async function executeAction(ref, action, args = {}) {
306
342
  const named = 0 < ref.length;
307
343
  const el = isReflessDocumentPress(ref, action, args) ? focusedOrDocument() : requireElement(ref);
@@ -336,7 +372,8 @@ export async function executeAction(ref, action, args = {}) {
336
372
  heldMs = outcome.heldMs;
337
373
  }
338
374
  finally {
339
- const outcome = await settle();
375
+ const budget = asSettleBudget(args['settleMs']);
376
+ const outcome = await settle(budget);
340
377
  settled = outcome.settled;
341
378
  settleReason = outcome.settled ? null : SettleReason.TIMEOUT;
342
379
  obs.disconnect();
@@ -6,6 +6,10 @@ const HAS_LETTER = /\p{L}/u;
6
6
  function saysSomething(text) {
7
7
  return HAS_LETTER.test(text);
8
8
  }
9
+ const MAX_MESSAGE_ELEMENTS = 3;
10
+ function isRenderedView(node) {
11
+ return node instanceof Element && node.querySelectorAll('*').length > MAX_MESSAGE_ELEMENTS;
12
+ }
9
13
  export class AppearedText {
10
14
  #seen = new Set();
11
15
  #length = 0;
@@ -20,6 +24,8 @@ export class AppearedText {
20
24
  for (const node of record.addedNodes) {
21
25
  if (this.#full())
22
26
  return;
27
+ if (isRenderedView(node))
28
+ continue;
23
29
  const owner = TEXT_NODE === node.nodeType ? node.parentElement : elementOf(node);
24
30
  this.#add(node.textContent, owner);
25
31
  }
@@ -4,6 +4,13 @@ interface ScrollResult {
4
4
  scrollHeight: number;
5
5
  clientHeight: number;
6
6
  atEnd: boolean;
7
+ atStart: boolean;
8
+ scrollLeft: number;
9
+ scrollWidth: number;
10
+ clientWidth: number;
11
+ atLeftEnd: boolean;
12
+ atRightEnd: boolean;
7
13
  }
8
- export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number): ScrollResult;
14
+ export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
15
+ export declare function scrollFrom(base: Element | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
9
16
  export {};
@@ -1,36 +1,54 @@
1
- import { refs } from '../dom/refs.js';
1
+ import { refs } from '../dom/addressing/refs.js';
2
2
  const FALLBACK_STEP_PX = 400;
3
3
  const VIEWPORT_FRACTION = 0.8;
4
- function nearestScrollable(el) {
4
+ function nearestScrollable(el, axis) {
5
5
  let cur = el;
6
6
  while (cur !== null && cur !== document.body) {
7
- const oy = getComputedStyle(cur).overflowY;
8
- if (('auto' === oy || 'scroll' === oy) && cur.scrollHeight > cur.clientHeight)
7
+ const style = getComputedStyle(cur);
8
+ const overflow = 'x' === axis ? style.overflowX : style.overflowY;
9
+ const scrollable = 'x' === axis ? cur.scrollWidth > cur.clientWidth : cur.scrollHeight > cur.clientHeight;
10
+ if (('auto' === overflow || 'scroll' === overflow) && scrollable)
9
11
  return cur;
10
12
  cur = cur.parentElement;
11
13
  }
12
14
  return document.scrollingElement ?? document.documentElement;
13
15
  }
14
- export function scrollContainer(ref, dy, fraction) {
16
+ export function scrollContainer(ref, dy, fraction, dx) {
15
17
  const base = ref !== undefined ? refs.resolve(ref) : null;
16
- const target = base instanceof Element
17
- ? nearestScrollable(base)
18
+ return scrollFrom(base instanceof Element ? base : undefined, dy, fraction, dx);
19
+ }
20
+ export function scrollFrom(base, dy, fraction, dx) {
21
+ const axis = dx !== undefined && dx !== 0 && (dy === undefined || 0 === dy) ? 'x' : 'y';
22
+ const target = base !== undefined
23
+ ? nearestScrollable(base, axis)
18
24
  : (document.scrollingElement ?? document.documentElement);
19
25
  const before = target.scrollTop;
26
+ const beforeLeft = target.scrollLeft;
20
27
  if (fraction !== undefined && fraction >= 0 && fraction <= 1) {
21
28
  target.scrollTop = Math.round(target.scrollHeight * fraction);
22
29
  }
23
30
  else {
24
- const step = dy ?? (Math.round(target.clientHeight * VIEWPORT_FRACTION) || FALLBACK_STEP_PX);
25
- target.scrollTop = before + step;
31
+ if (dx !== undefined && dx !== 0)
32
+ target.scrollLeft = beforeLeft + dx;
33
+ if (dx === undefined || (dy !== undefined && 0 !== dy)) {
34
+ const step = dy ?? (Math.round(target.clientHeight * VIEWPORT_FRACTION) || FALLBACK_STEP_PX);
35
+ target.scrollTop = before + step;
36
+ }
26
37
  }
27
38
  target.dispatchEvent(new Event('scroll', { bubbles: false }));
28
39
  const after = target.scrollTop;
40
+ const afterLeft = target.scrollLeft;
29
41
  return {
30
- scrolled: after !== before,
42
+ scrolled: after !== before || afterLeft !== beforeLeft,
31
43
  scrollTop: after,
32
44
  scrollHeight: target.scrollHeight,
33
45
  clientHeight: target.clientHeight,
34
46
  atEnd: after + target.clientHeight >= target.scrollHeight - 1,
47
+ atStart: after <= 1,
48
+ scrollLeft: afterLeft,
49
+ scrollWidth: target.scrollWidth,
50
+ clientWidth: target.clientWidth,
51
+ atLeftEnd: afterLeft <= 1,
52
+ atRightEnd: afterLeft + target.clientWidth >= target.scrollWidth - 1,
35
53
  };
36
54
  }
@@ -1,17 +1,18 @@
1
1
  import { ActionType, ComponentStateReason, DANGEROUS_ACTION_CONFIRM_ARG, ElementQuerySchema, ReticleCommand, SnapshotMode, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
2
2
  import { buildSnapshot } from '../dom/snapshot.js';
3
+ import { paintContextOf } from '../dom/paint-context.js';
3
4
  import { matchQuery, runQuery } from '../dom/query.js';
4
5
  import { executeAction, executeSequence, dispatchWebMcp, } from '../actions/actions.js';
5
6
  import { describe } from '../dom/a11y.js';
6
- import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/source.js';
7
+ import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/addressing/source.js';
7
8
  import { themeReport } from '../dom/theme.js';
8
- import { refs } from '../dom/refs.js';
9
+ import { refs } from '../dom/addressing/refs.js';
9
10
  import { editEpoch } from '../edit-epoch.js';
10
11
  import { isButton, isInput } from '../dom/realm.js';
11
12
  import { hitTestOccluder } from '../dom/occlusion.js';
12
13
  import { readStorage } from '../observers/storage.js';
13
14
  import { captureDesktopWindow } from '../dom/desktop-capture.js';
14
- import { identifyComponent, readComponentState } from '../registry/adapters.js';
15
+ import { identifyComponent, readComponentState } from '../registry/stores/adapters.js';
15
16
  import { readStoresWithTruncation, readStoresRaw, storeNames } from '../registry/stores.js';
16
17
  import { sanitizeWithReport } from '../security/serialization.js';
17
18
  import { getCapabilities } from '../registry/capabilities.js';
@@ -84,10 +85,16 @@ function inspect(ref) {
84
85
  scrollHeight: el.scrollHeight,
85
86
  clientHeight: el.clientHeight,
86
87
  overflowY: cs?.overflowY ?? 'visible',
88
+ scrollLeft: el.scrollLeft,
89
+ scrollWidth: el.scrollWidth,
90
+ clientWidth: el.clientWidth,
91
+ overflowX: cs?.overflowX ?? 'visible',
87
92
  };
93
+ const paintContext = paintContextOf(el);
88
94
  return {
89
95
  ...describe(el),
90
96
  ...(source !== undefined ? { source } : {}),
97
+ ...(paintContext.length > 0 ? { paintContext } : {}),
91
98
  ...(sourceUnavailable !== undefined ? { sourceUnavailable } : {}),
92
99
  tag: el.tagName.toLowerCase(),
93
100
  href: el.getAttribute('href') ?? undefined,
package/dist/dom/a11y.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { ElementState, REDACTED_VALUE } from '@reticlehq/core';
2
2
  import { isButton, isHtmlElement, isImage, isInput, isSelect, isTextArea } from './realm.js';
3
- import { refs } from './refs.js';
3
+ import { refs } from './addressing/refs.js';
4
4
  import { inspectChart } from './chart.js';
5
5
  import { isSensitiveKey } from '../security/serialization.js';
6
- import { formatSource, sourceFromDom } from './source.js';
6
+ import { formatSource, sourceFromDom } from './addressing/source.js';
7
7
  const NAME_FROM_CONTENT = new Set([
8
8
  'button',
9
9
  'link',
@@ -1,5 +1,5 @@
1
1
  import { RETICLE_IPC_GLOBAL, RETICLE_TAURI_CAPTURE_COMMAND, VisualReason } from '@reticlehq/core';
2
- import { nativeFrame } from '../timers/native-timers.js';
2
+ import { nativeFrame } from '../timers/native/native-timers.js';
3
3
  import { RETICLE_OVERLAY } from './dom-ignore.js';
4
4
  const FULL_PAGE_UNSUPPORTED_MARKER = VisualReason.FULL_PAGE_UNSUPPORTED;
5
5
  const RECOGNISED_MARKERS = [
@@ -1,4 +1,4 @@
1
- export const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark],[data-reticle-blocker]';
1
+ export const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark],[data-reticle-blocker],[data-reticle-tour]';
2
2
  const DEV_OVERLAYS = '[data-agentation],#__next-build-watcher,nextjs-portal,[data-nextjs-dialog],[data-nextjs-toast]';
3
3
  let extraIgnore = '';
4
4
  let presenterVisible = false;
@@ -1,13 +1,39 @@
1
1
  import { isReticleUi } from './dom-ignore.js';
2
+ const SAMPLE_POINTS = [
3
+ [0.5, 0.5],
4
+ [0.25, 0.25],
5
+ [0.75, 0.25],
6
+ [0.25, 0.75],
7
+ [0.75, 0.75],
8
+ ];
9
+ const BLOCKED_MAJORITY = 3;
2
10
  export function hitTestOccluder(el, rect) {
3
11
  if (0 === rect.width || 0 === rect.height)
4
12
  return null;
5
13
  const doc = el.ownerDocument;
6
14
  if (typeof doc.elementFromPoint !== 'function')
7
15
  return null;
8
- const top = doc.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
9
- if (null === top || isReticleUi(top))
10
- return null;
11
- const lands = top === el || el.contains(top) || top.contains(el);
12
- return lands ? null : top;
16
+ const withStack = doc;
17
+ const topAt = (x, y) => {
18
+ const found = withStack.elementsFromPoint?.(x, y) ?? [doc.elementFromPoint(x, y)];
19
+ return found.find((c) => null !== c && !isReticleUi(c)) ?? null;
20
+ };
21
+ const blockers = new Map();
22
+ for (const [fx, fy] of SAMPLE_POINTS) {
23
+ const top = topAt(rect.left + rect.width * fx, rect.top + rect.height * fy);
24
+ if (null === top)
25
+ continue;
26
+ if (top === el || el.contains(top) || top.contains(el))
27
+ continue;
28
+ blockers.set(top, (blockers.get(top) ?? 0) + 1);
29
+ }
30
+ let worst = null;
31
+ let worstCount = 0;
32
+ for (const [element, count] of blockers) {
33
+ if (count > worstCount) {
34
+ worst = element;
35
+ worstCount = count;
36
+ }
37
+ }
38
+ return worstCount >= BLOCKED_MAJORITY ? worst : null;
13
39
  }
@@ -0,0 +1,6 @@
1
+ export interface PaintContextEntry {
2
+ readonly on: string;
3
+ readonly property: string;
4
+ readonly value: string;
5
+ }
6
+ export declare function paintContextOf(el: Element): PaintContextEntry[];
@@ -0,0 +1,38 @@
1
+ const PAINT_PROPERTIES = [
2
+ 'filter',
3
+ 'opacity',
4
+ 'transform',
5
+ 'mix-blend-mode',
6
+ 'backdrop-filter',
7
+ ];
8
+ const INERT = {
9
+ filter: 'none',
10
+ opacity: '1',
11
+ transform: 'none',
12
+ 'mix-blend-mode': 'normal',
13
+ 'backdrop-filter': 'none',
14
+ };
15
+ function describeElement(el) {
16
+ if (el.id.length > 0)
17
+ return `#${el.id}`;
18
+ const cls = el.getAttribute('class');
19
+ if (cls !== null && cls.trim().length > 0)
20
+ return `.${cls.trim().split(/\s+/).join('.')}`;
21
+ return el.tagName.toLowerCase();
22
+ }
23
+ export function paintContextOf(el) {
24
+ const view = el.ownerDocument.defaultView;
25
+ if (null === view)
26
+ return [];
27
+ const found = [];
28
+ for (let node = el; node !== null && node !== el.ownerDocument.documentElement; node = node.parentElement) {
29
+ const style = view.getComputedStyle(node);
30
+ for (const property of PAINT_PROPERTIES) {
31
+ const value = style.getPropertyValue(property);
32
+ if (value.length > 0 && value !== INERT[property]) {
33
+ found.push({ on: describeElement(node), property, value });
34
+ }
35
+ }
36
+ }
37
+ return found;
38
+ }