@reticlehq/browser 3.9.0 → 3.10.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 (56) hide show
  1. package/dist/actions/action-report.d.ts +14 -0
  2. package/dist/actions/action-report.js +95 -22
  3. package/dist/actions/actions-dom.d.ts +16 -6
  4. package/dist/actions/actions-dom.js +94 -46
  5. package/dist/actions/actions-press.d.ts +10 -0
  6. package/dist/actions/actions-press.js +146 -2
  7. package/dist/actions/actions.js +52 -22
  8. package/dist/actions/insert-text.d.ts +3 -0
  9. package/dist/actions/insert-text.js +90 -0
  10. package/dist/actions/scroll.d.ts +1 -1
  11. package/dist/actions/scroll.js +25 -1
  12. package/dist/actions/value-input.d.ts +2 -0
  13. package/dist/actions/value-input.js +30 -9
  14. package/dist/commands/commands.js +82 -6
  15. package/dist/connect-options.d.ts +11 -1
  16. package/dist/connect-options.js +27 -1
  17. package/dist/dom/a11y.js +4 -3
  18. package/dist/dom/chart-geometry.d.ts +7 -0
  19. package/dist/dom/chart-geometry.js +39 -0
  20. package/dist/dom/occlusion.js +21 -0
  21. package/dist/dom/query-finders.d.ts +1 -1
  22. package/dist/dom/query-finders.js +10 -7
  23. package/dist/dom/query.d.ts +1 -0
  24. package/dist/dom/query.js +78 -51
  25. package/dist/dom/scope-roots.d.ts +8 -0
  26. package/dist/dom/scope-roots.js +44 -0
  27. package/dist/dom/snapshot.d.ts +3 -0
  28. package/dist/dom/snapshot.js +9 -2
  29. package/dist/dom/text-match.js +4 -3
  30. package/dist/dom/theme.js +6 -7
  31. package/dist/dom/visibility.d.ts +2 -0
  32. package/dist/dom/visibility.js +40 -8
  33. package/dist/observers/canvas.d.ts +1 -0
  34. package/dist/observers/canvas.js +10 -1
  35. package/dist/observers/console.js +46 -35
  36. package/dist/observers/dom.js +28 -4
  37. package/dist/observers/net-detail/resource-timing.d.ts +12 -0
  38. package/dist/observers/net-detail/resource-timing.js +69 -0
  39. package/dist/observers/network.js +12 -45
  40. package/dist/observers/perf.js +15 -1
  41. package/dist/presenter/chrome/presenter-verbs.js +1 -0
  42. package/dist/presenter/presenter-config.d.ts +1 -0
  43. package/dist/presenter/presenter-config.js +1 -0
  44. package/dist/presenter/presenter-shell.d.ts +1 -0
  45. package/dist/presenter/presenter-shell.js +6 -3
  46. package/dist/presenter/tour/tour.js +4 -0
  47. package/dist/recorder/recorder.js +82 -39
  48. package/dist/reticle-inject.js +597 -597
  49. package/dist/reticle.d.ts +3 -19
  50. package/dist/reticle.js +74 -108
  51. package/dist/security/serialization.js +35 -0
  52. package/dist/storage-keys.js +2 -1
  53. package/dist/timers/native/native-timers.js +21 -4
  54. package/dist/transport/bridge-url.d.ts +8 -0
  55. package/dist/transport/bridge-url.js +52 -0
  56. package/package.json +10 -10
