@reticlehq/browser 3.5.0 → 3.6.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 (87) hide show
  1. package/dist/actions/actions-dom.js +44 -43
  2. package/dist/actions/actions-press.d.ts +10 -4
  3. package/dist/actions/actions-press.js +108 -17
  4. package/dist/actions/actions.d.ts +2 -0
  5. package/dist/actions/actions.js +25 -16
  6. package/dist/actions/danger-context.js +1 -1
  7. package/dist/connect-options.d.ts +1 -0
  8. package/dist/dom/a11y.js +13 -4
  9. package/dist/dom/addressing/refs.js +2 -1
  10. package/dist/dom/addressing/testid-attr.d.ts +4 -0
  11. package/dist/dom/addressing/testid-attr.js +14 -0
  12. package/dist/dom/query.d.ts +1 -0
  13. package/dist/dom/query.js +38 -11
  14. package/dist/dom/snapshot.js +1 -0
  15. package/dist/observers/departure.js +51 -9
  16. package/dist/observers/dom.js +3 -2
  17. package/dist/observers/field.js +5 -3
  18. package/dist/observers/health.js +63 -2
  19. package/dist/observers/network-stream.d.ts +1 -1
  20. package/dist/observers/network-stream.js +3 -3
  21. package/dist/observers/network.js +1 -1
  22. package/dist/observers/storage.js +6 -1
  23. package/dist/presenter/carousel/carousel.d.ts +5 -2
  24. package/dist/presenter/carousel/carousel.js +34 -7
  25. package/dist/presenter/carousel/offer-card.d.ts +2 -2
  26. package/dist/presenter/carousel/offer-card.js +2 -1
  27. package/dist/presenter/carousel/panel-slides.d.ts +4 -3
  28. package/dist/presenter/carousel/panel-slides.js +43 -6
  29. package/dist/presenter/carousel/talk-card.d.ts +1 -1
  30. package/dist/presenter/chrome/log-kinds.d.ts +5 -0
  31. package/dist/presenter/chrome/log-kinds.js +1 -0
  32. package/dist/presenter/chrome/log-memory.d.ts +12 -0
  33. package/dist/presenter/chrome/log-memory.js +39 -0
  34. package/dist/presenter/chrome/presenter-hud-chrome.d.ts +1 -0
  35. package/dist/presenter/chrome/presenter-hud-chrome.js +7 -1
  36. package/dist/presenter/chrome/presenter-log.d.ts +5 -4
  37. package/dist/presenter/chrome/presenter-log.js +55 -5
  38. package/dist/presenter/chrome/presenter-report-copy.d.ts +11 -1
  39. package/dist/presenter/chrome/presenter-report-copy.js +14 -1
  40. package/dist/presenter/hud-telemetry.js +31 -2
  41. package/dist/presenter/icons/presenter-heroicons-data.d.ts +1 -0
  42. package/dist/presenter/icons/presenter-heroicons-data.js +4 -3
  43. package/dist/presenter/icons/presenter-icons.d.ts +2 -0
  44. package/dist/presenter/icons/presenter-icons.js +2 -0
  45. package/dist/presenter/presenter-account.d.ts +5 -4
  46. package/dist/presenter/presenter-account.js +23 -10
  47. package/dist/presenter/presenter-chat-views.d.ts +21 -0
  48. package/dist/presenter/presenter-chat-views.js +382 -0
  49. package/dist/presenter/presenter-config.d.ts +3 -2
  50. package/dist/presenter/presenter-config.js +4 -3
  51. package/dist/presenter/presenter-controls.d.ts +4 -2
  52. package/dist/presenter/presenter-controls.js +231 -51
  53. package/dist/presenter/presenter-design-tokens.d.ts +1 -0
  54. package/dist/presenter/presenter-design-tokens.js +34 -0
  55. package/dist/presenter/presenter-dock-layout.js +1 -1
  56. package/dist/presenter/presenter-frame.d.ts +12 -0
  57. package/dist/presenter/presenter-frame.js +189 -0
  58. package/dist/presenter/presenter-plan.d.ts +10 -0
  59. package/dist/presenter/presenter-plan.js +185 -0
  60. package/dist/presenter/presenter-report-styles.js +7 -0
  61. package/dist/presenter/presenter-report.d.ts +1 -1
  62. package/dist/presenter/presenter-report.js +53 -16
  63. package/dist/presenter/presenter-settings-styles.js +10 -0
  64. package/dist/presenter/presenter-settings.d.ts +5 -0
  65. package/dist/presenter/presenter-settings.js +71 -11
  66. package/dist/presenter/presenter-shell-styles.js +39 -42
  67. package/dist/presenter/presenter-shell.d.ts +8 -1
  68. package/dist/presenter/presenter-shell.js +124 -72
  69. package/dist/presenter/presenter-styles.js +9 -1
  70. package/dist/presenter/presenter.d.ts +4 -3
  71. package/dist/presenter/presenter.js +101 -13
  72. package/dist/presenter/tour/tour.d.ts +4 -1
  73. package/dist/presenter/tour/tour.js +6 -4
  74. package/dist/recorder/recorder.js +2 -2
  75. package/dist/registry/auto-testids.js +3 -4
  76. package/dist/registry/declared-platform.js +2 -0
  77. package/dist/reticle-inject.js +880 -513
  78. package/dist/reticle.d.ts +10 -1
  79. package/dist/reticle.js +73 -9
  80. package/dist/review/annotator-styles.d.ts +1 -1
  81. package/dist/review/annotator.d.ts +7 -0
  82. package/dist/review/annotator.js +20 -2
  83. package/dist/review/mark-anchor.js +2 -2
  84. package/dist/session-continuity.js +2 -1
  85. package/dist/storage-keys.d.ts +14 -0
  86. package/dist/storage-keys.js +17 -0
  87. package/package.json +3 -2
