@reticlehq/browser 3.6.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.
- package/dist/actions/action-report.d.ts +64 -0
- package/dist/actions/action-report.js +87 -0
- package/dist/actions/actions-dom.d.ts +5 -0
- package/dist/actions/actions-dom.js +32 -4
- package/dist/actions/actions-press.d.ts +1 -0
- package/dist/actions/actions-press.js +25 -1
- package/dist/actions/actions.d.ts +2 -37
- package/dist/actions/actions.js +56 -80
- package/dist/actions/appeared-text.js +5 -2
- package/dist/actions/click-target.d.ts +1 -0
- package/dist/actions/click-target.js +14 -0
- package/dist/actions/danger-context.d.ts +4 -1
- package/dist/actions/danger-context.js +45 -4
- package/dist/actions/value-input.d.ts +1 -0
- package/dist/actions/value-input.js +11 -0
- package/dist/activate.d.ts +14 -0
- package/dist/activate.js +43 -0
- package/dist/busy.d.ts +3 -0
- package/dist/busy.js +42 -0
- package/dist/commands/commands.js +49 -17
- package/dist/connect-options.d.ts +12 -1
- package/dist/connect-options.js +18 -1
- package/dist/dom/a11y.d.ts +2 -0
- package/dist/dom/a11y.js +353 -80
- package/dist/dom/addressing/coverage-markers.d.ts +6 -0
- package/dist/dom/addressing/coverage-markers.js +13 -0
- package/dist/dom/chart.d.ts +3 -2
- package/dist/dom/chart.js +3 -2
- package/dist/dom/dom-ignore.d.ts +8 -0
- package/dist/dom/dom-ignore.js +57 -2
- package/dist/dom/layout-audit.d.ts +15 -0
- package/dist/dom/layout-audit.js +208 -0
- package/dist/dom/paint-context.d.ts +15 -0
- package/dist/dom/paint-context.js +78 -0
- package/dist/dom/query.d.ts +2 -1
- package/dist/dom/query.js +163 -38
- package/dist/dom/render-page.d.ts +12 -0
- package/dist/dom/render-page.js +135 -0
- package/dist/dom/shadow-registry.d.ts +1 -0
- package/dist/dom/shadow-registry.js +27 -0
- package/dist/dom/snapshot.d.ts +5 -0
- package/dist/dom/snapshot.js +135 -36
- package/dist/dom/stable-selector.d.ts +1 -0
- package/dist/dom/stable-selector.js +19 -0
- package/dist/dom/text-match.d.ts +12 -0
- package/dist/dom/text-match.js +115 -0
- package/dist/dom/theme.d.ts +1 -0
- package/dist/dom/theme.js +21 -9
- package/dist/dom/virtualized.js +5 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -0
- package/dist/observers/blind-spots.js +3 -2
- package/dist/observers/canvas.d.ts +18 -0
- package/dist/observers/canvas.js +76 -0
- package/dist/observers/dom.js +96 -32
- package/dist/observers/field.d.ts +11 -0
- package/dist/observers/field.js +116 -30
- package/dist/observers/install-all.js +4 -0
- package/dist/observers/media.d.ts +2 -0
- package/dist/observers/media.js +32 -0
- package/dist/observers/net-detail/network-body.d.ts +2 -0
- package/dist/observers/net-detail/network-body.js +8 -1
- package/dist/observers/network.js +82 -13
- package/dist/observers/perf.js +53 -1
- package/dist/observers/route.d.ts +2 -0
- package/dist/observers/route.js +39 -9
- package/dist/presenter/carousel/carousel.js +2 -7
- package/dist/presenter/carousel/panel-slides.d.ts +2 -4
- package/dist/presenter/carousel/panel-slides.js +17 -15
- package/dist/presenter/chrome/log-memory.d.ts +1 -0
- package/dist/presenter/chrome/presenter-log.d.ts +7 -0
- package/dist/presenter/chrome/presenter-log.js +48 -8
- package/dist/presenter/chrome/presenter-morph.d.ts +1 -0
- package/dist/presenter/chrome/presenter-morph.js +34 -0
- package/dist/presenter/chrome/presenter-report-copy.d.ts +12 -4
- package/dist/presenter/chrome/presenter-report-copy.js +12 -4
- package/dist/presenter/hud-telemetry.js +55 -4
- package/dist/presenter/presenter-account-styles.js +3 -3
- package/dist/presenter/presenter-account.d.ts +27 -5
- package/dist/presenter/presenter-account.js +83 -6
- package/dist/presenter/presenter-agent-link.d.ts +9 -0
- package/dist/presenter/presenter-agent-link.js +62 -0
- package/dist/presenter/presenter-chat-views.d.ts +25 -4
- package/dist/presenter/presenter-chat-views.js +339 -72
- package/dist/presenter/presenter-config.d.ts +3 -0
- package/dist/presenter/presenter-config.js +2 -0
- package/dist/presenter/presenter-controls.d.ts +0 -1
- package/dist/presenter/presenter-controls.js +13 -91
- package/dist/presenter/presenter-design-tokens.js +1 -1
- package/dist/presenter/presenter-frame.d.ts +3 -2
- package/dist/presenter/presenter-frame.js +31 -24
- package/dist/presenter/presenter-harness-row.d.ts +56 -0
- package/dist/presenter/presenter-harness-row.js +170 -0
- package/dist/presenter/presenter-personas.d.ts +35 -0
- package/dist/presenter/presenter-personas.js +95 -0
- package/dist/presenter/presenter-plan.js +25 -24
- package/dist/presenter/presenter-report-styles.js +23 -22
- package/dist/presenter/presenter-report.js +29 -13
- package/dist/presenter/presenter-run-state.js +2 -1
- package/dist/presenter/presenter-settings-styles.js +8 -4
- package/dist/presenter/presenter-settings.d.ts +4 -4
- package/dist/presenter/presenter-settings.js +66 -28
- package/dist/presenter/presenter-shell-styles.js +12 -3
- package/dist/presenter/presenter-shell.d.ts +11 -3
- package/dist/presenter/presenter-shell.js +60 -22
- package/dist/presenter/presenter-styles.js +0 -2
- package/dist/presenter/presenter-workspace.js +10 -3
- package/dist/presenter/presenter.d.ts +3 -2
- package/dist/presenter/presenter.js +107 -25
- package/dist/registry/declared-platform.js +2 -0
- package/dist/registry/store-adapters.d.ts +5 -2
- package/dist/registry/store-adapters.js +40 -10
- package/dist/reticle-inject.js +771 -703
- package/dist/reticle.d.ts +4 -2
- package/dist/reticle.js +72 -8
- package/dist/security/page-location.d.ts +2 -0
- package/dist/security/page-location.js +7 -0
- package/dist/security/serialization.js +1 -1
- package/dist/storage-keys.d.ts +4 -2
- package/dist/storage-keys.js +8 -3
- package/dist/timers/native/native-timers.d.ts +1 -0
- package/dist/timers/native/native-timers.js +5 -0
- package/dist/transport/transport.d.ts +1 -0
- package/dist/transport/transport.js +23 -3
- package/dist/transport/unreachable-message.d.ts +1 -1
- package/dist/transport/unreachable-message.js +3 -5
- package/package.json +6 -2
- package/dist/presenter/carousel/offer-card.d.ts +0 -21
- package/dist/presenter/carousel/offer-card.js +0 -56
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
occluderName?: string;
|
|
17
|
+
noSize?: true;
|
|
18
|
+
offViewport?: true;
|
|
19
|
+
heldMs?: number;
|
|
20
|
+
alreadyAtValue?: true;
|
|
21
|
+
canvases?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function visibleCanvases(doc: Document): number;
|
|
24
|
+
export interface ActionResult {
|
|
25
|
+
ok: true;
|
|
26
|
+
ref: string;
|
|
27
|
+
action: string;
|
|
28
|
+
dispatched: boolean;
|
|
29
|
+
settled: boolean;
|
|
30
|
+
settleReason: SettleReason | null;
|
|
31
|
+
effect: ActionEffect;
|
|
32
|
+
testid?: string;
|
|
33
|
+
testidOf?: number;
|
|
34
|
+
component?: string;
|
|
35
|
+
role?: string;
|
|
36
|
+
name?: string;
|
|
37
|
+
nth?: number;
|
|
38
|
+
of?: number;
|
|
39
|
+
source?: {
|
|
40
|
+
file: string;
|
|
41
|
+
line: number;
|
|
42
|
+
column?: number;
|
|
43
|
+
};
|
|
44
|
+
secret?: true;
|
|
45
|
+
warning?: string;
|
|
46
|
+
reticleUi?: true;
|
|
47
|
+
}
|
|
48
|
+
export interface CapturedAnchor {
|
|
49
|
+
testid?: string;
|
|
50
|
+
testidOf?: number;
|
|
51
|
+
component?: string;
|
|
52
|
+
source?: {
|
|
53
|
+
file: string;
|
|
54
|
+
line: number;
|
|
55
|
+
column?: number;
|
|
56
|
+
};
|
|
57
|
+
role?: string;
|
|
58
|
+
name?: string;
|
|
59
|
+
nth?: number;
|
|
60
|
+
of?: number;
|
|
61
|
+
secret?: true;
|
|
62
|
+
}
|
|
63
|
+
export declare function anchorOf(el: Element): CapturedAnchor;
|
|
64
|
+
export declare const result: (ref: string, action: string, effect: ActionEffect, settled: boolean, settleReason: SettleReason | null, anchor: CapturedAnchor, warning?: string) => ActionResult;
|
|
@@ -0,0 +1,87 @@
|
|
|
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, isSensitiveField } from '../dom/a11y.js';
|
|
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
|
+
}
|
|
23
|
+
export function anchorOf(el) {
|
|
24
|
+
const testid = readTestId(el) ?? undefined;
|
|
25
|
+
const info = identifyComponent(el);
|
|
26
|
+
const out = {};
|
|
27
|
+
if (testid !== undefined) {
|
|
28
|
+
out.testid = testid;
|
|
29
|
+
const shared = elementsMatching({ by: QueryBy.TESTID, value: testid }).length;
|
|
30
|
+
if (1 < shared)
|
|
31
|
+
out.testidOf = shared;
|
|
32
|
+
}
|
|
33
|
+
const component = info?.componentStack[0];
|
|
34
|
+
if (component !== undefined)
|
|
35
|
+
out.component = component;
|
|
36
|
+
if (info?.source !== undefined)
|
|
37
|
+
out.source = info.source;
|
|
38
|
+
const role = getRole(el);
|
|
39
|
+
const name = getAccessibleName(el);
|
|
40
|
+
if (role.length > 0)
|
|
41
|
+
out.role = role;
|
|
42
|
+
if (name.length > 0)
|
|
43
|
+
out.name = name;
|
|
44
|
+
if (isSensitiveField(el))
|
|
45
|
+
out.secret = true;
|
|
46
|
+
if (role.length > 0 && name.length > 0) {
|
|
47
|
+
const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
|
|
48
|
+
const at = same.indexOf(el);
|
|
49
|
+
if (1 < same.length && 0 <= at) {
|
|
50
|
+
out.nth = at;
|
|
51
|
+
out.of = same.length;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
export const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
|
|
57
|
+
const base = {
|
|
58
|
+
ok: true,
|
|
59
|
+
ref,
|
|
60
|
+
action,
|
|
61
|
+
dispatched: true,
|
|
62
|
+
settled,
|
|
63
|
+
settleReason,
|
|
64
|
+
effect,
|
|
65
|
+
};
|
|
66
|
+
if (anchor.testid !== undefined)
|
|
67
|
+
base.testid = anchor.testid;
|
|
68
|
+
if (anchor.testidOf !== undefined)
|
|
69
|
+
base.testidOf = anchor.testidOf;
|
|
70
|
+
if (anchor.component !== undefined)
|
|
71
|
+
base.component = anchor.component;
|
|
72
|
+
if (anchor.source !== undefined)
|
|
73
|
+
base.source = anchor.source;
|
|
74
|
+
if (anchor.role !== undefined)
|
|
75
|
+
base.role = anchor.role;
|
|
76
|
+
if (anchor.name !== undefined)
|
|
77
|
+
base.name = anchor.name;
|
|
78
|
+
if (anchor.nth !== undefined)
|
|
79
|
+
base.nth = anchor.nth;
|
|
80
|
+
if (anchor.of !== undefined)
|
|
81
|
+
base.of = anchor.of;
|
|
82
|
+
if (anchor.secret !== undefined)
|
|
83
|
+
base.secret = anchor.secret;
|
|
84
|
+
if (warning !== undefined)
|
|
85
|
+
base.warning = warning;
|
|
86
|
+
return base;
|
|
87
|
+
};
|
|
@@ -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;
|
|
@@ -26,4 +30,5 @@ export declare function fireTapSequence(el: ActionTarget, hold: {
|
|
|
26
30
|
prevented: boolean;
|
|
27
31
|
heldMs: number;
|
|
28
32
|
}>;
|
|
33
|
+
export declare function assertNotLeaving(el: Element): void;
|
|
29
34
|
export {};
|
|
@@ -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
|
-
|
|
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 })));
|
|
@@ -231,3 +247,15 @@ function makeTouchEvent(type, touches) {
|
|
|
231
247
|
}
|
|
232
248
|
return new Event(type, { bubbles: true, cancelable: true });
|
|
233
249
|
}
|
|
250
|
+
const POPUP_ROLES = new Set(['menu', 'menubar', 'listbox', 'dialog', 'alertdialog', 'tooltip']);
|
|
251
|
+
const CLOSED_STATE = 'closed';
|
|
252
|
+
export function assertNotLeaving(el) {
|
|
253
|
+
for (let up = el.parentElement; up !== null; up = up.parentElement) {
|
|
254
|
+
if (CLOSED_STATE !== up.getAttribute('data-state'))
|
|
255
|
+
continue;
|
|
256
|
+
if (!POPUP_ROLES.has(up.getAttribute('role') ?? ''))
|
|
257
|
+
continue;
|
|
258
|
+
throw new Error(`'${up.getAttribute('role') ?? 'popup'}' holding this element is closing (data-state="closed"), ` +
|
|
259
|
+
'so a person could not click it: look again once it has gone and act on what is open');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -8,6 +8,7 @@ export interface ModifierFlags {
|
|
|
8
8
|
}
|
|
9
9
|
export declare function pressModifiers(args: Record<string, unknown>): ModifierFlags;
|
|
10
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;
|
|
11
12
|
export declare function focusedOrDocument(): HTMLElement;
|
|
12
13
|
export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
|
|
13
14
|
export declare function pressKeys(args: Record<string, unknown>): string[];
|
|
@@ -57,6 +57,26 @@ export function pressCode(args, key) {
|
|
|
57
57
|
}
|
|
58
58
|
return /^[A-Z][A-Za-z0-9]*$/.test(key) && KNOWN_NAMED_KEYS.has(key) ? key : '';
|
|
59
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'];
|
|
60
80
|
const KNOWN_NAMED_KEYS = new Set([
|
|
61
81
|
'Enter',
|
|
62
82
|
'Escape',
|
|
@@ -139,7 +159,7 @@ export async function holdKey(el, key, code, mods, ms) {
|
|
|
139
159
|
if (ms > KEY_REPEAT_DELAY_MS)
|
|
140
160
|
await sleep(KEY_REPEAT_DELAY_MS);
|
|
141
161
|
while (Date.now() - started < ms) {
|
|
142
|
-
asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
|
|
162
|
+
const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', {
|
|
143
163
|
key,
|
|
144
164
|
code,
|
|
145
165
|
bubbles: true,
|
|
@@ -147,6 +167,8 @@ export async function holdKey(el, key, code, mods, ms) {
|
|
|
147
167
|
repeat: true,
|
|
148
168
|
...mods,
|
|
149
169
|
})));
|
|
170
|
+
if (down)
|
|
171
|
+
dispatchKeypress(el, key, code, mods, true);
|
|
150
172
|
await sleep(Math.min(KEY_REPEAT_MS, Math.max(0, ms - (Date.now() - started))));
|
|
151
173
|
}
|
|
152
174
|
}
|
|
@@ -179,6 +201,8 @@ export async function pressCombo(el, keys, mods, holdMs) {
|
|
|
179
201
|
const ok = dispatch('keydown', key);
|
|
180
202
|
if (!ok)
|
|
181
203
|
prevented = true;
|
|
204
|
+
if (ok)
|
|
205
|
+
dispatchKeypress(el, key, pressCode({}, key), held);
|
|
182
206
|
closeModalOnEscape(el, key, ok);
|
|
183
207
|
}
|
|
184
208
|
if (holdMs > 0)
|
|
@@ -1,41 +1,5 @@
|
|
|
1
1
|
import { SettleReason } from '@reticlehq/core';
|
|
2
|
-
|
|
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
|
-
nth?: number;
|
|
31
|
-
of?: number;
|
|
32
|
-
source?: {
|
|
33
|
-
file: string;
|
|
34
|
-
line: number;
|
|
35
|
-
column?: number;
|
|
36
|
-
};
|
|
37
|
-
warning?: string;
|
|
38
|
-
}
|
|
2
|
+
import { type ActionEffect, type ActionResult } from './action-report.js';
|
|
39
3
|
export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
|
|
40
4
|
export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
|
|
41
5
|
export interface ActionStep {
|
|
@@ -58,6 +22,7 @@ interface SequenceStepResult {
|
|
|
58
22
|
line: number;
|
|
59
23
|
column?: number;
|
|
60
24
|
};
|
|
25
|
+
secret?: true;
|
|
61
26
|
warning?: string;
|
|
62
27
|
}
|
|
63
28
|
export declare function executeSequence(steps: ActionStep[]): Promise<{
|
package/dist/actions/actions.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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';
|
|
4
4
|
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
5
5
|
import { echoRef, refs } from '../dom/addressing/refs.js';
|
|
6
|
-
import {
|
|
7
|
-
import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
|
|
8
|
-
import {
|
|
9
|
-
import { elementHasHoverHandlers
|
|
6
|
+
import { requiresDangerousConfirmation, assertOutwardAllowed, assertPaymentEntryAllowed, submitControlFor, } from './danger-context.js';
|
|
7
|
+
import { assertEditable, assertNotRichText, isAutofillSafeTarget, setNativeValue, } from './value-input.js';
|
|
8
|
+
import { isVisible, getStates } from '../dom/a11y.js';
|
|
9
|
+
import { elementHasHoverHandlers } from '../registry/stores/adapters.js';
|
|
10
10
|
import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/realm.js';
|
|
11
|
-
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';
|
|
12
13
|
import { AppearedText } from './appeared-text.js';
|
|
13
14
|
import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
|
|
14
|
-
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
|
|
15
|
+
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
|
|
16
|
+
import { anchorOf, result, visibleCanvases, } from './action-report.js';
|
|
15
17
|
function asString(value, fallback = '') {
|
|
16
18
|
return 'string' === typeof value ? value : fallback;
|
|
17
19
|
}
|
|
@@ -23,61 +25,6 @@ function requireElement(ref) {
|
|
|
23
25
|
throw new Error(`ref '${echoRef(ref)}' is not an HTML or SVG element`);
|
|
24
26
|
return el;
|
|
25
27
|
}
|
|
26
|
-
function anchorOf(el) {
|
|
27
|
-
const testid = readTestId(el) ?? undefined;
|
|
28
|
-
const info = identifyComponent(el);
|
|
29
|
-
const out = {};
|
|
30
|
-
if (testid !== undefined)
|
|
31
|
-
out.testid = testid;
|
|
32
|
-
const component = info?.componentStack[0];
|
|
33
|
-
if (component !== undefined)
|
|
34
|
-
out.component = component;
|
|
35
|
-
if (info?.source !== undefined)
|
|
36
|
-
out.source = info.source;
|
|
37
|
-
const role = getRole(el);
|
|
38
|
-
const name = getAccessibleName(el);
|
|
39
|
-
if (role.length > 0)
|
|
40
|
-
out.role = role;
|
|
41
|
-
if (name.length > 0)
|
|
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
|
-
}
|
|
51
|
-
return out;
|
|
52
|
-
}
|
|
53
|
-
const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
|
|
54
|
-
const base = {
|
|
55
|
-
ok: true,
|
|
56
|
-
ref,
|
|
57
|
-
action,
|
|
58
|
-
dispatched: true,
|
|
59
|
-
settled,
|
|
60
|
-
settleReason,
|
|
61
|
-
effect,
|
|
62
|
-
};
|
|
63
|
-
if (anchor.testid !== undefined)
|
|
64
|
-
base.testid = anchor.testid;
|
|
65
|
-
if (anchor.component !== undefined)
|
|
66
|
-
base.component = anchor.component;
|
|
67
|
-
if (anchor.source !== undefined)
|
|
68
|
-
base.source = anchor.source;
|
|
69
|
-
if (anchor.role !== undefined)
|
|
70
|
-
base.role = anchor.role;
|
|
71
|
-
if (anchor.name !== undefined)
|
|
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;
|
|
77
|
-
if (warning !== undefined)
|
|
78
|
-
base.warning = warning;
|
|
79
|
-
return base;
|
|
80
|
-
};
|
|
81
28
|
const FILL_LIKE = new Set([
|
|
82
29
|
ActionType.FILL,
|
|
83
30
|
ActionType.TYPE,
|
|
@@ -123,19 +70,31 @@ function assertActionAllowed(el, action, args) {
|
|
|
123
70
|
(action === ActionType.PRESS && 'Enter' === pressKey(args));
|
|
124
71
|
const dragTarget = action === ActionType.DRAG ? refs.resolve(dragTargetRef(args)) : null;
|
|
125
72
|
const submitter = action === ActionType.PRESS && 'Enter' === pressKey(args) ? submitControlFor(el) : null;
|
|
126
|
-
|
|
127
|
-
(submitter
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
73
|
+
if (canTrigger)
|
|
74
|
+
assertOutwardAllowed([el, submitter, isActionTarget(dragTarget) ? dragTarget : null]);
|
|
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);
|
|
131
80
|
if (canTrigger &&
|
|
132
81
|
(sourceDangerous || targetDangerous) &&
|
|
133
82
|
args[DANGEROUS_ACTION_CONFIRM_ARG] !== true) {
|
|
134
83
|
throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
135
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
|
+
}
|
|
136
88
|
}
|
|
137
|
-
|
|
89
|
+
function optionValueFor(options, named) {
|
|
90
|
+
if (options.some((option) => option.value === named))
|
|
91
|
+
return named;
|
|
92
|
+
const byLabel = options.filter((option) => option.label.trim() === named.trim());
|
|
93
|
+
return 1 === byLabel.length ? (byLabel[0]?.value ?? named) : named;
|
|
94
|
+
}
|
|
95
|
+
import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, } from './actions-dom.js';
|
|
138
96
|
import { scrollFrom } from './scroll.js';
|
|
97
|
+
import { clickHitsNoControl } from './click-target.js';
|
|
139
98
|
function enabledOf(el) {
|
|
140
99
|
return !getStates(el).includes(ElementState.DISABLED);
|
|
141
100
|
}
|
|
@@ -209,7 +168,8 @@ async function dispatchOther(el, action, args) {
|
|
|
209
168
|
'To empty a field on purpose use the `clear` action, or fill with an explicit "".');
|
|
210
169
|
}
|
|
211
170
|
assertEditable(el, 'fill');
|
|
212
|
-
el
|
|
171
|
+
if (!isAutofillSafeTarget(el))
|
|
172
|
+
el.focus();
|
|
213
173
|
return setNativeValue(el, args['value']);
|
|
214
174
|
}
|
|
215
175
|
assertNotRichText(el, 'fill');
|
|
@@ -235,8 +195,8 @@ async function dispatchOther(el, action, args) {
|
|
|
235
195
|
if (typeof args['value'] !== 'string') {
|
|
236
196
|
throw new Error("select requires a string `value` — pass it nested, as args: { value: '…' }.");
|
|
237
197
|
}
|
|
238
|
-
const wanted = args['value'];
|
|
239
198
|
const options = Array.from(el.options);
|
|
199
|
+
const wanted = optionValueFor(options, args['value']);
|
|
240
200
|
if (!options.some((option) => option.value === wanted)) {
|
|
241
201
|
const available = options.map((option) => `${option.value} (${option.label})`).join(', ');
|
|
242
202
|
throw new Error(`no <option> with value '${echoRef(wanted)}' — available: ${0 === options.length ? '(none)' : available}`);
|
|
@@ -287,6 +247,8 @@ async function dispatchOther(el, action, args) {
|
|
|
287
247
|
const code = pressCode(args, key);
|
|
288
248
|
const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
|
|
289
249
|
closeModalOnEscape(el, key, down);
|
|
250
|
+
if (down)
|
|
251
|
+
dispatchKeypress(el, key, code, mods);
|
|
290
252
|
const hold = clampHoldMs(args['holdMs']);
|
|
291
253
|
if (hold > 0)
|
|
292
254
|
await holdKey(el, key, code, mods, hold);
|
|
@@ -344,24 +306,22 @@ async function dispatchOther(el, action, args) {
|
|
|
344
306
|
throw new Error(`unknown action '${action}'`);
|
|
345
307
|
}
|
|
346
308
|
}
|
|
347
|
-
function asSettleBudget(raw) {
|
|
348
|
-
if ('number' !== typeof raw || !Number.isFinite(raw))
|
|
349
|
-
return undefined;
|
|
350
|
-
return Math.max(0, Math.min(raw, FRAME_BUDGET_MS_CEILING));
|
|
351
|
-
}
|
|
352
|
-
const FRAME_BUDGET_MS_CEILING = 200;
|
|
353
309
|
export async function executeAction(ref, action, args = {}) {
|
|
354
310
|
const named = 0 < ref.length;
|
|
355
311
|
const el = isReflessDocumentPress(ref, action, args) ? focusedOrDocument() : requireElement(ref);
|
|
356
312
|
if (named)
|
|
357
313
|
assertActionAllowed(el, action, args);
|
|
314
|
+
if (named)
|
|
315
|
+
assertNotLeaving(el);
|
|
358
316
|
const anchor = named ? anchorOf(el) : {};
|
|
359
317
|
const visible = isVisible(el);
|
|
360
318
|
const enabled = enabledOf(el);
|
|
361
319
|
const prevFocus = activeRef(el);
|
|
362
320
|
const valueBefore = valueOf(el);
|
|
321
|
+
const actedField = watchActedField(el);
|
|
363
322
|
const geometry = CLICK_LIKE.has(action) ? clickGeometry(el) : NO_GEOMETRY;
|
|
364
323
|
const alreadyAtValue = alreadyAtCheckedState(el, action);
|
|
324
|
+
const hitsNoControl = clickHitsNoControl(el, action);
|
|
365
325
|
let mutated = 0;
|
|
366
326
|
const said = new AppearedText();
|
|
367
327
|
const obs = new MutationObserver((records) => {
|
|
@@ -389,9 +349,11 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
389
349
|
settled = outcome.settled;
|
|
390
350
|
settleReason = outcome.settled ? null : SettleReason.TIMEOUT;
|
|
391
351
|
obs.disconnect();
|
|
352
|
+
actedField?.stop();
|
|
392
353
|
}
|
|
393
354
|
assertAriaToggleLanded(el, action, alreadyAtValue);
|
|
394
355
|
const valueAfter = valueOf(el);
|
|
356
|
+
publishSettledField(el, actedField);
|
|
395
357
|
const nextFocus = activeRef(el);
|
|
396
358
|
const effect = {
|
|
397
359
|
dispatched: true,
|
|
@@ -401,13 +363,18 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
401
363
|
defaultPrevented,
|
|
402
364
|
focusMoved: prevFocus !== nextFocus ? `${prevFocus ?? 'null'}->${nextFocus ?? 'null'}` : null,
|
|
403
365
|
valueChanged: isFillLike(action) ? valueBefore !== valueAfter : false,
|
|
366
|
+
...(ActionType.FILL === action && isAutofillSafeTarget(el) ? { autofillSafe: true } : {}),
|
|
404
367
|
domMutatedWithin: mutated,
|
|
405
368
|
...said.effect(valueAfter),
|
|
406
369
|
occluded: geometry.occluded,
|
|
407
370
|
occludedBy: geometry.occludedBy,
|
|
408
371
|
scrolledIntoView: geometry.scrolledIntoView,
|
|
372
|
+
...(geometry.occluderName === undefined ? {} : { occluderName: geometry.occluderName }),
|
|
373
|
+
...(true === geometry.noSize ? { noSize: true } : {}),
|
|
374
|
+
...(true === geometry.offViewport ? { offViewport: true } : {}),
|
|
409
375
|
...(heldMs > 0 ? { heldMs } : {}),
|
|
410
376
|
...(alreadyAtValue ? { alreadyAtValue: true } : {}),
|
|
377
|
+
...canvasesOnScreen(),
|
|
411
378
|
};
|
|
412
379
|
const warning = !named
|
|
413
380
|
? ActionWarning.GLOBAL_PRESS
|
|
@@ -415,8 +382,15 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
415
382
|
? ActionWarning.CLICK_OCCLUDED
|
|
416
383
|
: action === ActionType.HOVER && elementHasHoverHandlers(el)
|
|
417
384
|
? ActionWarning.HOVER_NATIVE_ENTER_LEAVE
|
|
418
|
-
:
|
|
419
|
-
|
|
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 } : {};
|
|
420
394
|
}
|
|
421
395
|
const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
|
|
422
396
|
export async function executeSequence(steps) {
|
|
@@ -442,6 +416,8 @@ export async function executeSequence(steps) {
|
|
|
442
416
|
stepBase.name = res.name;
|
|
443
417
|
if (res.source !== undefined)
|
|
444
418
|
stepBase.source = res.source;
|
|
419
|
+
if (res.secret !== undefined)
|
|
420
|
+
stepBase.secret = res.secret;
|
|
445
421
|
if (res.warning !== undefined)
|
|
446
422
|
stepBase.warning = res.warning;
|
|
447
423
|
stepResults.push(stepBase);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isWordsIgnored } from '../dom/dom-ignore.js';
|
|
2
|
+
import { isSensitiveField } from '../dom/a11y.js';
|
|
2
3
|
const APPEARED_MAX = 200;
|
|
3
4
|
const JOIN = ' | ';
|
|
4
5
|
const TEXT_NODE = 3;
|
|
@@ -44,7 +45,9 @@ export class AppearedText {
|
|
|
44
45
|
return this.#length > APPEARED_MAX;
|
|
45
46
|
}
|
|
46
47
|
#add(raw, owner) {
|
|
47
|
-
if (null !== owner &&
|
|
48
|
+
if (null !== owner && isWordsIgnored(owner))
|
|
49
|
+
return;
|
|
50
|
+
if (null !== owner && isSensitiveField(owner))
|
|
48
51
|
return;
|
|
49
52
|
const text = (raw ?? '').replace(/\s+/g, ' ').trim();
|
|
50
53
|
if (0 === text.length)
|
|
@@ -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,4 +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(
|
|
4
|
+
export declare function requiresDangerousConfirmation(el: ActionTarget): boolean;
|
|
5
|
+
export declare function setOutwardPolicy(allowed: readonly string[] | undefined): void;
|
|
6
|
+
export declare function assertOutwardAllowed(targets: readonly (ActionTarget | null)[]): void;
|
|
7
|
+
export declare function assertPaymentEntryAllowed(el: ActionTarget): void;
|