@reticlehq/browser 3.5.0 → 3.7.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 (116) hide show
  1. package/dist/actions/action-report.d.ts +54 -0
  2. package/dist/actions/action-report.js +60 -0
  3. package/dist/actions/actions-dom.d.ts +1 -0
  4. package/dist/actions/actions-dom.js +56 -43
  5. package/dist/actions/actions-press.d.ts +11 -4
  6. package/dist/actions/actions-press.js +132 -17
  7. package/dist/actions/actions.d.ts +1 -35
  8. package/dist/actions/actions.js +32 -65
  9. package/dist/actions/appeared-text.js +3 -0
  10. package/dist/actions/danger-context.d.ts +2 -0
  11. package/dist/actions/danger-context.js +29 -3
  12. package/dist/actions/value-input.d.ts +1 -0
  13. package/dist/actions/value-input.js +11 -0
  14. package/dist/commands/commands.js +4 -0
  15. package/dist/connect-options.d.ts +1 -0
  16. package/dist/dom/a11y.js +264 -14
  17. package/dist/dom/addressing/coverage-markers.d.ts +6 -0
  18. package/dist/dom/addressing/coverage-markers.js +13 -0
  19. package/dist/dom/addressing/refs.js +2 -1
  20. package/dist/dom/addressing/testid-attr.d.ts +4 -0
  21. package/dist/dom/addressing/testid-attr.js +14 -0
  22. package/dist/dom/dom-ignore.d.ts +2 -0
  23. package/dist/dom/dom-ignore.js +15 -2
  24. package/dist/dom/query.d.ts +2 -1
  25. package/dist/dom/query.js +116 -27
  26. package/dist/dom/render-page.d.ts +12 -0
  27. package/dist/dom/render-page.js +135 -0
  28. package/dist/dom/snapshot.js +56 -7
  29. package/dist/observers/departure.js +51 -9
  30. package/dist/observers/dom.js +35 -18
  31. package/dist/observers/field.js +5 -21
  32. package/dist/observers/health.js +63 -2
  33. package/dist/observers/network-stream.d.ts +1 -1
  34. package/dist/observers/network-stream.js +3 -3
  35. package/dist/observers/network.js +57 -8
  36. package/dist/observers/perf.js +53 -1
  37. package/dist/observers/route.js +6 -5
  38. package/dist/observers/storage.js +6 -1
  39. package/dist/presenter/carousel/carousel.d.ts +5 -2
  40. package/dist/presenter/carousel/carousel.js +30 -8
  41. package/dist/presenter/carousel/panel-slides.d.ts +3 -4
  42. package/dist/presenter/carousel/panel-slides.js +48 -9
  43. package/dist/presenter/carousel/talk-card.d.ts +1 -1
  44. package/dist/presenter/chrome/log-kinds.d.ts +5 -0
  45. package/dist/presenter/chrome/log-kinds.js +1 -0
  46. package/dist/presenter/chrome/log-memory.d.ts +13 -0
  47. package/dist/presenter/chrome/log-memory.js +39 -0
  48. package/dist/presenter/chrome/presenter-hud-chrome.d.ts +1 -0
  49. package/dist/presenter/chrome/presenter-hud-chrome.js +7 -1
  50. package/dist/presenter/chrome/presenter-log.d.ts +12 -4
  51. package/dist/presenter/chrome/presenter-log.js +100 -10
  52. package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
  53. package/dist/presenter/chrome/presenter-morph.js +34 -0
  54. package/dist/presenter/chrome/presenter-report-copy.d.ts +28 -5
  55. package/dist/presenter/chrome/presenter-report-copy.js +40 -5
  56. package/dist/presenter/hud-telemetry.js +83 -3
  57. package/dist/presenter/icons/presenter-heroicons-data.d.ts +1 -0
  58. package/dist/presenter/icons/presenter-heroicons-data.js +4 -3
  59. package/dist/presenter/icons/presenter-icons.d.ts +2 -0
  60. package/dist/presenter/icons/presenter-icons.js +2 -0
  61. package/dist/presenter/presenter-account-styles.js +3 -3
  62. package/dist/presenter/presenter-account.d.ts +31 -8
  63. package/dist/presenter/presenter-account.js +105 -15
  64. package/dist/presenter/presenter-agent-link.d.ts +9 -0
  65. package/dist/presenter/presenter-agent-link.js +62 -0
  66. package/dist/presenter/presenter-chat-views.d.ts +43 -0
  67. package/dist/presenter/presenter-chat-views.js +622 -0
  68. package/dist/presenter/presenter-config.d.ts +5 -2
  69. package/dist/presenter/presenter-config.js +5 -3
  70. package/dist/presenter/presenter-controls.d.ts +3 -2
  71. package/dist/presenter/presenter-controls.js +163 -61
  72. package/dist/presenter/presenter-design-tokens.d.ts +1 -0
  73. package/dist/presenter/presenter-design-tokens.js +34 -0
  74. package/dist/presenter/presenter-dock-layout.js +1 -1
  75. package/dist/presenter/presenter-frame.d.ts +12 -0
  76. package/dist/presenter/presenter-frame.js +187 -0
  77. package/dist/presenter/presenter-harness-row.d.ts +67 -0
  78. package/dist/presenter/presenter-harness-row.js +209 -0
  79. package/dist/presenter/presenter-personas.d.ts +35 -0
  80. package/dist/presenter/presenter-personas.js +95 -0
  81. package/dist/presenter/presenter-plan.d.ts +10 -0
  82. package/dist/presenter/presenter-plan.js +186 -0
  83. package/dist/presenter/presenter-report-styles.js +46 -21
  84. package/dist/presenter/presenter-report.d.ts +2 -1
  85. package/dist/presenter/presenter-report.js +123 -20
  86. package/dist/presenter/presenter-settings-styles.js +18 -4
  87. package/dist/presenter/presenter-settings.d.ts +8 -2
  88. package/dist/presenter/presenter-settings.js +134 -33
  89. package/dist/presenter/presenter-shell-styles.js +48 -42
  90. package/dist/presenter/presenter-shell.d.ts +17 -4
  91. package/dist/presenter/presenter-shell.js +139 -79
  92. package/dist/presenter/presenter-styles.js +9 -3
  93. package/dist/presenter/presenter-workspace.js +10 -3
  94. package/dist/presenter/presenter.d.ts +6 -4
  95. package/dist/presenter/presenter.js +171 -22
  96. package/dist/presenter/tour/tour.d.ts +4 -1
  97. package/dist/presenter/tour/tour.js +6 -4
  98. package/dist/recorder/recorder.js +2 -2
  99. package/dist/registry/auto-testids.js +3 -4
  100. package/dist/registry/declared-platform.js +4 -0
  101. package/dist/reticle-inject.js +938 -494
  102. package/dist/reticle.d.ts +10 -1
  103. package/dist/reticle.js +109 -11
  104. package/dist/review/annotator-styles.d.ts +1 -1
  105. package/dist/review/annotator.d.ts +7 -0
  106. package/dist/review/annotator.js +20 -2
  107. package/dist/review/mark-anchor.js +2 -2
  108. package/dist/session-continuity.js +2 -1
  109. package/dist/storage-keys.d.ts +15 -0
  110. package/dist/storage-keys.js +21 -0
  111. package/dist/transport/transport.js +5 -3
  112. package/dist/transport/unreachable-message.d.ts +1 -1
  113. package/dist/transport/unreachable-message.js +3 -5
  114. package/package.json +3 -2
  115. package/dist/presenter/carousel/offer-card.d.ts +0 -21
  116. package/dist/presenter/carousel/offer-card.js +0 -55