@@ -16,12 +16,32 @@ function pointerEventFor(el, type, init) {
16
16
  ? new PointerEvent(type, full)
17
17
  : new MouseEvent(type, full);
18
18
  }
19
+ const BUTTON_HELD = 1;
20
+ const BUTTON_RELEASED = 0;
21
+ function fireAt(el, type, at, buttons, related, detail = 0) {
22
+ const init = {
23
+ bubbles: !(type.endsWith('enter') || type.endsWith('leave')),
24
+ cancelable: true,
25
+ detail,
26
+ clientX: at.x,
27
+ clientY: at.y,
28
+ screenX: at.x,
29
+ screenY: at.y,
30
+ buttons,
31
+ button: 0,
32
+ ...(related !== undefined ? { relatedTarget: related } : {}),
33
+ };
34
+ return el.dispatchEvent(type.startsWith('pointer')
35
+ ? pointerEventFor(el, type, { ...init, pointerId: 1, isPrimary: true })
36
+ : mouseEventFor(el, type, init));
37
+ }
19
38
  export async function fireClickSequence(el, hold, detail) {
20
39
  const doc = el.ownerDocument;
21
40
  const from = doc.activeElement ?? doc.body;
22
- const init = { bubbles: true, cancelable: true, detail: detail ?? 0 };
23
- firePointer(el, 'pointerdown', from);
24
- asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mousedown', init)));
41
+ const at = centreOf(el);
42
+ const d = detail ?? 0;
43
+ asSyntheticInput(() => fireAt(el, 'pointerdown', at, BUTTON_HELD, from, d));
44
+ asSyntheticInput(() => fireAt(el, 'mousedown', at, BUTTON_HELD, undefined, d));
25
45
  if (el.tabIndex >= 0 && 'function' === typeof el.focus)
26
46
  el.focus();
27
47
  let heldMs = 0;
@@ -30,9 +50,9 @@ export async function fireClickSequence(el, hold, detail) {
30
50
  await hold.sleep(hold.ms);
31
51
  heldMs = hold.now() - startedAt;
32
52
  }
33
- firePointer(el, 'pointerup', from);
34
- asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mouseup', init)));
35
- const notPrevented = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click', init)));
53
+ asSyntheticInput(() => fireAt(el, 'pointerup', at, BUTTON_RELEASED, from, d));
54
+ asSyntheticInput(() => fireAt(el, 'mouseup', at, BUTTON_RELEASED, undefined, d));
55
+ const notPrevented = asSyntheticInput(() => fireAt(el, 'click', at, BUTTON_RELEASED, undefined, d));
36
56
  return { prevented: !notPrevented, heldMs };
37
57
  }