@@ -1,10 +1,10 @@
1
- import { ActionType, ActionWarning, clampHoldMs, clickInputProblem, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
1
+ import { ACT_POINT_ARG, ActionType, ActionWarning, actPointProblem, boxFractionFromArgs, clampHoldMs, clickInputProblem, DRAG_TO_POINT_ARG, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
2
2
  import { isHiddenNativeOption } from '../dom/query-finders.js';
3
- import { isReticleUi } from '../dom/dom-ignore.js';
3
+ import { isReticleUi, isUnobserved } 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
6
  import { requiresDangerousConfirmation, assertOutwardAllowed, assertPaymentEntryAllowed, submitControlFor, } from './danger-context.js';
7
- import { assertEditable, assertNotRichText, isAutofillSafeTarget, setNativeValue, } from './value-input.js';
7
+ import { assertEditable, assertNotEditorTextArea, assertNotRichText, isAutofillSafeTarget, isRichText, setNativeValue, } from './value-input.js';
8
8
  import { getRole, getStates } from '../dom/a11y.js';
9
9
  import { isVisible } from '../dom/visibility.js';
10
10
  import { selectInCombobox } from './select-combobox.js';
@@ -14,7 +14,7 @@ import { asSettleBudget, nativeSetTimeout, settle } from '../timers/native/nativ
14
14
  import { publishSettledField, watchActedField } from '../observers/field.js';
15
15
  import { AppearedText } from './appeared-text.js';
16
16
  import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
17
- import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
17
+ import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, runKeydownDefault, runKeyupDefault, SPACE_KEY, } from './actions-press.js';
18
18
  import { anchorOf, result, visibleCanvases, } from './action-report.js';
19
19
  function asString(value, fallback = '') {
20
20
  return 'string' === typeof value ? value : fallback;
@@ -34,6 +34,7 @@ const FILL_LIKE = new Set([
34
34
  ActionType.TYPE,
35
35
  ActionType.CLEAR,
36
36
  ActionType.SELECT,
37
+ ActionType.INSERT_TEXT,
37
38
  ]);
38
39
  const isFillLike = (action) => FILL_LIKE.has(action);
39
40
  const CLICK_LIKE = new Set([
@@ -72,7 +73,7 @@ function assertActionAllowed(el, action, args) {
72
73
  action === ActionType.UNCHECK ||
73
74
  action === ActionType.DRAG ||
74
75
  action === ActionType.SUBMIT ||
75
- (action === ActionType.PRESS && 'Enter' === pressKey(args));
76
+ (action === ActionType.PRESS && ('Enter' === pressKey(args) || SPACE_KEY === pressKey(args)));
76
77
  const dragTarget = action === ActionType.DRAG ? refs.resolve(dragTargetRef(args)) : null;
77
78
  const submitter = action === ActionType.PRESS && 'Enter' === pressKey(args) ? submitControlFor(el) : null;
78
79
  if (canTrigger) {
@@ -100,7 +101,7 @@ function optionValueFor(options, named) {
100
101
  const byLabel = options.filter((option) => option.label.trim() === named.trim());
101
102
  return 1 === byLabel.length ? (byLabel[0]?.value ?? named) : named;
102
103
  }
103
- import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, scrollIntoViewNow, clickInputOf, } from './actions-dom.js';
104
+ import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, leavePreviousHover, dragElement, mouseEventFor, assertNotLeaving, scrollIntoViewNow, clickInputOf, coordsIn, } from './actions-dom.js';
104
105
  import { scrollFrom } from './scroll.js';
105
106
  import { clickHitsNoControl } from './click-target.js';
106
107
  function enabledOf(el) {
@@ -110,6 +111,9 @@ function valueOf(el) {
110
111
  if (isInput(el) || isTextArea(el) || isSelect(el)) {
111
112
  return el.value;
112
113
  }
114
+ if (isRichText(el)) {
115
+ return el.textContent ?? '';
116
+ }
113
117
  return undefined;
114
118
  }
115
119
  function activeRef(el) {
@@ -122,11 +126,11 @@ function activeRef(el) {
122
126
  async function dispatchFor(el, action, args) {
123
127
  if (ActionType.CLICK === action) {
124
128
  const hold = clampHoldMs(args['holdMs']);
125
- return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() }, undefined, clickInputOf(args));
129
+ return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() }, undefined, clickInputOf(args), boxFractionFromArgs(args, ACT_POINT_ARG));
126
130
  }
127
131
  if (ActionType.TAP === action) {
128
132
  const hold = clampHoldMs(args['holdMs']);
129
- return await fireTapSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
133
+ return await fireTapSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() }, boxFractionFromArgs(args, ACT_POINT_ARG));
130
134
  }
131
135
  return { prevented: await dispatchOther(el, action, args), heldMs: 0 };
132
136
  }
@@ -145,24 +149,26 @@ async function dispatchOther(el, action, args) {
145
149
  switch (action) {
146
150
  case ActionType.DBLCLICK: {
147
151
  const input = clickInputOf(args);
148
- await fireClickSequence(el, undefined, 1, input);
149
- await fireClickSequence(el, undefined, 2, input);
152
+ const point = boxFractionFromArgs(args, ACT_POINT_ARG);
153
+ await fireClickSequence(el, undefined, 1, input, point);
154
+ await fireClickSequence(el, undefined, 2, input, point);
150
155
  return !asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'dblclick', {
151
156
  bubbles: true,
152
157
  cancelable: true,
153
158
  detail: 2,
154
159
  ...input.modifiers,
160
+ ...coordsIn(el, point),
155
161
  })));
156
162
  }