@@ -0,0 +1,54 @@
1
+ import { type SettleReason } from '@reticlehq/core';
2
+ export interface ActionEffect {
3
+ dispatched: true;
4
+ targetMatched: boolean;
5
+ visible: boolean;
6
+ enabled: boolean;
7
+ defaultPrevented: boolean;
8
+ focusMoved: string | null;
9
+ valueChanged: boolean;
10
+ autofillSafe?: true;
11
+ domMutatedWithin: number;
12
+ appeared?: string;
13
+ occluded: boolean;
14
+ occludedBy: string | null;
15
+ scrolledIntoView: boolean;
16
+ heldMs?: number;
17
+ alreadyAtValue?: true;
18
+ }
19
+ export interface ActionResult {
20
+ ok: true;
21
+ ref: string;
22
+ action: string;
23
+ dispatched: boolean;
24
+ settled: boolean;
25
+ settleReason: SettleReason | null;
26
+ effect: ActionEffect;
27
+ testid?: string;
28
+ component?: string;
29
+ role?: string;
30
+ name?: string;
31
+ nth?: number;
32
+ of?: number;
33
+ source?: {
34
+ file: string;
35
+ line: number;
36
+ column?: number;
37
+ };
38
+ warning?: string;
39
+ }
40
+ export interface CapturedAnchor {
41
+ testid?: string;
42
+ component?: string;
43
+ source?: {
44
+ file: string;
45
+ line: number;
46
+ column?: number;
47
+ };
48
+ role?: string;
49
+ name?: string;
50
+ nth?: number;
51
+ of?: number;
52
+ }
53
+ export declare function anchorOf(el: Element): CapturedAnchor;
54
+ export declare const result: (ref: string, action: string, effect: ActionEffect, settled: boolean, settleReason: SettleReason | null, anchor: CapturedAnchor, warning?: string) => ActionResult;
@@ -0,0 +1,60 @@
1
+ import { readTestId } from '../dom/addressing/testid-attr.js';
2
+ import { QueryBy } from '@reticlehq/core';
3
+ import { elementsMatching } from '../dom/query.js';
4
+ import { getAccessibleName, getRole } from '../dom/a11y.js';
5
+ import { identifyComponent } from '../registry/stores/adapters.js';
6
+ export function anchorOf(el) {
7
+ const testid = readTestId(el) ?? undefined;
8
+ const info = identifyComponent(el);
9
+ const out = {};
10
+ if (testid !== undefined)
11
+ out.testid = testid;
12
+ const component = info?.componentStack[0];
13
+ if (component !== undefined)
14
+ out.component = component;
15
+ if (info?.source !== undefined)
16
+ out.source = info.source;
17
+ const role = getRole(el);
18
+ const name = getAccessibleName(el);
19
+ if (role.length > 0)
20
+ out.role = role;
21
+ if (name.length > 0)
22
+ out.name = name;
23
+ if (role.length > 0 && name.length > 0) {
24
+ const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
25
+ const at = same.indexOf(el);
26
+ if (1 < same.length && 0 <= at) {
27
+ out.nth = at;
28
+ out.of = same.length;
29
+ }
30
+ }
31
+ return out;
32
+ }
33
+ export const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
34
+ const base = {
35
+ ok: true,
36
+ ref,
37
+ action,
38
+ dispatched: true,
39
+ settled,
40
+ settleReason,
41
+ effect,
42
+ };
43
+ if (anchor.testid !== undefined)
44
+ base.testid = anchor.testid;
45
+ if (anchor.component !== undefined)
46
+ base.component = anchor.component;
47
+ if (anchor.source !== undefined)
48
+ base.source = anchor.source;
49
+ if (anchor.role !== undefined)
50
+ base.role = anchor.role;
51
+ if (anchor.name !== undefined)
52
+ base.name = anchor.name;
53
+ if (anchor.nth !== undefined)
54
+ base.nth = anchor.nth;
55
+ if (anchor.of !== undefined)
56
+ base.of = anchor.of;
57
+ if (warning !== undefined)
58
+ base.warning = warning;
59
+ return base;
60
+ };
@@ -26,4 +26,5 @@ export declare function fireTapSequence(el: ActionTarget, hold: {
26
26
  prevented: boolean;
27
27
  heldMs: number;
28
28
  }>;
29
+ export declare function assertNotLeaving(el: Element): void;
29
30
  export {};
@@ -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);
@@ -230,3 +231,15 @@ function makeTouchEvent(type, touches) {
230
231
  }
