@reticlehq/browser 3.9.0 → 3.10.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 (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,8 +1,9 @@
1
1
  import { readTestId } from '../dom/addressing/testid-attr.js';
2
- import { ActionType, AnchorKind, AnnotationKind, DEGRADED_ANCHOR_ROLE, EventType, FLOW_FILE_VERSION, FlowStepTool, RecorderPhase, PredicateKind, } from '@reticlehq/core';
2
+ import { ActionType, AnchorKind, AnnotationKind, DEGRADED_ANCHOR_ROLE, EventType, FLOW_FILE_VERSION, ACT_STEP_TOOL, RecorderPhase, PredicateKind, } from '@reticlehq/core';
3
3
  import { REDACTED_VALUE } from '@reticlehq/core';
4
4
  import { sourceFromDom } from '../dom/addressing/source.js';
5
5
  import { getAccessibleName, getRole, isSensitiveField } from '../dom/a11y.js';
6
+ import { positionAmongNamesakes } from '../actions/action-report.js';
6
7
  import { isReticleOverlay } from '../dom/dom-ignore.js';
7
8
  import { getCapabilities } from '../registry/capabilities.js';
8
9
  import { TOOLBAR_CSS, BTN_CSS, NAME_CSS, STATUS_CSS, MENU_CSS } from './recorder-styles.js';
@@ -10,7 +11,7 @@ export const RECORDER_EMPTY_MSG = 'recorded 0 steps';
10
11
  const STATUS_RECORDING = 'recording…';
11
12
  const STATUS_IDLE = 'ready';
12
13
  const STATUS_ANNOTATE = 'pick an annotation';
13
- const TOOL = FlowStepTool.ACT;
14
+ const TOOL = ACT_STEP_TOOL;
14
15
  const BUTTON_LABEL = {
15
16
  record: 'Record',
16
17
  stop: 'Stop',
@@ -38,33 +39,45 @@ export function anchorFor(el) {
38
39
  const role = getRole(el);
39
40
  const name = getAccessibleName(el);
40
41
  if (role !== 'generic' && (role.length > 0 || name.length > 0)) {
41
- const anchor = name.length > 0 ? { kind: AnchorKind.ROLE, role, name } : { kind: AnchorKind.ROLE, role };
42
+ const position = positionAmongNamesakes(el, role, name);
43
+ const anchor = {
44
+ kind: AnchorKind.ROLE,
45
+ role,
46
+ ...(name.length > 0 ? { name } : {}),
47
+ ...(null === position ? {} : position),
48
+ };
42
49
  return { anchor, degraded: false };
43
50
  }
44
51
  return { anchor: { kind: AnchorKind.ROLE, role: DEGRADED_ANCHOR_ROLE }, degraded: true };
45
52
  }
46
53
  function isTextbox(el) {
47
- if (el instanceof HTMLTextAreaElement)
54
+ if (el instanceof HTMLTextAreaElement) {
48
55
  return true;
56
+ }
49
57
  return el instanceof HTMLInputElement && 'textbox' === inputRole(el);
50
58
  }
51
59
  function inputRole(el) {
52
60
  const type = el.type.toLowerCase();
53
- if ('checkbox' === type)
61
+ if ('checkbox' === type) {
54
62
  return 'checkbox';
55
- if ('radio' === type)
63
+ }
64
+ if ('radio' === type) {
56
65
  return 'radio';
57
- if (['text', 'email', 'tel', 'url', 'search', 'password', ''].includes(type))
66
+ }
67
+ if (['text', 'email', 'tel', 'url', 'search', 'password', ''].includes(type)) {
58
68
  return 'textbox';
69
+ }
59
70
  return 'other';
60
71
  }
61
72
  function buildStep(el, action, args) {
62
73
  const { anchor, degraded } = anchorFor(el);
63
74
  const step = { tool: TOOL, anchor, action };
64
- if (args !== undefined)
75
+ if (args !== undefined) {
65
76
  step.args = args;
66
- if (degraded)
77
+ }
78
+ if (degraded) {
67
79
  step.degraded = true;
80
+ }
68
81
  const source = sourceFromDom(el);
69
82
  if (source !== undefined) {
70
83
  step.source =
@@ -82,13 +95,15 @@ export function compileRecording(name, steps, annotations, createdAt, startPath)
82
95
  continue;
83
96
  }
84
97
  if (ann.kind === AnnotationKind.SUCCESS_STATE) {
85
- if (ann.signal !== undefined)
98
+ if (ann.signal !== undefined) {
86
99
  success = { kind: PredicateKind.SIGNAL, name: ann.signal };
100
+ }
87
101
  continue;
88
102
  }
89
103
  const target = out.at(-1);
90
- if (target === undefined)
104
+ if (target === undefined) {
91
105
  continue;
106
+ }
92
107
  if (ann.kind === AnnotationKind.ASSERT_SIGNAL && ann.signal !== undefined) {
93
108
  target.expect = alsoAssert(target.expect, { kind: PredicateKind.SIGNAL, name: ann.signal });
94
109
  }
@@ -100,25 +115,30 @@ export function compileRecording(name, steps, annotations, createdAt, startPath)
100
115
  }
101
116
  }
102
117
  const flow = { version: FLOW_FILE_VERSION, name, createdAt, steps: out };
103
- if (startPath !== undefined && startPath.length > 0)
118
+ if (startPath !== undefined && startPath.length > 0) {
104
119
  flow.startPath = startPath;
105
- if (dynamic.length > 0)
120
+ }
121
+ if (dynamic.length > 0) {
106
122
  flow.dynamic = dynamic;
107
- if (success !== undefined)
123
+ }
124
+ if (success !== undefined) {
108
125
  flow.success = success;
126
+ }
109
127
  return flow;
110
128
  }