38
58
  function isMeasurable(rect) {
@@ -86,8 +106,6 @@ function makeDataTransfer(data) {
86
106
  }
87
107
  return dt;
88
108
  }
89
- const BUTTON_HELD = 1;
90
- const BUTTON_RELEASED = 0;
91
109
  const DRAG_STEPS = 5;
92
110
  function centreOf(el) {
93
111
  const box = el.getBoundingClientRect();
@@ -100,28 +118,11 @@ export async function dragElement(source, target, data) {
100
118
  const dest = target ?? source;
101
119
  const from = centreOf(source);
102
120
  const to = centreOf(dest);
103
- const fire = (el, type, at, buttons, related) => {
104
- const bubbles = !(type.endsWith('enter') || type.endsWith('leave'));
105
- const init = {
106
- bubbles,
107
- cancelable: true,
108
- clientX: at.x,
109
- clientY: at.y,
110
- screenX: at.x,
111
- screenY: at.y,
112
- buttons,
113
- button: 0,
114
- ...(related !== undefined ? { relatedTarget: related } : {}),
115
- };
116
- el.dispatchEvent(type.startsWith('pointer')
117
- ? pointerEventFor(el, type, { ...init, pointerId: 1, isPrimary: true })
118
- : mouseEventFor(el, type, init));
119
- };
120
121
  const sourceRect = source.getBoundingClientRect();
121
122
  const destRect = target !== null ? dest.getBoundingClientRect() : null;
122
123
  const crosses = (rect, p) => p.x >= rect.left && p.x <= rect.right && p.y >= rect.top && p.y <= rect.bottom;
123
- fire(source, 'pointerdown', from, BUTTON_HELD);
124
- fire(source, 'mousedown', from, BUTTON_HELD);
124
+ fireAt(source, 'pointerdown', from, BUTTON_HELD);
125
+ fireAt(source, 'mousedown', from, BUTTON_HELD);
125
126
  await nativeFrame();
126
127
  let leftSource = false;
127
128
  let enteredDest = false;
@@ -129,30 +130,30 @@ export async function dragElement(source, target, data) {
129
130
  const at = lerp(from, to, step / DRAG_STEPS);
130
131
  if (!leftSource && !crosses(sourceRect, at)) {
131
132
  leftSource = true;
132
- fire(source, 'pointerout', at, BUTTON_HELD, dest);
133
- fire(source, 'mouseout', at, BUTTON_HELD, dest);
134
- fire(source, 'pointerleave', at, BUTTON_HELD, dest);
135
- fire(source, 'mouseleave', at, BUTTON_HELD, dest);
133
+ fireAt(source, 'pointerout', at, BUTTON_HELD, dest);
134
+ fireAt(source, 'mouseout', at, BUTTON_HELD, dest);
135
+ fireAt(source, 'pointerleave', at, BUTTON_HELD, dest);
136
+ fireAt(source, 'mouseleave', at, BUTTON_HELD, dest);
136
137
  }
137
138
  if (leftSource && !enteredDest && destRect !== null && crosses(destRect, at)) {
138
139
  enteredDest = true;
139
- fire(dest, 'pointerover', at, BUTTON_HELD, source);
140
- fire(dest, 'mouseover', at, BUTTON_HELD, source);
141
- fire(dest, 'pointerenter', at, BUTTON_HELD, source);
142
- fire(dest, 'mouseenter', at, BUTTON_HELD, source);
140
+ fireAt(dest, 'pointerover', at, BUTTON_HELD, source);
141
+ fireAt(dest, 'mouseover', at, BUTTON_HELD, source);
142
+ fireAt(dest, 'pointerenter', at, BUTTON_HELD, source);
143
+ fireAt(dest, 'mouseenter', at, BUTTON_HELD, source);
143
144
  }
144
- fire(dest, 'pointermove', at, BUTTON_HELD);
145
- fire(dest, 'mousemove', at, BUTTON_HELD);
145
+ fireAt(dest, 'pointermove', at, BUTTON_HELD);
146
+ fireAt(dest, 'mousemove', at, BUTTON_HELD);
146
147
  await nativeFrame();
147
148
  }
148
149
  if (!enteredDest && destRect !== null) {
149
- fire(dest, 'pointerover', to, BUTTON_HELD, source);
150
- fire(dest, 'mouseover', to, BUTTON_HELD, source);
151
- fire(dest, 'pointerenter', to, BUTTON_HELD, source);
152
- fire(dest, 'mouseenter', to, BUTTON_HELD, source);
150
+ fireAt(dest, 'pointerover', to, BUTTON_HELD, source);
151
+ fireAt(dest, 'mouseover', to, BUTTON_HELD, source);
152
+ fireAt(dest, 'pointerenter', to, BUTTON_HELD, source);
153
+ fireAt(dest, 'mouseenter', to, BUTTON_HELD, source);
153
154
  }
154
- fire(dest, 'pointerup', to, BUTTON_RELEASED);
155
- fire(dest, 'mouseup', to, BUTTON_RELEASED);
155
+ fireAt(dest, 'pointerup', to, BUTTON_RELEASED);
156
+ fireAt(dest, 'mouseup', to, BUTTON_RELEASED);
156
157
  let dropPrevented = false;
157
158
  if ('function' === typeof DragEvent) {
158
159
  const dataTransfer = makeDataTransfer(data);
@@ -1,14 +1,20 @@
1
1
  import { type ActionTarget } from '../dom/realm.js';
2
2
  export declare function pressKey(args: Record<string, unknown>): string;
3
- export declare function pressModifiers(args: Record<string, unknown>): {
3
+ export interface ModifierFlags {
4
4
  metaKey: boolean;
5
5
  ctrlKey: boolean;
6
6
  shiftKey: boolean;
7
7
  altKey: boolean;
8
- };
8
+ }
9
+ export declare function pressModifiers(args: Record<string, unknown>): ModifierFlags;
9
10
  export declare function pressCode(args: Record<string, unknown>, key: string): string;
10
11
  export declare function focusedOrDocument(): HTMLElement;
11
12
  export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
12
13
  export declare function pressKeys(args: Record<string, unknown>): string[];
13
- export declare function holdKey(el: ActionTarget, key: string, code: string, mods: Record<string, boolean>, ms: number): Promise<void>;
14
- export declare function pressCombo(el: ActionTarget, keys: readonly string[], mods: Record<string, boolean>, holdMs: number): Promise<boolean>;
14
+ export declare const legacyKeyCode: (key: string) => number;
15
+ export declare function keyboardEvent(type: string, init: KeyboardEventInit & {
16
+ key: string;
17
+ }): KeyboardEvent;
18
+ export declare function holdKey(el: ActionTarget, key: string, code: string, mods: ModifierFlags, ms: number): Promise<void>;
19
+ export declare function pressCombo(el: ActionTarget, keys: readonly string[], mods: ModifierFlags, holdMs: number): Promise<boolean>;
20
+ export declare function closeModalOnEscape(el: ActionTarget, key: string, keydownProceeded: boolean): void;
@@ -1,4 +1,4 @@
1
- import { ActionType, isGlobalPress, pressKeyFromArgs } from '@reticlehq/core';
1
+ import { ActionType, isGlobalPress, pressKeyFromArgs, pressKeysFromArgs } from '@reticlehq/core';
2
2
  import { asSyntheticInput } from './synthetic/synthetic-input.js';
3
3
  import { nativeSetTimeout } from '../timers/native/native-timers.js';
4
4
  const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
@@ -9,16 +9,38 @@ function asString(value, fallback = '') {
9
9
  export function pressKey(args) {
10
10
  return pressKeyFromArgs(args);
11
11
  }
12
+ const MODIFIER_FLAG_KEYS = [
13
+ 'metaKey',
14
+ 'ctrlKey',
15
+ 'shiftKey',
16
+ 'altKey',
17
+ ];
18
+ const MODIFIER_FLAG_ALIASES = {
19
+ meta: 'metaKey',
20
+ cmd: 'metaKey',
21
+ command: 'metaKey',
22
+ super: 'metaKey',
23
+ win: 'metaKey',
24
+ control: 'ctrlKey',
25
+ ctrl: 'ctrlKey',
26
+ shift: 'shiftKey',
27
+ alt: 'altKey',
28
+ option: 'altKey',
29
+ opt: 'altKey',
30
+ };
12
31
  export function pressModifiers(args) {
13
32
  const raw = args['modifiers'];
14
33
  const names = Array.isArray(raw) ? raw.map((m) => asString(m).toLowerCase()) : [];
15
- const has = (...aliases) => aliases.some((a) => names.includes(a));
16
- return {
17
- metaKey: has('meta', 'cmd', 'command', 'super', 'win'),
18
- ctrlKey: has('control', 'ctrl'),
19
- shiftKey: has('shift'),
20
- altKey: has('alt', 'option', 'opt'),
21
- };
34
+ const flags = { metaKey: false, ctrlKey: false, shiftKey: false, altKey: false };
35
+ for (const name of names) {
36
+ const flag = MODIFIER_FLAG_ALIASES[name];
37
+ if (flag !== undefined)
38
+ flags[flag] = true;
39
+ }
40
+ return flags;
41
+ }
42
+ function modifierFlagFor(key) {
43
+ return MODIFIER_FLAG_ALIASES[key.toLowerCase()];
22
44
  }
23
45
  export function pressCode(args, key) {
24
46
  const explicit = args['code'];
@@ -79,19 +101,45 @@ export function isReflessDocumentPress(ref, action, args) {
79
101
  return 0 === ref.length && isGlobalPress(args) && ActionType.PRESS === action;
80
102
  }
81
103
  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);
104
+ return pressKeysFromArgs(args);
86
105
  }
87
106
  const KEY_REPEAT_MS = 33;
88
107
  const KEY_REPEAT_DELAY_MS = 500;
108
+ const LEGACY_KEY_CODES = {
109
+ Backspace: 8,
110
+ Tab: 9,
111
+ Enter: 13,
112
+ Shift: 16,
113
+ Control: 17,
114
+ Alt: 18,
115
+ Escape: 27,
116
+ ' ': 32,
117
+ PageUp: 33,
118
+ PageDown: 34,
119
+ End: 35,
120
+ Home: 36,
121
+ ArrowLeft: 37,
122
+ ArrowUp: 38,
123
+ ArrowRight: 39,
124
+ ArrowDown: 40,
125
+ Delete: 46,
126
+ Meta: 91,
127
+ };
128
+ const SINGLE_ALNUM = /^[a-z0-9]$/i;
129
+ export const legacyKeyCode = (key) => LEGACY_KEY_CODES[key] ?? (SINGLE_ALNUM.test(key) ? key.toUpperCase().charCodeAt(0) : 0);
130
+ export function keyboardEvent(type, init) {
131
+ const event = new KeyboardEvent(type, init);
132
+ const code = legacyKeyCode(init.key);
133
+ for (const legacy of ['keyCode', 'which'])
134
+ Object.defineProperty(event, legacy, { get: () => code });
135
+ return event;
136
+ }
89
137
  export async function holdKey(el, key, code, mods, ms) {
90
138
  const started = Date.now();
91
139
  if (ms > KEY_REPEAT_DELAY_MS)
92
140
  await sleep(KEY_REPEAT_DELAY_MS);
93
141
  while (Date.now() - started < ms) {
94
- asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', {
142
+ asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
95
143
  key,
96
144
  code,
97
145
  bubbles: true,
@@ -103,18 +151,61 @@ export async function holdKey(el, key, code, mods, ms) {
103
151
  }
104
152
  }
105
153
  export async function pressCombo(el, keys, mods, holdMs) {
154
+ const held = { ...mods };
155
+ const dispatch = (type, key) => {
156
+ const code = pressCode({}, key);
157
+ return asSyntheticInput(() => el.dispatchEvent(keyboardEvent(type, { key, code, bubbles: true, cancelable: true, ...held })));
158
+ };
159
+ const presses = new Map();
160
+ for (const flag of MODIFIER_FLAG_KEYS) {
161
+ if (held[flag])
162
+ presses.set(flag, 1);
163
+ }
164
+ const pressFlag = (flag) => {
165
+ presses.set(flag, (presses.get(flag) ?? 0) + 1);
166
+ held[flag] = true;
167
+ };
168
+ const releaseFlag = (flag) => {
169
+ const left = (presses.get(flag) ?? 1) - 1;
170
+ presses.set(flag, left);
171
+ if (left <= 0)
172
+ held[flag] = false;
173
+ };
106
174
  let prevented = false;
107
175
  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 })));
176
+ const flag = modifierFlagFor(key);
177
+ if (flag !== undefined)
178
+ pressFlag(flag);
179
+ const ok = dispatch('keydown', key);
110
180
  if (!ok)
111
181
  prevented = true;
182
+ closeModalOnEscape(el, key, ok);
112
183
  }