231
232
  return new Event(type, { bubbles: true, cancelable: true });
232
233
  }
234
+ const POPUP_ROLES = new Set(['menu', 'menubar', 'listbox', 'dialog', 'alertdialog', 'tooltip']);
235
+ const CLOSED_STATE = 'closed';
236
+ export function assertNotLeaving(el) {
237
+ for (let up = el.parentElement; up !== null; up = up.parentElement) {
238
+ if (CLOSED_STATE !== up.getAttribute('data-state'))
239
+ continue;
240
+ if (!POPUP_ROLES.has(up.getAttribute('role') ?? ''))
241
+ continue;
242
+ throw new Error(`'${up.getAttribute('role') ?? 'popup'}' holding this element is closing (data-state="closed"), ` +
243
+ 'so a person could not click it: look again once it has gone and act on what is open');
244
+ }
245
+ }
@@ -1,14 +1,21 @@
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;
11
+ export declare function dispatchKeypress(el: ActionTarget, key: string, code: string, mods: ModifierFlags, repeat?: boolean): void;
10
12
  export declare function focusedOrDocument(): HTMLElement;
11
13
  export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
12
14
  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>;
15
+ export declare const legacyKeyCode: (key: string) => number;
16
+ export declare function keyboardEvent(type: string, init: KeyboardEventInit & {
17
+ key: string;
18
+ }): KeyboardEvent;
19
+ export declare function holdKey(el: ActionTarget, key: string, code: string, mods: ModifierFlags, ms: number): Promise<void>;
20
+ export declare function pressCombo(el: ActionTarget, keys: readonly string[], mods: ModifierFlags, holdMs: number): Promise<boolean>;
21
+ 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'];
@@ -35,6 +57,26 @@ export function pressCode(args, key) {
35
57
  }