157
163
  case ActionType.HOVER: {
158
- const doc = el.ownerDocument;
159
- const from = doc.activeElement ?? doc.body;
160
- firePointer(el, 'pointerover', from);
161
- el.dispatchEvent(mouseEventFor(el, 'mouseover', { bubbles: true, relatedTarget: from }));
162
- firePointerNonBubbling(el, 'pointerenter', from);
163
- el.dispatchEvent(mouseEventFor(el, 'mouseenter', { relatedTarget: from }));
164
- firePointer(el, 'pointermove', from);
165
- const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true }));
164
+ const coords = coordsIn(el, boxFractionFromArgs(args, ACT_POINT_ARG));
165
+ const from = leavePreviousHover(el);
166
+ firePointer(el, 'pointerover', from, coords);
167
+ el.dispatchEvent(mouseEventFor(el, 'mouseover', { bubbles: true, relatedTarget: from, ...coords }));
168
+ firePointerNonBubbling(el, 'pointerenter', from, coords);
169
+ el.dispatchEvent(mouseEventFor(el, 'mouseenter', { relatedTarget: from, ...coords }));
170
+ firePointer(el, 'pointermove', from, coords);
171
+ const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true, ...coords }));
166
172
  const holdMs = clampHoldMs(args['holdMs']);
