@reticlehq/browser 3.7.0 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/dist/actions/action-report.d.ts +10 -0
  2. package/dist/actions/action-report.js +29 -2
  3. package/dist/actions/actions-dom.d.ts +4 -0
  4. package/dist/actions/actions-dom.js +20 -4
  5. package/dist/actions/actions.d.ts +1 -0
  6. package/dist/actions/actions.js +35 -17
  7. package/dist/actions/appeared-text.js +2 -2
  8. package/dist/actions/click-target.d.ts +1 -0
  9. package/dist/actions/click-target.js +14 -0
  10. package/dist/actions/danger-context.d.ts +2 -1
  11. package/dist/actions/danger-context.js +20 -5
  12. package/dist/activate.d.ts +14 -0
  13. package/dist/activate.js +43 -0
  14. package/dist/busy.d.ts +3 -0
  15. package/dist/busy.js +42 -0
  16. package/dist/commands/commands.js +45 -17
  17. package/dist/connect-options.d.ts +12 -1
  18. package/dist/connect-options.js +18 -1
  19. package/dist/dom/a11y.d.ts +2 -0
  20. package/dist/dom/a11y.js +191 -159
  21. package/dist/dom/chart.d.ts +3 -2
  22. package/dist/dom/chart.js +3 -2
  23. package/dist/dom/dom-ignore.d.ts +6 -0
  24. package/dist/dom/dom-ignore.js +43 -1
  25. package/dist/dom/layout-audit.d.ts +15 -0
  26. package/dist/dom/layout-audit.js +208 -0
  27. package/dist/dom/paint-context.d.ts +15 -0
  28. package/dist/dom/paint-context.js +78 -0
  29. package/dist/dom/query.d.ts +2 -1
  30. package/dist/dom/query.js +97 -34
  31. package/dist/dom/shadow-registry.d.ts +1 -0
  32. package/dist/dom/shadow-registry.js +27 -0
  33. package/dist/dom/snapshot.d.ts +5 -0
  34. package/dist/dom/snapshot.js +85 -34
  35. package/dist/dom/stable-selector.d.ts +1 -0
  36. package/dist/dom/stable-selector.js +19 -0
  37. package/dist/dom/text-match.d.ts +12 -0
  38. package/dist/dom/text-match.js +115 -0
  39. package/dist/dom/theme.d.ts +1 -0
  40. package/dist/dom/theme.js +21 -9
  41. package/dist/dom/virtualized.js +5 -1
  42. package/dist/index.d.ts +2 -1
  43. package/dist/index.js +5 -0
  44. package/dist/observers/blind-spots.js +3 -2
  45. package/dist/observers/canvas.d.ts +18 -0
  46. package/dist/observers/canvas.js +76 -0
  47. package/dist/observers/dom.js +66 -18
  48. package/dist/observers/field.d.ts +11 -0
  49. package/dist/observers/field.js +117 -13
  50. package/dist/observers/install-all.js +4 -0
  51. package/dist/observers/media.d.ts +2 -0
  52. package/dist/observers/media.js +32 -0
  53. package/dist/observers/net-detail/network-body.d.ts +2 -0
  54. package/dist/observers/net-detail/network-body.js +8 -1
  55. package/dist/observers/network.js +26 -6
  56. package/dist/observers/route.d.ts +2 -0
  57. package/dist/observers/route.js +33 -4
  58. package/dist/presenter/chrome/presenter-report-copy.d.ts +0 -5
  59. package/dist/presenter/chrome/presenter-report-copy.js +0 -14
  60. package/dist/presenter/hud-telemetry.js +1 -1
  61. package/dist/presenter/presenter-chat-views.d.ts +3 -4
  62. package/dist/presenter/presenter-chat-views.js +47 -20
  63. package/dist/presenter/presenter-config.d.ts +1 -0
  64. package/dist/presenter/presenter-config.js +1 -0
  65. package/dist/presenter/presenter-frame.d.ts +3 -2
  66. package/dist/presenter/presenter-frame.js +24 -15
  67. package/dist/presenter/presenter-harness-row.d.ts +4 -15
  68. package/dist/presenter/presenter-harness-row.js +11 -50
  69. package/dist/presenter/presenter-report-styles.js +0 -17
  70. package/dist/presenter/presenter-report.d.ts +1 -2
  71. package/dist/presenter/presenter-report.js +4 -54
  72. package/dist/presenter/presenter-run-state.js +2 -1
  73. package/dist/presenter/presenter-settings.d.ts +2 -3
  74. package/dist/presenter/presenter-settings.js +14 -17
  75. package/dist/presenter/presenter-shell.d.ts +6 -4
  76. package/dist/presenter/presenter-shell.js +37 -7
  77. package/dist/presenter/presenter.js +32 -11
  78. package/dist/registry/store-adapters.d.ts +5 -2
  79. package/dist/registry/store-adapters.js +40 -10
  80. package/dist/reticle-inject.js +602 -611
  81. package/dist/reticle.d.ts +4 -2
  82. package/dist/reticle.js +36 -6
  83. package/dist/security/page-location.d.ts +2 -0
  84. package/dist/security/page-location.js +7 -0
  85. package/dist/security/serialization.js +1 -1
  86. package/dist/storage-keys.d.ts +1 -0
  87. package/dist/storage-keys.js +1 -0
  88. package/dist/timers/native/native-timers.d.ts +1 -0
  89. package/dist/timers/native/native-timers.js +5 -0
  90. package/dist/transport/transport.d.ts +1 -0
  91. package/dist/transport/transport.js +18 -0
  92. package/package.json +6 -2