36
58
  return /^[A-Z][A-Za-z0-9]*$/.test(key) && KNOWN_NAMED_KEYS.has(key) ? key : '';
37
59
  }
60
+ export function dispatchKeypress(el, key, code, mods, repeat = false) {
61
+ if (ENTER_KEY !== key && 1 !== key.length)
62
+ return;
63
+ const event = new KeyboardEvent('keypress', {
64
+ key,
65
+ code,
66
+ bubbles: true,
67
+ cancelable: true,
68
+ repeat,
69
+ ...mods,
70
+ });
71
+ const legacy = ENTER_KEY === key ? ENTER_CHAR_CODE : key.charCodeAt(0);
72
+ for (const field of LEGACY_KEY_CODE_FIELDS) {
73
+ Object.defineProperty(event, field, { value: legacy });
74
+ }
75
+ asSyntheticInput(() => el.dispatchEvent(event));
76
+ }
77
+ const ENTER_KEY = 'Enter';
78
+ const ENTER_CHAR_CODE = 13;
79
+ const LEGACY_KEY_CODE_FIELDS = ['keyCode', 'charCode', 'which'];
38
80
  const KNOWN_NAMED_KEYS = new Set([
39
81
  'Enter',
40
82
  'Escape',
@@ -79,19 +121,45 @@ export function isReflessDocumentPress(ref, action, args) {
79
121
  return 0 === ref.length && isGlobalPress(args) && ActionType.PRESS === action;
80
122
  }
81
123
  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);
124
+ return pressKeysFromArgs(args);
86
125
  }
87
126
  const KEY_REPEAT_MS = 33;
88
127
  const KEY_REPEAT_DELAY_MS = 500;
128
+ const LEGACY_KEY_CODES = {
129
+ Backspace: 8,
130
+ Tab: 9,
131
+ Enter: 13,
132
+ Shift: 16,
133
+ Control: 17,
134
+ Alt: 18,
135
+ Escape: 27,
136
+ ' ': 32,
137
+ PageUp: 33,
138
+ PageDown: 34,
139
+ End: 35,
140
+ Home: 36,
141
+ ArrowLeft: 37,
142
+ ArrowUp: 38,
143
+ ArrowRight: 39,
144
+ ArrowDown: 40,
145
+ Delete: 46,
146
+ Meta: 91,
147
+ };
148
+ const SINGLE_ALNUM = /^[a-z0-9]$/i;
149
+ export const legacyKeyCode = (key) => LEGACY_KEY_CODES[key] ?? (SINGLE_ALNUM.test(key) ? key.toUpperCase().charCodeAt(0) : 0);
150
+ export function keyboardEvent(type, init) {
151
+ const event = new KeyboardEvent(type, init);
152
+ const code = legacyKeyCode(init.key);
153
+ for (const legacy of ['keyCode', 'which'])
154
+ Object.defineProperty(event, legacy, { get: () => code });
155
+ return event;
156
+ }
89
157
  export async function holdKey(el, key, code, mods, ms) {
90
158
  const started = Date.now();
91
159
  if (ms > KEY_REPEAT_DELAY_MS)
92
160
  await sleep(KEY_REPEAT_DELAY_MS);
93
161
  while (Date.now() - started < ms) {
94
- asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keydown', {
162
+ const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
95
163
  key,
96
164
  code,
97
165
  bubbles: true,
@@ -99,22 +167,69 @@ export async function holdKey(el, key, code, mods, ms) {
99
167
  repeat: true,
100
168
  ...mods,
101
169
  })));