167
173
  if (holdMs > 0) {
168
174
  await sleep(holdMs);
@@ -184,6 +190,7 @@ async function dispatchOther(el, action, args) {
184
190
  return false;
185
191
  }
186
192
  case ActionType.FILL:
193
+ assertNotEditorTextArea(el, 'fill');
187
194
  if (isInput(el) || isTextArea(el)) {
188
195
  if (typeof args['value'] !== 'string') {
189
196
  throw new Error("fill requires a string `value` — pass it nested, as args: { value: '…' }. " +
@@ -198,6 +205,7 @@ async function dispatchOther(el, action, args) {
198
205
  assertNotRichText(el, 'fill');
199
206
  throw new Error(`cannot fill a <${el.tagName.toLowerCase()}>`);
200
207
  case ActionType.TYPE:
208
+ assertNotEditorTextArea(el, 'type into');
201
209
  if (isInput(el) || isTextArea(el)) {
202
210
  if (typeof args['text'] !== 'string') {
203
211
  throw new Error("type requires a string `text` — pass it nested, as args: { text: '…' }.");
@@ -209,10 +217,15 @@ async function dispatchOther(el, action, args) {
209
217
  assertNotRichText(el, 'type into');
210
218
  throw new Error(`cannot type into a <${el.tagName.toLowerCase()}>`);
211
219
  case ActionType.CLEAR:
220
+ assertNotEditorTextArea(el, 'clear');
212
221
  if (isInput(el) || isTextArea(el)) {
213
222
  return setNativeValue(el, '');
214
223
  }
215
224
  throw new Error(`cannot clear a <${el.tagName.toLowerCase()}>`);
225
+ case ActionType.INSERT_TEXT: {
226
+ const { insertText } = await import('./insert-text.js');
227
+ return insertText(el, args['text']);
228
+ }
216
229
  case ActionType.SELECT:
217
230
  if (isSelect(el)) {
218
231
  if (typeof args['value'] !== 'string') {
@@ -281,12 +294,16 @@ async function dispatchOther(el, action, args) {
281
294
  closeModalOnEscape(el, key, down);
282
295
  if (down) {
283
296
  dispatchKeypress(el, key, code, mods);
297
+ runKeydownDefault(el, key, mods);
284
298
  }
285
299
  const hold = clampHoldMs(args['holdMs']);
286
300
  if (hold > 0) {
287
301
  await holdKey(el, key, code, mods, hold);
288
302
  }
289
303
  asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
304
+ if (down) {
305
+ runKeyupDefault(el, key, mods);
306
+ }
290
307
  return !down;
291
308
  }
292
309
  case ActionType.SCROLL_INTO_VIEW:
@@ -298,7 +315,7 @@ async function dispatchOther(el, action, args) {
298
315
  const dy = asFiniteNumber(args['dy']);
299
316
  const dx = asFiniteNumber(args['dx']);
300
317
  const fraction = asFiniteNumber(args['fraction']);
301
- scrollFrom(el, dy, fraction, dx);
318
+ scrollFrom(el, dy, fraction, dx, true === args['wheel']);
302
319
  return false;
303
320
  }
304
321
  case ActionType.UPLOAD: {
@@ -328,22 +345,35 @@ async function dispatchOther(el, action, args) {
328
345
  }
329
346
  case ActionType.DRAG: {
330
347
  const toRef = dragTargetRef(args);
348
+ const points = {
349
+ at: boxFractionFromArgs(args, ACT_POINT_ARG),
350
+ to: boxFractionFromArgs(args, DRAG_TO_POINT_ARG),
351
+ };
331
352
  if ('' === toRef) {
332
- return await dragElement(el, null, args['data']);
353
+ return await dragElement(el, null, args['data'], points, clickInputOf(args));
333
354
  }
334
355
  const resolved = refs.resolve(toRef);
335
356
  if (!isActionTarget(resolved)) {
336
357
  throw new Error(`drag target '${echoRef(toRef)}' did not resolve to an element — pass a ref from ` +
337
358
  'reticle_snapshot or reticle_query as args.toRef (alias: args.target)');
338
359
  }
339
- return await dragElement(el, resolved, args['data']);
360
+ return await dragElement(el, resolved, args['data'], points, clickInputOf(args));
340
361
  }
341
362
  default:
342
363
  throw new Error(`unknown action '${action}'`);
343
364
  }
344
365
  }
366
+ function isPageMutation(record) {
367
+ const target = record.target;
368
+ const el = target instanceof Element ? target : target.parentElement;
369
+ return null === el || !isUnobserved(el);
370
+ }
345
371
  export async function executeAction(ref, action, args = {}) {
346
372
  const named = 0 < ref.length;
373
+ const pointProblem = actPointProblem(action, args);
374
+ if (pointProblem !== undefined) {
375
+ throw new Error(`cannot ${action}: ${pointProblem}`);
376
+ }
347
377
  if (ActionType.CLICK === action || ActionType.DBLCLICK === action) {
348
378
  const problem = clickInputProblem(action, args);
349
379
  if (problem !== undefined) {
@@ -379,7 +409,7 @@ export async function executeAction(ref, action, args = {}) {
379
409
  let mutated = 0;
380
410
  const said = new AppearedText();
381
411
  const obs = new MutationObserver((records) => {
382
- mutated += records.length;
412
+ mutated += records.filter(isPageMutation).length;
383
413
  said.collect(records);
384
414
  });
385
415
  obs.observe(el.ownerDocument.documentElement, {
@@ -0,0 +1,3 @@
1
+ import type { ActionTarget } from '../dom/realm.js';
2
+ export declare function insertText(el: ActionTarget, text: unknown): boolean;
3
+ export declare function insertTextInto(el: ActionTarget, text: string): boolean;
@@ -0,0 +1,90 @@
1
+ import { isRichText } from './value-input.js';
2
+ export function insertText(el, text) {
3
+ if (typeof text !== 'string') {
4
+ throw new Error("insertText requires a string `text` — pass it nested, as args: { text: '…' }.");
5
+ }
6
+ if (!isRichText(el)) {
7
+ throw new Error(`cannot insertText into a <${el.tagName.toLowerCase()}> — insertText needs a contenteditable rich-text editor (Tiptap, ProseMirror, Quill), not this surface`);
8
+ }
9
+ if (hasEditContext(el)) {
10
+ throw new Error('cannot insertText into an EditContext editor (Monaco, CodeMirror 6): code that calls updateText does not fire textupdate, so the editor model would keep the old text. Use press for a key.');
11
+ }
12
+ return insertTextInto(el, text);
13
+ }
14
+ function hasEditContext(el) {
15
+ if (!('editContext' in el)) {
16
+ return false;
17
+ }
18
+ const surface = el;
19
+ return null !== surface.editContext && undefined !== surface.editContext;
20
+ }
21
+ export function insertTextInto(el, text) {
22
+ el.focus();
23
+ const selection = window.getSelection();
24
+ if (selection !== null && !selectionInside(selection, el)) {
25
+ const range = document.createRange();
26
+ range.selectNodeContents(el);
27
+ range.collapse(false);
28
+ selection.removeAllRanges();
29
+ selection.addRange(range);
30
+ }
31
+ const beforeInput = new InputEvent('beforeinput', {
32
+ inputType: 'insertText',
33
+ data: text,
34
+ bubbles: true,
35
+ cancelable: true,
36
+ });
37
+ if (!el.dispatchEvent(beforeInput)) {
38
+ return true;
39
+ }
40
+ if ('function' !== typeof el.ownerDocument.execCommand) {
41
+ insertAtCaret(el, text);
42
+ const input = new InputEvent('input', {
43
+ inputType: 'insertText',
44
+ data: text,
45
+ bubbles: true,
46
+ cancelable: true,
47
+ });
48
+ return !el.dispatchEvent(input);
49
+ }
50
+ let sawInput = false;
51
+ const recordInput = (event) => {
52
+ if ('input' === event.type) {
53
+ sawInput = true;
54
+ }
55
+ };
56
+ window.addEventListener('input', recordInput, false);
57
+ try {
58
+ if (el.ownerDocument.execCommand('insertText', false, text)) {
59
+ return false;
60
+ }
61
+ }
62
+ catch {
63
+ }
64
+ finally {
65
+ window.removeEventListener('input', recordInput, false);
66
+ }
67
+ if (sawInput) {
68
+ return false;
69
+ }
70
+ throw new Error('the editor did not accept the text: the native insertText command failed, so the editor model is unchanged. Check that the surface is editable and focused.');
71
+ }
72
+ function insertAtCaret(el, text) {
73
+ const selection = window.getSelection();
74
+ const range = selection !== null && 0 < selection.rangeCount ? selection.getRangeAt(0) : null;
75
+ if (range !== null && el.contains(range.commonAncestorContainer)) {
76
+ range.deleteContents();
77
+ const node = el.ownerDocument.createTextNode(text);
78
+ range.insertNode(node);
79
+ range.setStartAfter(node);
80
+ range.collapse(true);
81
+ selection?.removeAllRanges();
82
+ selection?.addRange(range);
83
+ return;
84
+ }
85
+ el.appendChild(el.ownerDocument.createTextNode(text));
86
+ }
87
+ function selectionInside(selection, el) {
88
+ const anchor = selection.anchorNode;
89
+ return anchor !== null && el.contains(anchor);
90
+ }
@@ -14,5 +14,5 @@ interface ScrollResult {
14
14
  export declare const SCROLL_FRACTION_RANGE_ERROR_PREFIX = "scroll fraction must be between 0 and 1, got ";
15
15
  export declare function fractionOutOfRangeMessage(fraction: number): string;
16
16
  export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
17
- export declare function scrollFrom(base: Element | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
17
+ export declare function scrollFrom(base: Element | undefined, dy: number | undefined, fraction?: number, dx?: number, wheel?: boolean): ScrollResult;
18
18
  export {};
@@ -2,6 +2,7 @@ import { refs } from '../dom/addressing/refs.js';
2
2
  const FALLBACK_STEP_PX = 400;
3
3
  const VIEWPORT_FRACTION = 0.8;
4
4
  export const SCROLL_FRACTION_RANGE_ERROR_PREFIX = 'scroll fraction must be between 0 and 1, got ';
5
+ const SCROLL_WHEEL_FRACTION_ERROR = 'scroll with wheel: true cannot use fraction; pass dy or dx';
5
6
  export function fractionOutOfRangeMessage(fraction) {
6
7
  return `${SCROLL_FRACTION_RANGE_ERROR_PREFIX}${fraction}`;
7
8
  }
@@ -21,7 +22,7 @@ export function scrollContainer(ref, dy, fraction, dx) {
21
22
  const base = ref !== undefined ? refs.resolve(ref) : null;
22
23
  return scrollFrom(base instanceof Element ? base : undefined, dy, fraction, dx);
23
24
  }
24
- export function scrollFrom(base, dy, fraction, dx) {
25
+ export function scrollFrom(base, dy, fraction, dx, wheel) {
25
26
  if (fraction !== undefined && (fraction < 0 || fraction > 1 || Number.isNaN(fraction))) {
26
27
  throw new RangeError(fractionOutOfRangeMessage(fraction));
27
28
  }
@@ -31,6 +32,26 @@ export function scrollFrom(base, dy, fraction, dx) {
31
32
  : (document.scrollingElement ?? document.documentElement);
32
33
  const before = target.scrollTop;
33
34
  const beforeLeft = target.scrollLeft;
35
+ if (true === wheel) {
36
+ if (fraction !== undefined) {
37
+ throw new RangeError(SCROLL_WHEEL_FRACTION_ERROR);
38
+ }
39
+ const deltaY = dy ?? (Math.round(target.clientHeight * VIEWPORT_FRACTION) || FALLBACK_STEP_PX);
40
+ const deltaX = dx ?? 0;
41
+ const wheelTarget = base ?? target;
42
+ const rect = wheelTarget.getBoundingClientRect();
43
+ wheelTarget.dispatchEvent(new WheelEvent('wheel', {
44
+ deltaX,
45
+ deltaY,
46
+ deltaMode: WheelEvent.DOM_DELTA_PIXEL,
47
+ clientX: rect.width > 0 ? rect.left + rect.width / 2 : 0,
48
+ clientY: rect.height > 0 ? rect.top + rect.height / 2 : 0,
49
+ bubbles: true,
50
+ cancelable: true,
51
+ composed: true,
52
+ }));
53
+ return scrollResult(target, before, beforeLeft);
54
+ }
34
55
  if (fraction !== undefined) {
35
56
  target.scrollTop = Math.round(target.scrollHeight * fraction);
36
57
  }
@@ -44,6 +65,9 @@ export function scrollFrom(base, dy, fraction, dx) {
44
65
  }
45
66
  }
46
67
  target.dispatchEvent(new Event('scroll', { bubbles: false }));
68
+ return scrollResult(target, before, beforeLeft);
69
+ }
70
+ function scrollResult(target, before, beforeLeft) {
47
71
  const after = target.scrollTop;
48
72
  const afterLeft = target.scrollLeft;
49
73
  return {
@@ -1,5 +1,7 @@
1
1
  import { type ActionTarget } from '../dom/realm.js';
2
2
  export declare function isAutofillSafeTarget(el: Element, ua?: string): boolean;
3
3
  export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): boolean;
4
+ export declare function isRichText(el: Element): boolean;
4
5
  export declare function assertNotRichText(el: ActionTarget, action: string): void;
6
+ export declare function assertNotEditorTextArea(el: ActionTarget, action: string): void;
5
7
  export declare function assertEditable(el: HTMLInputElement | HTMLTextAreaElement, action: string): void;
@@ -1,14 +1,16 @@
1
1
  import { captureValueSetter } from '../patching/capture-method.js';
2
- import { isHtmlElement, valuePrototypeOf } from '../dom/realm.js';
2
+ import { isHtmlElement, isTextArea, valuePrototypeOf } from '../dom/realm.js';
3
3
  const PASSWORD_AUTOCOMPLETE = /\b(current|new)-password\b/;
4
4
  const NOT_WEBKIT = /Chrome|Chromium|Edg\/|Firefox|Gecko\//;
5
5
  export function isAutofillSafeTarget(el, ua = navigator.userAgent) {
6
- if (!(el instanceof HTMLInputElement))
6
+ if (!(el instanceof HTMLInputElement)) {
7
7
  return false;
8
+ }
8
9
  const password = 'password' === el.type.toLowerCase() ||
9
10
  PASSWORD_AUTOCOMPLETE.test(el.getAttribute('autocomplete') ?? '');
10
- if (!password || NOT_WEBKIT.test(ua))
11
+ if (!password || NOT_WEBKIT.test(ua)) {
11
12
  return false;
13
+ }
12
14
  return 600 <= Number(/AppleWebKit\/(\d+)/.exec(ua)?.[1] ?? 0);
13
15
  }
14
16
  export function setNativeValue(el, value) {
@@ -24,21 +26,40 @@ export function setNativeValue(el, value) {
24
26
  el.dispatchEvent(new Event('change', { bubbles: true }));
25
27
  return !notPrevented;
26
28
  }
27
- function isRichText(el) {
29
+ export function isRichText(el) {
28
30
  const flag = el.getAttribute('contenteditable');
29
- if ((isHtmlElement(el) && el.isContentEditable) || (null !== flag && 'false' !== flag))
31
+ if ((isHtmlElement(el) && el.isContentEditable) || (null !== flag && 'false' !== flag)) {
30
32
  return true;
31
- if ('textbox' === el.getAttribute('role'))
33
+ }
34
+ if ('textbox' === el.getAttribute('role')) {
32
35
  return true;
33
- if (!('editContext' in el))
36
+ }
37
+ if (!('editContext' in el)) {
34
38
  return false;
39
+ }
35
40
  const surface = el;
36
41
  return null !== surface.editContext && undefined !== surface.editContext;
37
42
  }
43
+ const EDITOR_ROOT = '.monaco-editor, .CodeMirror, .cm-editor';
44
+ function isEditorTextArea(el) {
45
+ if (!isTextArea(el)) {
46
+ return false;
47
+ }
48
+ return 'textbox' === el.getAttribute('role') || null !== el.closest(EDITOR_ROOT);
49
+ }
50
+ function refuseEditor(action) {
51
+ throw new Error(`cannot ${action} a contenteditable or EditContext editor; use insertText, or press for a key`);
52
+ }
38
53
  export function assertNotRichText(el, action) {
39
- if (!isRichText(el))
54
+ if (!isRichText(el)) {
40
55
  return;
41
- throw new Error(`cannot ${action} a contenteditable or EditContext editor; use press or an input`);
56
+ }
57
+ refuseEditor(action);
58
+ }
59
+ export function assertNotEditorTextArea(el, action) {
60
+ if (isEditorTextArea(el)) {
61
+ refuseEditor(action);
62
+ }
42
63
  }
43
64
  export function assertEditable(el, action) {
44
65
  if (el.disabled) {
@@ -1,7 +1,7 @@
1
- import { ComponentStateReason, ElementQuerySchema, INSPECT_IN_VIEWPORT_FIELD, INSPECT_REVEAL_ARG, MatchArg, ReticleCommand, SnapshotMode, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
1
+ import { ANIMATION_TARGET_ARG, ComponentStateReason, ElementQuerySchema, INSPECT_IN_VIEWPORT_FIELD, INSPECT_REVEAL_ARG, MatchArg, ReticleCommand, SnapshotMode, SESSION_REF, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
2
2
  import { buildSnapshot } from '../dom/snapshot.js';
3
3
  import { paintContextOf } from '../dom/paint-context.js';
4
- import { matchQuery, runQuery } from '../dom/query.js';
4
+ import { embeddedRootsUnder, matchQuery, runQuery } from '../dom/query.js';
5
5
  import { executeAction, executeSequence } from '../actions/actions.js';
6
6
  import { describe } from '../dom/a11y.js';
7
7
  import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/addressing/source.js';
@@ -173,23 +173,98 @@ function readState(ref, store, path, depth) {
173
173
  }
174
174
  return result;
175
175
  }
176
- function listAnimations() {
176
+ function animationName(animation) {
177
+ const fields = animation;
178
+ const name = str(fields.animationName);
179
+ if (name !== undefined && name.length > 0) {
180
+ return name;
181
+ }
182
+ const transition = str(fields.transitionProperty);
183
+ return transition !== undefined && transition.length > 0 ? transition : undefined;
184
+ }
185
+ function resolveAnimationTarget(target) {
186
+ if (target === undefined) {
187
+ return undefined;
188
+ }
189
+ const byRef = refs.resolve(target);
190
+ if (byRef !== null) {
191
+ const ref = refs.refFor(byRef);
192
+ return {
193
+ report: { target, matched: true, refs: [ref], count: 1 },
194
+ matches: (animationTarget) => animationTarget !== null && refs.refFor(animationTarget) === ref,
195
+ };
196
+ }
197
+ if (SESSION_REF.test(target)) {
198
+ return {
199
+ report: { target, matched: false, refs: [], count: 0 },
200
+ matches: () => false,
201
+ };
202
+ }
203
+ let elements;
204
+ try {
205
+ const roots = [document];
206
+ const container = document.body ?? document.documentElement;
207
+ if (container !== null) {
208
+ roots.push(...embeddedRootsUnder(container));
209
+ }
210
+ const seen = new Set();
211
+ elements = [];
212
+ for (const root of roots) {
213
+ for (const el of root.querySelectorAll(target)) {
214
+ if (seen.has(el)) {
215
+ continue;
216
+ }
217
+ seen.add(el);
218
+ elements.push(el);
219
+ }
220
+ }
221
+ }
222
+ catch {
223
+ return {
224
+ report: { target, matched: false, refs: [], count: 0, invalid: true },
225
+ matches: () => false,
226
+ };
227
+ }
228
+ const allRefs = new Set(elements.map((el) => refs.refFor(el)));
229
+ const refsInReport = Array.from(allRefs).slice(0, TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS);
230
+ return {
231
+ report: {
232
+ target,
233
+ matched: elements.length > 0,
234
+ refs: refsInReport,
235
+ count: elements.length,
236
+ ...(allRefs.size > refsInReport.length ? { truncated: true } : {}),
237
+ },
238
+ matches: (animationTarget) => animationTarget !== null && allRefs.has(refs.refFor(animationTarget)),
239
+ };
240
+ }
241
+ function listAnimations(args = {}) {
177
242
  const doc = document;
243
+ const targetFilter = resolveAnimationTarget(str(args[ANIMATION_TARGET_ARG]));
178
244
  if (typeof doc.getAnimations !== 'function') {
179
- return { animations: [] };
245
+ return {
246
+ animations: [],
247
+ ...(targetFilter === undefined ? {} : { target: targetFilter.report }),
248
+ };
180
249
  }
181
250
  const animations = doc.getAnimations().map((a) => {
182
251
  const effect = a.effect;
183
252
  const timing = effect?.getTiming();
184
253
  const target = effect?.target ?? null;
254
+ const name = animationName(a);
185
255
  return {
256
+ ...(name === undefined ? {} : { name }),
186
257
  playState: a.playState,
187
258
  currentTime: a.currentTime,
188
259
  duration: timing?.duration,
189
260
  target: null === target ? null : describe(target),
261
+ ...(targetFilter === undefined ? {} : { targetMatches: targetFilter.matches(target) }),
190
262
  };
191
263
  });
192
- return { animations };
264
+ return {
265
+ animations,
266
+ ...(targetFilter === undefined ? {} : { target: targetFilter.report }),
267
+ };
193
268
  }
194
269
  export function resolveNavigationUrl(rawUrl, baseUrl) {
195
270
  if (0 === rawUrl.length || rawUrl.length > TRANSPORT_LIMITS.MAX_URL_LENGTH) {
@@ -211,6 +286,7 @@ export function createCommandRegistry() {
211
286
  includeRoot: true === args['includeRoot'],
212
287
  ids: true === args['ids'],
213
288
  audit: true === args['audit'],
289
+ charts: true === args['charts'],
214
290
  }));
215
291
  reg.set(ReticleCommand.QUERY, (args) => {
216
292
  const limit = args['limit'];
@@ -229,7 +305,7 @@ export function createCommandRegistry() {
229
305
  return result;
230
306
  });
231
307
  reg.set(ReticleCommand.INSPECT, (args) => inspect(str(args['ref']) ?? '', true === args[INSPECT_REVEAL_ARG]));
232
- reg.set(ReticleCommand.ANIMATIONS, () => listAnimations());
308
+ reg.set(ReticleCommand.ANIMATIONS, (args) => listAnimations(args));
233
309
  reg.set(ReticleCommand.CLOCK, (args) => {
234
310
  if (true === args['reset']) {
235
311
  resetClock();
@@ -1,4 +1,4 @@
1
- import { HelloInstrumentation, type HelloEnv, type HelloScope, type RedactionConfig } from '@reticlehq/core';
1
+ import { HelloInstrumentation, HudVisibility, type HelloEnv, type HelloScope, type RedactionConfig } from '@reticlehq/core';
2
2
  export interface ReticleConnectOptions {
3
3
  url?: string;
4
4
  session?: string;
@@ -37,3 +37,13 @@ export declare function helloMode(options: Pick<ReticleConnectOptions, 'env' | '
37
37
  };
38
38
  export declare function shouldBlockProduction(nodeEnv: string | undefined, allowInProduction: boolean): boolean;
39
39
  export declare function leaseToken(options: ReticleConnectOptions, current: string | undefined): string | undefined;
40
+ export declare function oneOf<T extends Record<string, string>>(values: T, raw: unknown): T[keyof T] | undefined;
41
+ export declare function reticleParamsFromSearch(search: string): {
42
+ session?: string;
43
+ projectId?: string;
44
+ hud?: HudVisibility;
45
+ };
46
+ export declare function hudWithoutPanel(hud: HudVisibility | undefined): {
47
+ remember?: HudVisibility;
48
+ placed: HudVisibility[];
49
+ };
@@ -1,4 +1,4 @@
1
- import { HelloInstrumentation, } from '@reticlehq/core';
1
+ import { HelloInstrumentation, HudVisibility, RETICLE_URL_PARAM, } from '@reticlehq/core';
2
2
  export function helloMode(options) {
3
3
  return {
4
4
  ...(options.env === undefined ? {} : { env: options.env }),
@@ -16,3 +16,29 @@ export function leaseToken(options, current) {
16
16
  }
17
17
  return token;
18
18
  }
19
+ export function oneOf(values, raw) {
20
+ return Object.values(values).includes(raw) ? raw : undefined;
21
+ }
22
+ export function reticleParamsFromSearch(search) {
23
+ const params = new URLSearchParams(search);
24
+ const out = {};
25
+ const session = params.get(RETICLE_URL_PARAM.SESSION);
26
+ const projectId = params.get(RETICLE_URL_PARAM.PROJECT);
27
+ const hud = oneOf(HudVisibility, params.get(RETICLE_URL_PARAM.HUD));
28
+ if (session !== null && session.length > 0) {
29
+ out.session = session;
30
+ }
31
+ if (projectId !== null && projectId.length > 0) {
32
+ out.projectId = projectId;
33
+ }
34
+ if (hud !== undefined) {
35
+ out.hud = hud;
36
+ }
37
+ return out;
38
+ }
39
+ export function hudWithoutPanel(hud) {
40
+ if (hud === undefined) {
41
+ return { placed: [] };
42
+ }
43
+ return { remember: hud, placed: HudVisibility.SHOWN === hud ? [] : [hud] };
44
+ }
package/dist/dom/a11y.js CHANGED
@@ -5,7 +5,7 @@ import { refs } from './addressing/refs.js';
5
5
  import { inspectChart } from './chart.js';
6
6
  import { isSensitiveKey } from '../security/serialization.js';
7
7
  import { formatSource, sourceFromDom } from './addressing/source.js';
8
- import { HTML_DETAILS_OPEN_ATTRIBUTE, HTML_DETAILS_TAG, HTML_SUMMARY_TAG, isVisible, } from './visibility.js';
8
+ import { HTML_DETAILS_OPEN_ATTRIBUTE, HTML_DETAILS_TAG, HTML_SUMMARY_TAG, isNonRendered, isVisible, } from './visibility.js';
9
9
  const NAME_FROM_CONTENT = new Set([
10
10
  'button',
11
11
  'link',
@@ -30,6 +30,7 @@ const NAME_FROM_CONTENT = new Set([
30
30
  'alert',
31
31
  ]);
32
32
  const INPUT_TEXT_TYPES = new Set(['text', 'email', 'tel', 'url', 'password', '']);
33
+ const INPUT_BUTTON_TYPES = new Set(['submit', 'button', 'reset', 'file', 'image']);
33
34
  function inputRole(input) {
34
35
  const type = input.type.toLowerCase();
35
36
  if ('search' === type) {
@@ -50,7 +51,7 @@ function inputRole(input) {
50
51
  if ('number' === type) {
51
52
  return 'spinbutton';
52
53
  }
53
- if ('submit' === type || 'button' === type || 'reset' === type) {
54
+ if (INPUT_BUTTON_TYPES.has(type)) {
54
55
  return 'button';
55
56
  }
56
57
  return 'textbox';
@@ -182,7 +183,7 @@ function textWithoutHidden(node) {
182
183
  return '';
183
184
  }
184
185
  const el = node;
185
- if ('true' === el.getAttribute('aria-hidden')) {
186
+ if ('true' === el.getAttribute('aria-hidden') || isNonRendered(el)) {
186
187
  return '';
187
188
  }
188
189
  if (isImage(el)) {