@@ -13,9 +13,14 @@ export interface ActionEffect {
13
13
  occluded: boolean;
14
14
  occludedBy: string | null;
15
15
  scrolledIntoView: boolean;
16
+ occluderName?: string;
17
+ noSize?: true;
18
+ offViewport?: true;
16
19
  heldMs?: number;
17
20
  alreadyAtValue?: true;
21
+ canvases?: number;
18
22
  }
23
+ export declare function visibleCanvases(doc: Document): number;
19
24
  export interface ActionResult {
20
25
  ok: true;
21
26
  ref: string;
@@ -25,6 +30,7 @@ export interface ActionResult {
25
30
  settleReason: SettleReason | null;
26
31
  effect: ActionEffect;
27
32
  testid?: string;
33
+ testidOf?: number;
28
34
  component?: string;
29
35
  role?: string;
30
36
  name?: string;
@@ -35,10 +41,13 @@ export interface ActionResult {
35
41
  line: number;
36
42
  column?: number;
37
43
  };
44
+ secret?: true;
38
45
  warning?: string;
46
+ reticleUi?: true;
39
47
  }
40
48
  export interface CapturedAnchor {
41
49
  testid?: string;
50
+ testidOf?: number;
42
51
  component?: string;
43
52
  source?: {
44
53
  file: string;
@@ -49,6 +58,7 @@ export interface CapturedAnchor {
49
58
  name?: string;
50
59
  nth?: number;
51
60
  of?: number;
61
+ secret?: true;
52
62
  }
53
63
  export declare function anchorOf(el: Element): CapturedAnchor;
54
64
  export declare const result: (ref: string, action: string, effect: ActionEffect, settled: boolean, settleReason: SettleReason | null, anchor: CapturedAnchor, warning?: string) => ActionResult;
@@ -1,14 +1,35 @@
1
1
  import { readTestId } from '../dom/addressing/testid-attr.js';
2
2
  import { QueryBy } from '@reticlehq/core';
3
3
  import { elementsMatching } from '../dom/query.js';
4
- import { getAccessibleName, getRole } from '../dom/a11y.js';
4
+ import { getAccessibleName, getRole, isSensitiveField } from '../dom/a11y.js';
5
5
  import { identifyComponent } from '../registry/stores/adapters.js';
6
+ import { isReticleUi } from '../dom/dom-ignore.js';
7
+ const CANVAS_SELECTOR = 'canvas';
8
+ export function visibleCanvases(doc) {
9
+ const width = doc.defaultView?.innerWidth ?? 0;
10
+ const height = doc.defaultView?.innerHeight ?? 0;
11
+ let count = 0;
12
+ for (const canvas of doc.querySelectorAll(CANVAS_SELECTOR)) {
13
+ if (isReticleUi(canvas))
14
+ continue;
15
+ const box = canvas.getBoundingClientRect();
16
+ const sized = box.width > 0 && box.height > 0;
17
+ const onScreen = box.bottom > 0 && box.right > 0 && box.top < height && box.left < width;
18
+ if (sized && onScreen)
19
+ count += 1;
20
+ }
21
+ return count;
22
+ }
6
23
  export function anchorOf(el) {
7
24
  const testid = readTestId(el) ?? undefined;
8
25
  const info = identifyComponent(el);
9
26
  const out = {};
10
- if (testid !== undefined)
27
+ if (testid !== undefined) {
11
28
  out.testid = testid;
29
+ const shared = elementsMatching({ by: QueryBy.TESTID, value: testid }).length;
30
+ if (1 < shared)
31
+ out.testidOf = shared;
32
+ }
12
33
  const component = info?.componentStack[0];
13
34
  if (component !== undefined)
14
35
  out.component = component;
@@ -20,6 +41,8 @@ export function anchorOf(el) {
20
41
  out.role = role;
21
42
  if (name.length > 0)
22
43
  out.name = name;
44
+ if (isSensitiveField(el))
45
+ out.secret = true;
23
46
  if (role.length > 0 && name.length > 0) {
24
47
  const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
25
48
  const at = same.indexOf(el);
@@ -42,6 +65,8 @@ export const result = (ref, action, effect, settled, settleReason, anchor, warni
42
65
  };
43
66
  if (anchor.testid !== undefined)
44
67
  base.testid = anchor.testid;
68
+ if (anchor.testidOf !== undefined)
69
+ base.testidOf = anchor.testidOf;
45
70
  if (anchor.component !== undefined)
46
71
  base.component = anchor.component;
47
72
  if (anchor.source !== undefined)
@@ -54,6 +79,8 @@ export const result = (ref, action, effect, settled, settleReason, anchor, warni
54
79
  base.nth = anchor.nth;
55
80
  if (anchor.of !== undefined)
56
81
  base.of = anchor.of;
82
+ if (anchor.secret !== undefined)
83
+ base.secret = anchor.secret;
57
84
  if (warning !== undefined)
58
85
  base.warning = warning;
59
86
  return base;
@@ -3,6 +3,9 @@ interface ClickGeometry {
3
3
  occluded: boolean;
4
4
  occludedBy: string | null;
5
5
  scrolledIntoView: boolean;
6
+ occluderName?: string;
7
+ noSize?: true;
8
+ offViewport?: true;
6
9
  }
7
10
  export declare const NO_GEOMETRY: ClickGeometry;
8
11
  export declare function mouseEventFor(el: Element, type: string, init?: MouseEventInit): MouseEvent;
@@ -14,6 +17,7 @@ export declare function fireClickSequence(el: ActionTarget, hold?: {
14
17
  prevented: boolean;
15
18
  heldMs: number;
16
19
  }>;
20
+ export declare function isOffViewport(el: Element, rect: DOMRect): boolean;
17
21
  export declare function clickGeometry(el: ActionTarget): ClickGeometry;
18
22
  export declare function firePointer(el: Element, type: string, relatedTarget?: EventTarget | null): void;
19
23
  export declare function firePointerNonBubbling(el: Element, type: string, relatedTarget?: EventTarget | null): void;
@@ -1,4 +1,6 @@
1
1
  import { refs } from '../dom/addressing/refs.js';
2
+ import { readTestId } from '../dom/addressing/testid-attr.js';
3
+ import { getAccessibleName, getRole } from '../dom/a11y.js';
2
4
  import { hitTestOccluder } from '../dom/occlusion.js';
3
5
  import { nativeFrame } from '../timers/native/native-timers.js';
4
6
  import { asSyntheticInput } from './synthetic/synthetic-input.js';
@@ -58,7 +60,7 @@ export async function fireClickSequence(el, hold, detail) {
58
60
  function isMeasurable(rect) {
59
61
  return rect.width > 0 || rect.height > 0;
60
62
  }
61
- function isOffViewport(el, rect) {
63
+ export function isOffViewport(el, rect) {
62
64
  const win = el.ownerDocument.defaultView;
63
65
  if (null === win)
64
66
  return false;
@@ -70,21 +72,35 @@ function hitTest(el, rect) {
70
72
  const top = hitTestOccluder(el, rect);
71
73
  return null === top
72
74
  ? { occluded: false, occludedBy: null }
73
- : { occluded: true, occludedBy: refs.refFor(top) };
75
+ : { occluded: true, occludedBy: refs.refFor(top), occluderName: nameOf(top) };
76
+ }
77
+ function nameOf(el) {
78
+ const testid = readTestId(el);
79
+ if (testid !== null && testid.length > 0)
80
+ return `[data-testid="${testid}"]`;
81
+ const name = getAccessibleName(el).trim();
82
+ return name.length > 0 ? `${getRole(el)} "${name.slice(0, 40)}"` : el.tagName.toLowerCase();
83
+ }
84
+ function hasNoSize(el) {
85
+ if (typeof el.getClientRects !== 'function')
86
+ return false;
87
+ const rects = Array.from(el.getClientRects());
88
+ return rects.length > 0 && rects.every((r) => 0 === r.width || 0 === r.height);
74
89
  }
75
90
  export function clickGeometry(el) {
76
91
  if (typeof el.getBoundingClientRect !== 'function')
77
92
  return NO_GEOMETRY;
78
93
  let rect = el.getBoundingClientRect();
79
94
  if (!isMeasurable(rect))
80
- return NO_GEOMETRY;
95
+ return hasNoSize(el) ? { ...NO_GEOMETRY, noSize: true } : NO_GEOMETRY;
81
96
  let scrolledIntoView = false;
82
97
  if (isOffViewport(el, rect) && 'function' === typeof el.scrollIntoView) {
83
98
  el.scrollIntoView({ block: 'center', inline: 'center' });
84
99
  scrolledIntoView = true;
85
100
  rect = el.getBoundingClientRect();
86
101
  }
87
- return { ...hitTest(el, rect), scrolledIntoView };
102
+ const stillOff = scrolledIntoView && isOffViewport(el, rect);
103
+ return { ...hitTest(el, rect), scrolledIntoView, ...(stillOff ? { offViewport: true } : {}) };
88
104
  }
89
105
  export function firePointer(el, type, relatedTarget = null) {
90
106
  asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: true, cancelable: true, relatedTarget })));
@@ -22,6 +22,7 @@ interface SequenceStepResult {
22
22
  line: number;
23
23
  column?: number;
24
24
  };
25
+ secret?: true;
25
26
  warning?: string;
26
27
  }
27
28
  export declare function executeSequence(steps: ActionStep[]): Promise<{
@@ -1,16 +1,19 @@
1
1
  import { ActionType, ActionWarning, clampHoldMs, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
2
+ import { isHiddenNativeOption } from '../dom/query.js';
3
+ import { isReticleUi } from '../dom/dom-ignore.js';
2
4
  import { asSyntheticInput } from './synthetic/synthetic-input.js';
3
5
  import { echoRef, refs } from '../dom/addressing/refs.js';
4
- import { dangerousActionContext, requiresDangerousConfirmation, assertOutwardAllowed, submitControlFor, } from './danger-context.js';
6
+ import { requiresDangerousConfirmation, assertOutwardAllowed, assertPaymentEntryAllowed, submitControlFor, } from './danger-context.js';
5
7
  import { assertEditable, assertNotRichText, isAutofillSafeTarget, setNativeValue, } from './value-input.js';
6
- import { getRole, isVisible, getStates } from '../dom/a11y.js';
8
+ import { isVisible, getStates } from '../dom/a11y.js';
7
9
  import { elementHasHoverHandlers } from '../registry/stores/adapters.js';
8
10
  import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/realm.js';
9
- import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
11
+ import { asSettleBudget, nativeSetTimeout, settle } from '../timers/native/native-timers.js';
12
+ import { publishSettledField, watchActedField } from '../observers/field.js';
10
13
  import { AppearedText } from './appeared-text.js';
11
14
  import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
12
15
  import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
13
- import { anchorOf, result } from './action-report.js';
16
+ import { anchorOf, result, visibleCanvases, } from './action-report.js';
14
17
  function asString(value, fallback = '') {
15
18
  return 'string' === typeof value ? value : fallback;
16
19
  }
@@ -69,16 +72,19 @@ function assertActionAllowed(el, action, args) {
69
72
  const submitter = action === ActionType.PRESS && 'Enter' === pressKey(args) ? submitControlFor(el) : null;
70
73
  if (canTrigger)
71
74
  assertOutwardAllowed([el, submitter, isActionTarget(dragTarget) ? dragTarget : null]);
72
- const sourceDangerous = requiresDangerousConfirmation(dangerousActionContext(el), getRole(el)) ||
73
- (submitter !== null &&
74
- requiresDangerousConfirmation(dangerousActionContext(submitter), getRole(submitter)));
75
- const targetDangerous = isActionTarget(dragTarget) &&
76
- requiresDangerousConfirmation(dangerousActionContext(dragTarget), getRole(dragTarget));
75
+ if (action === ActionType.FILL || action === ActionType.TYPE)
76
+ assertPaymentEntryAllowed(el);
77
+ const sourceDangerous = requiresDangerousConfirmation(el) ||
78
+ (submitter !== null && requiresDangerousConfirmation(submitter));
79
+ const targetDangerous = isActionTarget(dragTarget) && requiresDangerousConfirmation(dragTarget);
77
80
  if (canTrigger &&
78
81
  (sourceDangerous || targetDangerous) &&
79
82
  args[DANGEROUS_ACTION_CONFIRM_ARG] !== true) {
80
83
  throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
81
84
  }
85
+ if (CLICK_LIKE.has(action) && el instanceof HTMLElement && isHiddenNativeOption(el)) {
86
+ throw new Error('cannot click a native <option> inside a visually-hidden <select> — open the dropdown and target the visible option');
87
+ }
82
88
  }
83
89
  function optionValueFor(options, named) {
84
90
  if (options.some((option) => option.value === named))
@@ -88,6 +94,7 @@ function optionValueFor(options, named) {
88
94
  }
89
95
  import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, } from './actions-dom.js';
90
96
  import { scrollFrom } from './scroll.js';
97
+ import { clickHitsNoControl } from './click-target.js';
91
98
  function enabledOf(el) {
92
99
  return !getStates(el).includes(ElementState.DISABLED);
93
100
  }
@@ -299,12 +306,6 @@ async function dispatchOther(el, action, args) {
299
306
  throw new Error(`unknown action '${action}'`);
300
307
  }
301
308
  }
302
- function asSettleBudget(raw) {
303
- if ('number' !== typeof raw || !Number.isFinite(raw))
304
- return undefined;
305
- return Math.max(0, Math.min(raw, FRAME_BUDGET_MS_CEILING));
306
- }
307
- const FRAME_BUDGET_MS_CEILING = 200;
308
309
  export async function executeAction(ref, action, args = {}) {
309
310
  const named = 0 < ref.length;
310
311
  const el = isReflessDocumentPress(ref, action, args) ? focusedOrDocument() : requireElement(ref);
@@ -317,8 +318,10 @@ export async function executeAction(ref, action, args = {}) {
317
318
  const enabled = enabledOf(el);
318
319
  const prevFocus = activeRef(el);
319
320
  const valueBefore = valueOf(el);
321
+ const actedField = watchActedField(el);
320
322
  const geometry = CLICK_LIKE.has(action) ? clickGeometry(el) : NO_GEOMETRY;
321
323
  const alreadyAtValue = alreadyAtCheckedState(el, action);
324
+ const hitsNoControl = clickHitsNoControl(el, action);
322
325
  let mutated = 0;
323
326
  const said = new AppearedText();
324
327
  const obs = new MutationObserver((records) => {
@@ -346,9 +349,11 @@ export async function executeAction(ref, action, args = {}) {
346
349
  settled = outcome.settled;
347
350
  settleReason = outcome.settled ? null : SettleReason.TIMEOUT;
348
351
  obs.disconnect();
352
+ actedField?.stop();
349
353
  }
350
354
  assertAriaToggleLanded(el, action, alreadyAtValue);
351
355
  const valueAfter = valueOf(el);
356
+ publishSettledField(el, actedField);
352
357
  const nextFocus = activeRef(el);
353
358
  const effect = {
354
359
  dispatched: true,
@@ -364,8 +369,12 @@ export async function executeAction(ref, action, args = {}) {
364
369
  occluded: geometry.occluded,
365
370
  occludedBy: geometry.occludedBy,
366
371
  scrolledIntoView: geometry.scrolledIntoView,
372
+ ...(geometry.occluderName === undefined ? {} : { occluderName: geometry.occluderName }),
373
+ ...(true === geometry.noSize ? { noSize: true } : {}),
374
+ ...(true === geometry.offViewport ? { offViewport: true } : {}),
367
375
  ...(heldMs > 0 ? { heldMs } : {}),
368
376
  ...(alreadyAtValue ? { alreadyAtValue: true } : {}),
377
+ ...canvasesOnScreen(),
369
378
  };
370
379
  const warning = !named
371
380
  ? ActionWarning.GLOBAL_PRESS
@@ -373,8 +382,15 @@ export async function executeAction(ref, action, args = {}) {
373
382
  ? ActionWarning.CLICK_OCCLUDED
374
383
  : action === ActionType.HOVER && elementHasHoverHandlers(el)
375
384
  ? ActionWarning.HOVER_NATIVE_ENTER_LEAVE
376
- : undefined;
377
- return result(ref, action, effect, settled, settleReason, anchor, warning);
385
+ : hitsNoControl
386
+ ? ActionWarning.CLICK_NOT_A_CONTROL
387
+ : undefined;
388
+ const done = result(ref, action, effect, settled, settleReason, anchor, warning);
389
+ return isReticleUi(el) ? { ...done, reticleUi: true } : done;
390
+ }
391
+ function canvasesOnScreen() {
392
+ const canvases = visibleCanvases(document);
393
+ return canvases > 0 ? { canvases } : {};
378
394
  }
379
395
  const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
380
396
  export async function executeSequence(steps) {
@@ -400,6 +416,8 @@ export async function executeSequence(steps) {
400
416
  stepBase.name = res.name;
401
417
  if (res.source !== undefined)
402
418
  stepBase.source = res.source;
419
+ if (res.secret !== undefined)
420
+ stepBase.secret = res.secret;
403
421
  if (res.warning !== undefined)
404
422
  stepBase.warning = res.warning;
405
423
  stepResults.push(stepBase);
@@ -1,4 +1,4 @@
1
- import { isIgnored } from '../dom/dom-ignore.js';
1
+ import { isWordsIgnored } from '../dom/dom-ignore.js';
2
2
  import { isSensitiveField } from '../dom/a11y.js';
3
3
  const APPEARED_MAX = 200;
4
4
  const JOIN = ' | ';
@@ -45,7 +45,7 @@ export class AppearedText {
45
45
  return this.#length > APPEARED_MAX;
46
46
  }
47
47
  #add(raw, owner) {
48
- if (null !== owner && isIgnored(owner))
48
+ if (null !== owner && isWordsIgnored(owner))
49
49
  return;
50
50
  if (null !== owner && isSensitiveField(owner))
51
51
  return;
@@ -0,0 +1 @@
1
+ export declare function clickHitsNoControl(el: Element, action: string): boolean;
@@ -0,0 +1,14 @@
1
+ import { ActionType } from '@reticlehq/core';
2
+ import { getRole } from '../dom/a11y.js';
3
+ import { parentAcrossShadowBoundary } from '../dom/shadow-registry.js';
4
+ import { INTERACTIVE } from '../dom/snapshot.js';
5
+ const CLICK_FORWARDING = 'label, summary, [onclick], [tabindex], [contenteditable]:not([contenteditable="false"])';
6
+ export function clickHitsNoControl(el, action) {
7
+ if (ActionType.CLICK !== action && ActionType.DBLCLICK !== action)
8
+ return false;
9
+ for (let node = el; node !== null; node = parentAcrossShadowBoundary(node)) {
10
+ if (INTERACTIVE.has(getRole(node)) || node.matches(CLICK_FORWARDING))
11
+ return false;
12
+ }
13
+ return 'pointer' !== getComputedStyle(el).cursor;
14
+ }
@@ -1,6 +1,7 @@
1
1
  import { type ActionTarget } from '../dom/realm.js';
2
2
  export declare function dangerousActionContext(el: ActionTarget): string;
3
3
  export declare function submitControlFor(el: ActionTarget): HTMLElement | null;
4
- export declare function requiresDangerousConfirmation(text: string, role?: string): boolean;
4
+ export declare function requiresDangerousConfirmation(el: ActionTarget): boolean;
5
5
  export declare function setOutwardPolicy(allowed: readonly string[] | undefined): void;
6
6
  export declare function assertOutwardAllowed(targets: readonly (ActionTarget | null)[]): void;
7
+ export declare function assertPaymentEntryAllowed(el: ActionTarget): void;
@@ -1,4 +1,4 @@
1
- import { isDangerousActionText, outwardActionOf, outwardRefusal } from '@reticlehq/core';
1
+ import { OutwardAction, isDangerousActionText, isInertHref, isDangerousNavigation, isPaymentField, outwardActionOf, outwardRefusal, } from '@reticlehq/core';
2
2
  import { getAccessibleName, getRole } from '../dom/a11y.js';
3
3
  import { isHtmlElement } from '../dom/realm.js';
4
4
  const LABEL_VALUED_INPUT_TYPES = new Set([
@@ -17,12 +17,11 @@ function holdsUserText(el) {
17
17
  return false;
18
18
  }
19
19
  const ANCHOR_TAG = 'A';
20
- const INERT_HREF = /^\s*(#.*|javascript:.*)?$/i;
21
20
  function navigatingHref(el) {
22
21
  if (ANCHOR_TAG !== el.tagName)
23
22
  return undefined;
24
23
  const href = el.getAttribute('href');
25
- return null === href || INERT_HREF.test(href) ? undefined : href;
24
+ return null === href || isInertHref(href) ? undefined : href;
26
25
  }
27
26
  export function dangerousActionContext(el) {
28
27
  const href = navigatingHref(el);
@@ -46,8 +45,11 @@ export function submitControlFor(el) {
46
45
  const found = el.closest('form')?.querySelector(SUBMIT_CONTROL_SELECTOR);
47
46
  return isHtmlElement(found) ? found : null;
48
47
  }
49
- export function requiresDangerousConfirmation(text, role) {
50
- return isDangerousActionText(text, role);
48
+ export function requiresDangerousConfirmation(el) {
49
+ const href = navigatingHref(el);
50
+ if (href !== undefined)
51
+ return isDangerousNavigation(href);
52
+ return isDangerousActionText(dangerousActionContext(el), getRole(el));
51
53
  }
52
54
  let outwardAllowed;
53
55
  export function setOutwardPolicy(allowed) {
@@ -64,3 +66,16 @@ export function assertOutwardAllowed(targets) {
64
66
  throw new Error(outwardRefusal(kind));
65
67
  }
66
68
  }
69
+ export function assertPaymentEntryAllowed(el) {
70
+ if (outwardAllowed === undefined || outwardAllowed.includes(OutwardAction.PAY))
71
+ return;
72
+ const facts = {
73
+ autocomplete: el.getAttribute('autocomplete') ?? undefined,
74
+ name: el.getAttribute('name') ?? undefined,
75
+ id: el.getAttribute('id') ?? undefined,
76
+ inputMode: el.getAttribute('inputmode') ?? undefined,
77
+ label: `${getAccessibleName(el)} ${el.getAttribute('placeholder') ?? ''}`,
78
+ };
79
+ if (isPaymentField(facts))
80
+ throw new Error(outwardRefusal(OutwardAction.PAY));
81
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReticleConnectOptions } from './connect-options.js';
2
+ export interface ActivateOptions {
3
+ publicKey: string;
4
+ }
5
+ export interface ActivateHost {
6
+ location: Pick<Location, 'href' | 'origin'>;
7
+ history: Pick<History, 'replaceState' | 'state'>;
8
+ now: () => number;
9
+ connect: () => Promise<{
10
+ connect: (options: ReticleConnectOptions) => void;
11
+ }>;
12
+ warn: (message: string) => void;
13
+ }
14
+ export declare function activate(options: ActivateOptions, host?: ActivateHost | undefined): Promise<boolean>;
@@ -0,0 +1,43 @@
1
+ import { HelloEnv, PRODUCTION_ACTIVATE_PARAM, verifyActivationTicket, } from '@reticlehq/core/activation';
2
+ const REFUSED_PREFIX = '[Reticle] activation refused: ';
3
+ function pageHost() {
4
+ if ('undefined' === typeof location || 'undefined' === typeof history)
5
+ return undefined;
6
+ return {
7
+ location,
8
+ history,
9
+ now: () => Date.now(),
10
+ connect: () => import('./reticle-singleton.js').then((sdk) => sdk.reticle),
11
+ warn: (message) => console.warn(message),
12
+ };
13
+ }
14
+ export async function activate(options, host = pageHost()) {
15
+ if (host === undefined)
16
+ return false;
17
+ const url = new URL(host.location.href);
18
+ const ticket = url.searchParams.get(PRODUCTION_ACTIVATE_PARAM);
19
+ if (null === ticket)
20
+ return false;
21
+ url.searchParams.delete(PRODUCTION_ACTIVATE_PARAM);
22
+ host.history.replaceState(host.history.state, '', url.toString());
23
+ const verdict = await verifyActivationTicket(ticket, {
24
+ publicKey: options.publicKey,
25
+ origin: host.location.origin,
26
+ now: host.now(),
27
+ });
28
+ if (!verdict.ok) {
29
+ host.warn(`${REFUSED_PREFIX}${verdict.reason}`);
30
+ return false;
31
+ }
32
+ const { payload } = verdict;
33
+ const sdk = await host.connect();
34
+ sdk.connect({
35
+ url: payload.bridge,
36
+ token: payload.nonce,
37
+ env: HelloEnv.PRODUCTION,
38
+ scope: payload.scope,
39
+ allowInProduction: true,
40
+ allowNonLocalhost: true,
41
+ });
42
+ return true;
43
+ }
package/dist/busy.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export declare function trackBusy<T>(work: Promise<T>, signal: (name: string, data: Record<string, unknown>) => void): Promise<T>;
2
+ export declare const SHORT_TIMER_MS = 1000;
3
+ export declare function trackShortTimers(signal: (name: string, data: Record<string, unknown>) => void, maxMs?: number): () => void;
package/dist/busy.js ADDED
@@ -0,0 +1,42 @@
1
+ import { RETICLE_BUSY_SIGNAL } from '@reticlehq/core';
2
+ import { captureMethod } from './patching/capture-method.js';
3
+ export function trackBusy(work, signal) {
4
+ signal(RETICLE_BUSY_SIGNAL, { delta: 1 });
5
+ const done = () => {
6
+ signal(RETICLE_BUSY_SIGNAL, { delta: -1 });
7
+ };
8
+ work.then(done, done);
9
+ return work;
10
+ }
11
+ export const SHORT_TIMER_MS = 1000;
12
+ export function trackShortTimers(signal, maxMs = SHORT_TIMER_MS) {
13
+ const realSet = captureMethod(window, 'setTimeout');
14
+ const realClear = captureMethod(window, 'clearTimeout');
15
+ const callSet = realSet.bind(window);
16
+ const callClear = realClear.bind(window);
17
+ const pending = new Set();
18
+ const end = (id) => {
19
+ if (pending.delete(id))
20
+ signal(RETICLE_BUSY_SIGNAL, { delta: -1 });
21
+ };
22
+ window.setTimeout = ((cb, delay = 0, ...args) => {
23
+ if ('function' !== typeof cb || delay > maxMs)
24
+ return Reflect.apply(realSet, window, [cb, delay, ...args]);
25
+ signal(RETICLE_BUSY_SIGNAL, { delta: 1 });
26
+ const id = callSet(() => {
27
+ end(id);
28
+ cb(...args);
29
+ }, delay);
30
+ pending.add(id);
31
+ return id;
32
+ });
33
+ window.clearTimeout = ((id) => {
34
+ if (id !== undefined)
35
+ end(id);
36
+ callClear(id);
37
+ });
38
+ return () => {
39
+ window.setTimeout = realSet;
40
+ window.clearTimeout = realClear;
41
+ };
42
+ }