170
+ if (down)
171
+ dispatchKeypress(el, key, code, mods, true);
102
172
  await sleep(Math.min(KEY_REPEAT_MS, Math.max(0, ms - (Date.now() - started))));
103
173
  }
104
174
  }
105
175
  export async function pressCombo(el, keys, mods, holdMs) {
176
+ const held = { ...mods };
177
+ const dispatch = (type, key) => {
178
+ const code = pressCode({}, key);
179
+ return asSyntheticInput(() => el.dispatchEvent(keyboardEvent(type, { key, code, bubbles: true, cancelable: true, ...held })));
180
+ };
181
+ const presses = new Map();
182
+ for (const flag of MODIFIER_FLAG_KEYS) {
183
+ if (held[flag])
184
+ presses.set(flag, 1);
185
+ }
186
+ const pressFlag = (flag) => {
187
+ presses.set(flag, (presses.get(flag) ?? 0) + 1);
188
+ held[flag] = true;
189
+ };
190
+ const releaseFlag = (flag) => {
191
+ const left = (presses.get(flag) ?? 1) - 1;
192
+ presses.set(flag, left);
193
+ if (left <= 0)
194
+ held[flag] = false;
195
+ };
106
196
  let prevented = false;
107
197
  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 })));
198
+ const flag = modifierFlagFor(key);
199
+ if (flag !== undefined)
200
+ pressFlag(flag);
201
+ const ok = dispatch('keydown', key);
110
202
  if (!ok)
111
203
  prevented = true;
204
+ if (ok)
205
+ dispatchKeypress(el, key, pressCode({}, key), held);
206
+ closeModalOnEscape(el, key, ok);
112
207
  }
113
208
  if (holdMs > 0)
114
209
  await sleep(holdMs);
115
210
  for (const key of [...keys].reverse()) {
116
- const code = pressCode({}, key);
117
- asSyntheticInput(() => el.dispatchEvent(new KeyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
211
+ const flag = modifierFlagFor(key);
212
+ if (flag !== undefined)
213
+ releaseFlag(flag);
214
+ dispatch('keyup', key);
118
215
  }
119
216
  return prevented;
120
217
  }
218
+ export function closeModalOnEscape(el, key, keydownProceeded) {
219
+ if ('Escape' !== key || !keydownProceeded)
220
+ return;
221
+ for (let dialog = el.closest('dialog'); null !== dialog; dialog = dialog.parentElement?.closest('dialog') ?? null) {
222
+ try {
223
+ if (!dialog.open || !dialog.matches(':modal'))
224
+ continue;
225
+ }
226
+ catch {
227
+ return;
228
+ }
229
+ if ('function' === typeof dialog.requestClose)
230
+ dialog.requestClose();
231
+ else if (dialog.dispatchEvent(new Event('cancel', { cancelable: true })))
232
+ dialog.close();
233
+ return;
234
+ }
235
+ }
@@ -1,39 +1,5 @@
1
1
  import { SettleReason } from '@reticlehq/core';
2
- interface ActionEffect {
3
- dispatched: true;
4
- targetMatched: boolean;
5
- visible: boolean;
6
- enabled: boolean;
7
- defaultPrevented: boolean;
8
- focusMoved: string | null;
9
- valueChanged: boolean;
10
- domMutatedWithin: number;
11
- appeared?: string;
12
- occluded: boolean;
13
- occludedBy: string | null;
14
- scrolledIntoView: boolean;
15
- heldMs?: number;
16
- alreadyAtValue?: true;
17
- }
18
- interface ActionResult {
19
- ok: true;
20
- ref: string;
21
- action: string;
22
- dispatched: boolean;
23
- settled: boolean;
24
- settleReason: SettleReason | null;
25
- effect: ActionEffect;
26
- testid?: string;
27
- component?: string;
28
- role?: string;
29
- name?: string;
30
- source?: {
31
- file: string;
32
- line: number;
33
- column?: number;
34
- };
35
- warning?: string;
36
- }
2
+ import { type ActionEffect, type ActionResult } from './action-report.js';
37
3
  export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
38
4
  export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
39
5
  export interface ActionStep {