113
184
  if (holdMs > 0)
114
185
  await sleep(holdMs);
115
186
  for (const key of [...keys].reverse()) {
116
- const code = pressCode({}, key);
117
- asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
187
+ const flag = modifierFlagFor(key);
188
+ if (flag !== undefined)
189
+ releaseFlag(flag);
190
+ dispatch('keyup', key);
118
191
  }
119
192
  return prevented;
120
193
  }
194
+ export function closeModalOnEscape(el, key, keydownProceeded) {
195
+ if ('Escape' !== key || !keydownProceeded)
196
+ return;
197
+ for (let dialog = el.closest('dialog'); null !== dialog; dialog = dialog.parentElement?.closest('dialog') ?? null) {
198
+ try {
199
+ if (!dialog.open || !dialog.matches(':modal'))
200
+ continue;
201
+ }
202
+ catch {
203
+ return;
204
+ }
205
+ if ('function' === typeof dialog.requestClose)
206
+ dialog.requestClose();
207
+ else if (dialog.dispatchEvent(new Event('cancel', { cancelable: true })))
208
+ dialog.close();
209
+ return;
210
+ }
211
+ }
@@ -27,6 +27,8 @@ interface ActionResult {
27
27
  component?: string;
28
28
  role?: string;
29
29
  name?: string;
30
+ nth?: number;
31
+ of?: number;
30
32
  source?: {
31
33
  file: string;
32
34
  line: number;
@@ -1,4 +1,6 @@
1
- import { ActionType, ActionWarning, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
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';
2
4
  import { asSyntheticInput } from './synthetic/synthetic-input.js';
3
5
  import { echoRef, refs } from '../dom/addressing/refs.js';
4
6
  import { dangerousActionContext, requiresDangerousConfirmation, submitControlFor, } from './danger-context.js';
@@ -9,7 +11,7 @@ import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/r
9
11
  import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
10
12
  import { AppearedText } from './appeared-text.js';
11
13
  import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
12
- import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, } from './actions-press.js';
14
+ import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
13
15
  function asString(value, fallback = '') {
14
16
  return 'string' === typeof value ? value : fallback;
15
17
  }
@@ -22,7 +24,7 @@ function requireElement(ref) {
22
24
  return el;
23
25
  }
24
26
  function anchorOf(el) {
25
- const testid = el.getAttribute('data-testid') ?? undefined;
27
+ const testid = readTestId(el) ?? undefined;
26
28
  const info = identifyComponent(el);
27
29
  const out = {};
28
30
  if (testid !== undefined)
@@ -38,6 +40,14 @@ function anchorOf(el) {
38
40
  out.role = role;
39
41
  if (name.length > 0)
40
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
+ }
41
51
  return out;
42
52
  }
43
53
  const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
@@ -60,6 +70,10 @@ const result = (ref, action, effect, settled, settleReason, anchor, warning) =>
60
70
  base.role = anchor.role;
61
71
  if (anchor.name !== undefined)
62
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;
63
77
  if (warning !== undefined)
64
78
  base.warning = warning;
65
79
  return base;
@@ -137,19 +151,13 @@ function activeRef(el) {
137
151
  return null;
138
152
  return refs.refFor(active);
139
153
  }
140
- const MAX_HOLD_MS = 30_000;
141
- function clampHold(raw) {
142
- if ('number' !== typeof raw || !Number.isFinite(raw) || raw <= 0)
143
- return 0;
144
- return Math.min(raw, MAX_HOLD_MS);
145
- }
146
154
  async function dispatchFor(el, action, args) {
147
155
  if (ActionType.CLICK === action) {
148
- const hold = clampHold(args['holdMs']);
156
+ const hold = clampHoldMs(args['holdMs']);
149
157
  return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
150
158
  }
151
159
  if (ActionType.TAP === action) {
152
- const hold = clampHold(args['holdMs']);
160
+ const hold = clampHoldMs(args['holdMs']);
153
161
  return await fireTapSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
154
162
  }
155
163
  return { prevented: await dispatchOther(el, action, args), heldMs: 0 };
@@ -175,7 +183,7 @@ async function dispatchOther(el, action, args) {
175
183
  el.dispatchEvent(mouseEventFor(el, 'mouseenter', { relatedTarget: from }));
176
184
  firePointer(el, 'pointermove', from);
177
185
  const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true }));
178
- const holdMs = clampHold(args['holdMs']);
186
+ const holdMs = clampHoldMs(args['holdMs']);
179
187
  if (holdMs > 0)
180
188
  await sleep(holdMs);
181
189
  return !moved;
@@ -274,14 +282,15 @@ async function dispatchOther(el, action, args) {
274
282
  const mods = pressModifiers(args);
275
283
  const combo = pressKeys(args);
276
284
  if (combo.length > 0)
277
- return await pressCombo(el, combo, mods, clampHold(args['holdMs']));
285
+ return await pressCombo(el, combo, mods, clampHoldMs(args['holdMs']));
278
286
  const key = pressKey(args);
279
287
  const code = pressCode(args, key);
280
- const down = asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
281
- const hold = clampHold(args['holdMs']);
288
+ const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
289
+ closeModalOnEscape(el, key, down);
290
+ const hold = clampHoldMs(args['holdMs']);
282
291
  if (hold > 0)
283
292
  await holdKey(el, key, code, mods, hold);
284
- asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
293
+ asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
285
294
  return !down;
286
295
  }
287
296
  case ActionType.SCROLL_INTO_VIEW:
@@ -33,7 +33,7 @@ export function dangerousActionContext(el) {
33
33
  const SUBMIT_CONTROL_SELECTOR = 'button[type="submit"], input[type="submit"], button:not([type]):not([type=""])';
34
34
  export function submitControlFor(el) {
35
35
  const found = el.closest('form')?.querySelector(SUBMIT_CONTROL_SELECTOR);
36
- return found instanceof HTMLElement ? found : null;
36
+ return isHtmlElement(found) ? found : null;
37
37
  }
38
38
  export function requiresDangerousConfirmation(text, role) {
39
39
  return isDangerousActionText(text, role);
@@ -6,6 +6,7 @@ export interface ReticleConnectOptions {
6
6
  token?: string;
7
7
  allowNonLocalhost?: boolean;
8
8
  allowInProduction?: boolean;
9
+ testIdAttribute?: string;
9
10
  overlay?: boolean;
10
11
  captureNetworkBodies?: boolean;
11
12
  networkBodyMaxChars?: number;
package/dist/dom/a11y.js CHANGED
@@ -1,4 +1,5 @@
1
- import { ElementState, REDACTED_VALUE } from '@reticlehq/core';
1
+ import { readTestId } from './addressing/testid-attr.js';
2
+ import { DEFAULT_TESTID_ATTR, ElementState, REDACTED_VALUE, } from '@reticlehq/core';
2
3
  import { isButton, isHtmlElement, isImage, isInput, isMeter, isOutput, isProgress, isSelect, isTextArea, } from './realm.js';
3
4
  import { refs } from './addressing/refs.js';
4
5
  import { capturedRootOf } from './shadow-registry.js';
@@ -31,9 +32,11 @@ const NAME_FROM_CONTENT = new Set([
31
32
  'status',
32
33
  'alert',
33
34
  ]);
34
- const INPUT_TEXT_TYPES = new Set(['text', 'email', 'tel', 'url', 'search', 'password', '']);
35
+ const INPUT_TEXT_TYPES = new Set(['text', 'email', 'tel', 'url', 'password', '']);
35
36
  function inputRole(input) {
36
37
  const type = input.type.toLowerCase();
38
+ if ('search' === type)
39
+ return 'searchbox';
37
40
  if (INPUT_TEXT_TYPES.has(type))
38
41
  return 'textbox';
39
42
  if ('checkbox' === type)
@@ -282,7 +285,8 @@ export function isSensitiveField(el) {
282
285
  const identifiers = [
283
286
  el.getAttribute('name') ?? '',
284
287
  el.id,
285
- el.getAttribute('data-testid') ?? '',
288
+ readTestId(el) ?? '',
289
+ el.getAttribute(DEFAULT_TESTID_ATTR) ?? '',
286
290
  el.getAttribute('aria-label') ?? '',
287
291
  ];
288
292
  const sensitiveAutocomplete = /current-password|new-password|cc-number|cc-csc|one-time-code/i.test(autocomplete);
@@ -309,6 +313,11 @@ function hiddenInsideClosedDetails(el) {
309
313
  const summary = Array.from(details.children).find((child) => HTML_SUMMARY_TAG === child.localName);
310
314
  return summary === undefined || !summary.contains(el);
311
315
  }
316
+ function stalledFade(el) {
317
+ if (!el.ownerDocument.hidden || !('getAnimations' in el))
318
+ return false;
319
+ return el.getAnimations().some((a) => 'running' === a.playState || a.pending);
320
+ }
312
321
  function selfHidden(el) {
313
322
  if ('true' === el.getAttribute('aria-hidden'))
314
323
  return true;
@@ -324,7 +333,7 @@ function selfHidden(el) {
324
333
  'collapse' === style.visibility) {
325
334
  return true;
326
335
  }
327
- if (0 === Number.parseFloat(style.opacity || '1'))
336
+ if (0 === Number.parseFloat(style.opacity || '1') && !stalledFade(el))
328
337
  return true;
329
338
  }
330
339
  return false;
@@ -1,4 +1,5 @@
1
1
  import { TRANSPORT_LIMITS, asRef } from '@reticlehq/core';
2
+ import { ReticleStorageKey } from '../../storage-keys.js';
2
3
  export const MAX_TRACKED_REFS = 10000;
3
4
  const REF_ELISION = '…';
4
5
  const REF_PREFIX = 'e';
@@ -7,7 +8,7 @@ export function echoRef(ref) {
7
8
  return max >= ref.length ? ref : `${ref.slice(0, max)}${REF_ELISION}`;
8
9
  }
9
10
  const SWEEP_EVERY_MINTS = 1000;
10
- const REF_BASE_KEY = '__reticle_ref_base';
11
+ const REF_BASE_KEY = ReticleStorageKey.REF_BASE;
11
12
  export const REF_BLOCK = 100;
12
13
  function readBase(store) {
13
14
  try {
@@ -0,0 +1,4 @@
1
+ export declare function getTestIdAttr(): string;
2
+ export declare function testIdSelector(): string;
3
+ export declare function readTestId(el: Element): string | null;
4
+ export declare function setTestIdAttr(name: string | undefined): void;
@@ -0,0 +1,14 @@
1
+ import { DEFAULT_TESTID_ATTR, TESTID_ATTR_PATTERN } from '@reticlehq/core';
2
+ let testIdAttr = DEFAULT_TESTID_ATTR;
3
+ export function getTestIdAttr() {
4
+ return testIdAttr;
5
+ }
6
+ export function testIdSelector() {
7
+ return `[${testIdAttr}]`;
8
+ }
9
+ export function readTestId(el) {
10
+ return el.getAttribute(testIdAttr);
11
+ }
12
+ export function setTestIdAttr(name) {
13
+ testIdAttr = undefined !== name && TESTID_ATTR_PATTERN.test(name) ? name : DEFAULT_TESTID_ATTR;
14
+ }
@@ -1,4 +1,5 @@
1
1
  import { ElementState, type ElementQuery, type MatchResult, type QueryResult } from '@reticlehq/core';
2
2
  export declare const ATTR_VALUE_MAX = 512;
3
3
  export declare function matchQuery(query: ElementQuery, state?: ElementState, limit?: number): MatchResult;
4
+ export declare function elementsMatching(query: ElementQuery): HTMLElement[];
4
5
  export declare function runQuery(query: ElementQuery, limit?: number): QueryResult;