111
129
  function alsoAssert(existing, clause) {
112
- if (existing === undefined)
130
+ if (existing === undefined) {
113
131
  return clause;
132
+ }
114
133
  if (existing.kind === PredicateKind.ALL_OF) {
115
134
  return { kind: PredicateKind.ALL_OF, predicates: [...existing.predicates, clause] };
116
135
  }
117
136
  return { kind: PredicateKind.ALL_OF, predicates: [existing, clause] };
118
137
  }
119
138
  function anchorToElement(anchor) {
120
- if (anchor.kind === AnchorKind.TESTID)
139
+ if (anchor.kind === AnchorKind.TESTID) {
121
140
  return { testid: anchor.value };
141
+ }
122
142
  if (anchor.kind === AnchorKind.ROLE) {
123
143
  return anchor.name !== undefined
124
144
  ? { role: anchor.role, name: anchor.name }
@@ -148,10 +168,12 @@ class Recorder {
148
168
  return this.#steps.map((s) => ({ ...s }));
149
169
  }
150
170
  mount() {
151
- if (this.#root !== undefined)
171
+ if (this.#root !== undefined) {
152
172
  return;
153
- if ('undefined' === typeof document)
173
+ }
174
+ if ('undefined' === typeof document) {
154
175
  return;
176
+ }
155
177
  this.#buildToolbar();
156
178
  this.#installCapture();
157
179
  }
@@ -179,22 +201,26 @@ class Recorder {
179
201
  }
180
202
  #ignore(ev) {
181
203
  const target = ev.target;
182
- if (!(target instanceof Element))
204
+ if (!(target instanceof Element)) {
183
205
  return undefined;
184
- if (isReticleOverlay(target))
206
+ }
207
+ if (isReticleOverlay(target)) {
185
208
  return undefined;
209
+ }
186
210
  return target;
187
211
  }
188
212
  #onClick(ev) {
189
213
  const target = this.#ignore(ev);
190
- if (target === undefined)
214
+ if (target === undefined) {
191
215
  return;
216
+ }
192
217
  if (this.#phase === RecorderPhase.ANNOTATING) {
193
218
  this.#captureAnnotationTarget(target);
194
219
  return;
195
220
  }
196
- if (this.#phase !== RecorderPhase.RECORDING)
221
+ if (this.#phase !== RecorderPhase.RECORDING) {
197
222
  return;
223
+ }
198
224
  if ((target instanceof HTMLInputElement &&
199
225
  ('checkbox' === inputRole(target) || 'radio' === inputRole(target))) ||
200
226
  target instanceof HTMLSelectElement) {
@@ -204,19 +230,23 @@ class Recorder {
204
230
  this.#steps.push(buildStep(target, ActionType.CLICK));
205
231
  }
206
232
  #onInput(ev) {
207
- if (this.#phase !== RecorderPhase.RECORDING)
233
+ if (this.#phase !== RecorderPhase.RECORDING) {
208
234
  return;
235
+ }
209
236
  const target = this.#ignore(ev);
210
- if (target === undefined || !isTextbox(target))
237
+ if (target === undefined || !isTextbox(target)) {
211
238
  return;
239
+ }
212
240
  this.#pendingFill = { el: target, value: target.value };
213
241
  }
214
242
  #onChange(ev) {
215
- if (this.#phase !== RecorderPhase.RECORDING)
243
+ if (this.#phase !== RecorderPhase.RECORDING) {
216
244
  return;
245
+ }
217
246
  const target = this.#ignore(ev);
218
- if (target === undefined)
247
+ if (target === undefined) {
219
248
  return;
249
+ }
220
250
  if (target instanceof HTMLInputElement && 'checkbox' === inputRole(target)) {
221
251
  this.#flushPendingFill();
222
252
  this.#steps.push(buildStep(target, target.checked ? ActionType.CHECK : ActionType.UNCHECK));
@@ -238,31 +268,36 @@ class Recorder {
238
268
  }
239
269
  }
240
270
  #onSubmit(ev) {
241
- if (this.#phase !== RecorderPhase.RECORDING)
271
+ if (this.#phase !== RecorderPhase.RECORDING) {
242
272
  return;
273
+ }
243
274
  const target = this.#ignore(ev);
244
- if (target === undefined)
275
+ if (target === undefined) {
245
276
  return;
277
+ }
246
278
  this.#flushPendingFill();
247
279
  this.#steps.push(buildStep(target, ActionType.SUBMIT));
248
280
  }
249
281
  #flushPendingFill() {
250
282
  const pending = this.#pendingFill;
251
- if (pending === undefined)
283
+ if (pending === undefined) {
252
284
  return;
285
+ }
253
286
  this.#pendingFill = undefined;
254
287
  const value = isSensitiveField(pending.el) ? REDACTED_VALUE : pending.value;
255
288
  this.#steps.push(buildStep(pending.el, ActionType.FILL, { value }));
256
289
  }
257
290
  #captureAnnotationTarget(target) {
258
291
  const draft = this.#draft;
259
- if (draft === undefined)
292
+ if (draft === undefined) {
260
293
  return;
294
+ }
261
295
  const { anchor } = anchorFor(target);
262
296
  const ann = { kind: draft.kind, anchor };
263
297
  const signal = this.#selectedSignal();
264
- if (signal !== undefined)
298
+ if (signal !== undefined) {
265
299
  ann.signal = signal;
300
+ }
266
301
  this.#annotations.push(ann);
267
302
  this.#draft = undefined;
268
303
  this.#setPhase(RecorderPhase.RECORDING);
@@ -270,8 +305,9 @@ class Recorder {
270
305
  }
271
306
  #confirmAnnotationOnPrior() {
272
307
  const draft = this.#draft;
273
- if (draft === undefined)
308
+ if (draft === undefined) {
274
309
  return;
310
+ }
275
311
  const last = this.#steps.at(-1);
276
312
  if (last === undefined) {
277
313
  this.#draft = undefined;
@@ -281,8 +317,9 @@ class Recorder {
281
317
  }
282
318
  const ann = { kind: draft.kind, anchor: last.anchor };
283
319
  const signal = this.#selectedSignal();
284
- if (signal !== undefined)
320
+ if (signal !== undefined) {
285
321
  ann.signal = signal;
322
+ }
286
323
  this.#annotations.push(ann);
287
324
  this.#draft = undefined;
288
325
  this.#setPhase(RecorderPhase.RECORDING);
@@ -312,8 +349,9 @@ class Recorder {
312
349
  this.#closeMenu();
313
350
  }
314
351
  #openAnnotateMenu() {
315
- if (this.#phase !== RecorderPhase.RECORDING)
352
+ if (this.#phase !== RecorderPhase.RECORDING) {
316
353
  return;
354
+ }
317
355
  this.#setPhase(RecorderPhase.ANNOTATING);
318
356
  this.#setStatus(STATUS_ANNOTATE);
319
357
  this.#renderMenu();
@@ -357,12 +395,14 @@ class Recorder {
357
395
  }
358
396
  #renderMenu() {
359
397
  const menu = this.#menuEl;
360
- if (menu === undefined)
398
+ if (menu === undefined) {
361
399
  return;
400
+ }
362
401
  menu.textContent = '';
363
402
  for (const kind of Object.values(AnnotationKind)) {
364
- if (kind === AnnotationKind.INTENT)
403
+ if (kind === AnnotationKind.INTENT) {
365
404
  continue;
405
+ }
366
406
  const item = document.createElement('button');
367
407
  item.setAttribute('data-reticle-annkind', kind);
368
408
  item.textContent = ANNOTATION_LABEL[kind];
@@ -374,8 +414,9 @@ class Recorder {
374
414
  #chooseKind(kind) {
375
415
  this.#draft = { kind };
376
416
  const menu = this.#menuEl;
377
- if (menu === undefined)
417
+ if (menu === undefined) {
378
418
  return;
419
+ }
379
420
  if (NEEDS_SIGNAL.has(kind)) {
380
421
  const select = document.createElement('select');
381
422
  select.setAttribute('data-reticle-signal', '');
@@ -396,12 +437,14 @@ class Recorder {
396
437
  menu.appendChild(confirm);
397
438
  }
398
439
  #closeMenu() {
399
- if (this.#menuEl !== undefined)
440
+ if (this.#menuEl !== undefined) {
400
441
  this.#menuEl.textContent = '';
442
+ }
401
443
  }
402
444
  #setStatus(text) {
403
- if (this.#statusEl !== undefined)
445
+ if (this.#statusEl !== undefined) {
404
446
  this.#statusEl.textContent = text;
447
+ }
405
448
  }
406
449
  #nameField() {
407
450
  const input = this.#root?.querySelector('[data-reticle-name]');