@reticlehq/browser 3.8.0 → 3.8.2
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.js +36 -18
- package/dist/actions/actions-dom.d.ts +9 -1
- package/dist/actions/actions-dom.js +61 -21
- package/dist/actions/actions.js +98 -43
- package/dist/actions/danger-context.js +27 -16
- package/dist/actions/scroll.d.ts +2 -0
- package/dist/actions/scroll.js +10 -2
- package/dist/actions/select-combobox.d.ts +6 -0
- package/dist/actions/select-combobox.js +69 -0
- package/dist/dom/a11y.d.ts +0 -4
- package/dist/dom/a11y.js +2 -294
- package/dist/dom/dom-ignore.js +23 -10
- package/dist/dom/query-finders.d.ts +6 -0
- package/dist/dom/query-finders.js +216 -0
- package/dist/dom/query.d.ts +0 -1
- package/dist/dom/query.js +108 -244
- package/dist/dom/snapshot.js +2 -1
- package/dist/dom/visibility.d.ts +7 -0
- package/dist/dom/visibility.js +322 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/observers/dom.js +2 -1
- package/dist/observers/net-detail/network-body.js +29 -9
- package/dist/observers/net-detail/network-event-stream.d.ts +5 -0
- package/dist/observers/net-detail/network-event-stream.js +55 -0
- package/dist/observers/net-detail/network-frames.d.ts +2 -0
- package/dist/observers/net-detail/network-frames.js +39 -0
- package/dist/observers/net-detail/network-response-body.d.ts +3 -0
- package/dist/observers/net-detail/network-response-body.js +97 -0
- package/dist/observers/network.js +95 -105
- package/dist/reticle-inject.js +695 -631
- package/package.json +2 -2
|
@@ -10,13 +10,15 @@ export function visibleCanvases(doc) {
|
|
|
10
10
|
const height = doc.defaultView?.innerHeight ?? 0;
|
|
11
11
|
let count = 0;
|
|
12
12
|
for (const canvas of doc.querySelectorAll(CANVAS_SELECTOR)) {
|
|
13
|
-
if (isReticleUi(canvas))
|
|
13
|
+
if (isReticleUi(canvas)) {
|
|
14
14
|
continue;
|
|
15
|
+
}
|
|
15
16
|
const box = canvas.getBoundingClientRect();
|
|
16
17
|
const sized = box.width > 0 && box.height > 0;
|
|
17
18
|
const onScreen = box.bottom > 0 && box.right > 0 && box.top < height && box.left < width;
|
|
18
|
-
if (sized && onScreen)
|
|
19
|
+
if (sized && onScreen) {
|
|
19
20
|
count += 1;
|
|
21
|
+
}
|
|
20
22
|
}
|
|
21
23
|
return count;
|
|
22
24
|
}
|
|
@@ -27,22 +29,28 @@ export function anchorOf(el) {
|
|
|
27
29
|
if (testid !== undefined) {
|
|
28
30
|
out.testid = testid;
|
|
29
31
|
const shared = elementsMatching({ by: QueryBy.TESTID, value: testid }).length;
|
|
30
|
-
if (1 < shared)
|
|
32
|
+
if (1 < shared) {
|
|
31
33
|
out.testidOf = shared;
|
|
34
|
+
}
|
|
32
35
|
}
|
|
33
36
|
const component = info?.componentStack[0];
|
|
34
|
-
if (component !== undefined)
|
|
37
|
+
if (component !== undefined) {
|
|
35
38
|
out.component = component;
|
|
36
|
-
|
|
39
|
+
}
|
|
40
|
+
if (info?.source !== undefined) {
|
|
37
41
|
out.source = info.source;
|
|
42
|
+
}
|
|
38
43
|
const role = getRole(el);
|
|
39
44
|
const name = getAccessibleName(el);
|
|
40
|
-
if (role.length > 0)
|
|
45
|
+
if (role.length > 0) {
|
|
41
46
|
out.role = role;
|
|
42
|
-
|
|
47
|
+
}
|
|
48
|
+
if (name.length > 0) {
|
|
43
49
|
out.name = name;
|
|
44
|
-
|
|
50
|
+
}
|
|
51
|
+
if (isSensitiveField(el)) {
|
|
45
52
|
out.secret = true;
|
|
53
|
+
}
|
|
46
54
|
if (role.length > 0 && name.length > 0) {
|
|
47
55
|
const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
|
|
48
56
|
const at = same.indexOf(el);
|
|
@@ -63,25 +71,35 @@ export const result = (ref, action, effect, settled, settleReason, anchor, warni
|
|
|
63
71
|
settleReason,
|
|
64
72
|
effect,
|
|
65
73
|
};
|
|
66
|
-
if (anchor.testid !== undefined)
|
|
74
|
+
if (anchor.testid !== undefined) {
|
|
67
75
|
base.testid = anchor.testid;
|
|
68
|
-
|
|
76
|
+
}
|
|
77
|
+
if (anchor.testidOf !== undefined) {
|
|
69
78
|
base.testidOf = anchor.testidOf;
|
|
70
|
-
|
|
79
|
+
}
|
|
80
|
+
if (anchor.component !== undefined) {
|
|
71
81
|
base.component = anchor.component;
|
|
72
|
-
|
|
82
|
+
}
|
|
83
|
+
if (anchor.source !== undefined) {
|
|
73
84
|
base.source = anchor.source;
|
|
74
|
-
|
|
85
|
+
}
|
|
86
|
+
if (anchor.role !== undefined) {
|
|
75
87
|
base.role = anchor.role;
|
|
76
|
-
|
|
88
|
+
}
|
|
89
|
+
if (anchor.name !== undefined) {
|
|
77
90
|
base.name = anchor.name;
|
|
78
|
-
|
|
91
|
+
}
|
|
92
|
+
if (anchor.nth !== undefined) {
|
|
79
93
|
base.nth = anchor.nth;
|
|
80
|
-
|
|
94
|
+
}
|
|
95
|
+
if (anchor.of !== undefined) {
|
|
81
96
|
base.of = anchor.of;
|
|
82
|
-
|
|
97
|
+
}
|
|
98
|
+
if (anchor.secret !== undefined) {
|
|
83
99
|
base.secret = anchor.secret;
|
|
84
|
-
|
|
100
|
+
}
|
|
101
|
+
if (warning !== undefined) {
|
|
85
102
|
base.warning = warning;
|
|
103
|
+
}
|
|
86
104
|
return base;
|
|
87
105
|
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ActionTarget } from '../dom/realm.js';
|
|
2
|
+
import { ClickButton } from '@reticlehq/core';
|
|
3
|
+
import { type ModifierFlags } from './actions-press.js';
|
|
2
4
|
interface ClickGeometry {
|
|
3
5
|
occluded: boolean;
|
|
4
6
|
occludedBy: string | null;
|
|
@@ -9,11 +11,17 @@ interface ClickGeometry {
|
|
|
9
11
|
}
|
|
10
12
|
export declare const NO_GEOMETRY: ClickGeometry;
|
|
11
13
|
export declare function mouseEventFor(el: Element, type: string, init?: MouseEventInit): MouseEvent;
|
|
14
|
+
export interface ClickInput {
|
|
15
|
+
modifiers: ModifierFlags;
|
|
16
|
+
button: ClickButton;
|
|
17
|
+
}
|
|
18
|
+
export declare const PLAIN_CLICK: ClickInput;
|
|
19
|
+
export declare function clickInputOf(args: Record<string, unknown>): ClickInput;
|
|
12
20
|
export declare function fireClickSequence(el: ActionTarget, hold?: {
|
|
13
21
|
ms: number;
|
|
14
22
|
sleep: (ms: number) => Promise<void>;
|
|
15
23
|
now: () => number;
|
|
16
|
-
}, detail?: number): Promise<{
|
|
24
|
+
}, detail?: number, input?: ClickInput): Promise<{
|
|
17
25
|
prevented: boolean;
|
|
18
26
|
heldMs: number;
|
|
19
27
|
}>;
|
|
@@ -4,6 +4,8 @@ import { getAccessibleName, getRole } from '../dom/a11y.js';
|
|
|
4
4
|
import { hitTestOccluder } from '../dom/occlusion.js';
|
|
5
5
|
import { nativeFrame } from '../timers/native/native-timers.js';
|
|
6
6
|
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
7
|
+
import { ClickButton, clickButtonFromArgs } from '@reticlehq/core';
|
|
8
|
+
import { pressModifiers } from './actions-press.js';
|
|
7
9
|
export const NO_GEOMETRY = {
|
|
8
10
|
occluded: false,
|
|
9
11
|
occludedBy: null,
|
|
@@ -20,7 +22,22 @@ function pointerEventFor(el, type, init) {
|
|
|
20
22
|
}
|
|
21
23
|
const BUTTON_HELD = 1;
|
|
22
24
|
const BUTTON_RELEASED = 0;
|
|
23
|
-
|
|
25
|
+
export const PLAIN_CLICK = {
|
|
26
|
+
modifiers: { metaKey: false, ctrlKey: false, shiftKey: false, altKey: false },
|
|
27
|
+
button: ClickButton.LEFT,
|
|
28
|
+
};
|
|
29
|
+
const BUTTON_CODES = {
|
|
30
|
+
[ClickButton.LEFT]: { button: 0, bit: 1 },
|
|
31
|
+
[ClickButton.RIGHT]: { button: 2, bit: 2 },
|
|
32
|
+
[ClickButton.MIDDLE]: { button: 1, bit: 4 },
|
|
33
|
+
};
|
|
34
|
+
export function clickInputOf(args) {
|
|
35
|
+
return {
|
|
36
|
+
modifiers: pressModifiers(args),
|
|
37
|
+
button: clickButtonFromArgs(args) ?? ClickButton.LEFT,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function fireAt(el, type, at, buttons, related, detail = 0, input = PLAIN_CLICK) {
|
|
24
41
|
const init = {
|
|
25
42
|
bubbles: !(type.endsWith('enter') || type.endsWith('leave')),
|
|
26
43
|
cancelable: true,
|
|
@@ -30,31 +47,42 @@ function fireAt(el, type, at, buttons, related, detail = 0) {
|
|
|
30
47
|
screenX: at.x,
|
|
31
48
|
screenY: at.y,
|
|
32
49
|
buttons,
|
|
33
|
-
button:
|
|
50
|
+
button: BUTTON_CODES[input.button].button,
|
|
51
|
+
...input.modifiers,
|
|
34
52
|
...(related !== undefined ? { relatedTarget: related } : {}),
|
|
35
53
|
};
|
|
36
54
|
return el.dispatchEvent(type.startsWith('pointer')
|
|
37
55
|
? pointerEventFor(el, type, { ...init, pointerId: 1, isPrimary: true })
|
|
38
56
|
: mouseEventFor(el, type, init));
|
|
39
57
|
}
|
|
40
|
-
export async function fireClickSequence(el, hold, detail) {
|
|
58
|
+
export async function fireClickSequence(el, hold, detail, input = PLAIN_CLICK) {
|
|
41
59
|
const doc = el.ownerDocument;
|
|
42
60
|
const from = doc.activeElement ?? doc.body;
|
|
43
61
|
const at = centreOf(el);
|
|
44
62
|
const d = detail ?? 0;
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
const held = BUTTON_CODES[input.button].bit;
|
|
64
|
+
const isLeft = ClickButton.LEFT === input.button;
|
|
65
|
+
let notPrevented = true;
|
|
66
|
+
asSyntheticInput(() => fireAt(el, 'pointerdown', at, held, from, d, input));
|
|
67
|
+
asSyntheticInput(() => fireAt(el, 'mousedown', at, held, undefined, d, input));
|
|
68
|
+
if (ClickButton.RIGHT === input.button) {
|
|
69
|
+
notPrevented = asSyntheticInput(() => fireAt(el, 'contextmenu', at, held, undefined, d, input));
|
|
70
|
+
}
|
|
71
|
+
if (el.tabIndex >= 0 && 'function' === typeof el.focus) {
|
|
48
72
|
el.focus();
|
|
73
|
+
}
|
|
49
74
|
let heldMs = 0;
|
|
50
75
|
if (hold !== undefined && hold.ms > 0) {
|
|
51
76
|
const startedAt = hold.now();
|
|
52
77
|
await hold.sleep(hold.ms);
|
|
53
78
|
heldMs = hold.now() - startedAt;
|
|
54
79
|
}
|
|
55
|
-
asSyntheticInput(() => fireAt(el, 'pointerup', at, BUTTON_RELEASED, from, d));
|
|
56
|
-
asSyntheticInput(() => fireAt(el, 'mouseup', at, BUTTON_RELEASED, undefined, d));
|
|
57
|
-
const
|
|
80
|
+
asSyntheticInput(() => fireAt(el, 'pointerup', at, BUTTON_RELEASED, from, d, input));
|
|
81
|
+
asSyntheticInput(() => fireAt(el, 'mouseup', at, BUTTON_RELEASED, undefined, d, input));
|
|
82
|
+
const clicked = asSyntheticInput(() => fireAt(el, isLeft ? 'click' : 'auxclick', at, BUTTON_RELEASED, undefined, d, input));
|
|
83
|
+
if (ClickButton.RIGHT !== input.button) {
|
|
84
|
+
notPrevented = clicked;
|
|
85
|
+
}
|
|
58
86
|
return { prevented: !notPrevented, heldMs };
|
|
59
87
|
}
|
|
60
88
|
function isMeasurable(rect) {
|
|
@@ -62,8 +90,9 @@ function isMeasurable(rect) {
|
|
|
62
90
|
}
|
|
63
91
|
export function isOffViewport(el, rect) {
|
|
64
92
|
const win = el.ownerDocument.defaultView;
|
|
65
|
-
if (null === win)
|
|
93
|
+
if (null === win) {
|
|
66
94
|
return false;
|
|
95
|
+
}
|
|
67
96
|
const cx = rect.left + rect.width / 2;
|
|
68
97
|
const cy = rect.top + rect.height / 2;
|
|
69
98
|
return cx < 0 || cy < 0 || cx > win.innerWidth || cy > win.innerHeight;
|
|
@@ -76,23 +105,27 @@ function hitTest(el, rect) {
|
|
|
76
105
|
}
|
|
77
106
|
function nameOf(el) {
|
|
78
107
|
const testid = readTestId(el);
|
|
79
|
-
if (testid !== null && testid.length > 0)
|
|
108
|
+
if (testid !== null && testid.length > 0) {
|
|
80
109
|
return `[data-testid="${testid}"]`;
|
|
110
|
+
}
|
|
81
111
|
const name = getAccessibleName(el).trim();
|
|
82
112
|
return name.length > 0 ? `${getRole(el)} "${name.slice(0, 40)}"` : el.tagName.toLowerCase();
|
|
83
113
|
}
|
|
84
114
|
function hasNoSize(el) {
|
|
85
|
-
if (typeof el.getClientRects !== 'function')
|
|
115
|
+
if (typeof el.getClientRects !== 'function') {
|
|
86
116
|
return false;
|
|
117
|
+
}
|
|
87
118
|
const rects = Array.from(el.getClientRects());
|
|
88
119
|
return rects.length > 0 && rects.every((r) => 0 === r.width || 0 === r.height);
|
|
89
120
|
}
|
|
90
121
|
export function clickGeometry(el) {
|
|
91
|
-
if (typeof el.getBoundingClientRect !== 'function')
|
|
122
|
+
if (typeof el.getBoundingClientRect !== 'function') {
|
|
92
123
|
return NO_GEOMETRY;
|
|
124
|
+
}
|
|
93
125
|
let rect = el.getBoundingClientRect();
|
|
94
|
-
if (!isMeasurable(rect))
|
|
126
|
+
if (!isMeasurable(rect)) {
|
|
95
127
|
return hasNoSize(el) ? { ...NO_GEOMETRY, noSize: true } : NO_GEOMETRY;
|
|
128
|
+
}
|
|
96
129
|
let scrolledIntoView = false;
|
|
97
130
|
if (isOffViewport(el, rect) && 'function' === typeof el.scrollIntoView) {
|
|
98
131
|
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
@@ -109,15 +142,17 @@ export function firePointerNonBubbling(el, type, relatedTarget = null) {
|
|
|
109
142
|
asSyntheticInput(() => el.dispatchEvent(pointerEventFor(el, type, { bubbles: false, cancelable: true, relatedTarget })));
|
|
110
143
|
}
|
|
111
144
|
function makeDataTransfer(data) {
|
|
112
|
-
if (typeof DataTransfer !== 'function')
|
|
145
|
+
if (typeof DataTransfer !== 'function') {
|
|
113
146
|
return null;
|
|
147
|
+
}
|
|
114
148
|
const dt = new DataTransfer();
|
|
115
149
|
const entries = Array.isArray(data) ? data : data !== undefined ? [data] : [];
|
|
116
150
|
for (const entry of entries) {
|
|
117
151
|
if ('object' === typeof entry && entry !== null) {
|
|
118
152
|
const e = entry;
|
|
119
|
-
if ('string' === typeof e.mime && 'string' === typeof e.value)
|
|
153
|
+
if ('string' === typeof e.mime && 'string' === typeof e.value) {
|
|
120
154
|
dt.setData(e.mime, e.value);
|
|
155
|
+
}
|
|
121
156
|
}
|
|
122
157
|
}
|
|
123
158
|
return dt;
|
|
@@ -174,8 +209,9 @@ export async function dragElement(source, target, data) {
|
|
|
174
209
|
if ('function' === typeof DragEvent) {
|
|
175
210
|
const dataTransfer = makeDataTransfer(data);
|
|
176
211
|
const init = { bubbles: true, cancelable: true };
|
|
177
|
-
if (dataTransfer !== null)
|
|
212
|
+
if (dataTransfer !== null) {
|
|
178
213
|
init.dataTransfer = dataTransfer;
|
|
214
|
+
}
|
|
179
215
|
source.dispatchEvent(new DragEvent('dragstart', init));
|
|
180
216
|
await nativeFrame();
|
|
181
217
|
dest.dispatchEvent(new DragEvent('dragenter', init));
|
|
@@ -190,8 +226,9 @@ export async function fireTapSequence(el, hold) {
|
|
|
190
226
|
const touches = touchListFor(el);
|
|
191
227
|
firePointerTouch(el, 'pointerdown');
|
|
192
228
|
asSyntheticInput(() => el.dispatchEvent(makeTouchEvent('touchstart', touches)));
|
|
193
|
-
if (el.tabIndex >= 0 && 'function' === typeof el.focus)
|
|
229
|
+
if (el.tabIndex >= 0 && 'function' === typeof el.focus) {
|
|
194
230
|
el.focus();
|
|
231
|
+
}
|
|
195
232
|
let heldMs = 0;
|
|
196
233
|
if (hold !== undefined && hold.ms > 0) {
|
|
197
234
|
const started = hold.now();
|
|
@@ -214,8 +251,9 @@ function firePointerTouch(el, type) {
|
|
|
214
251
|
})));
|
|
215
252
|
}
|
|
216
253
|
function touchListFor(el) {
|
|
217
|
-
if ('function' !== typeof Touch || 'function' !== typeof el.getBoundingClientRect)
|
|
254
|
+
if ('function' !== typeof Touch || 'function' !== typeof el.getBoundingClientRect) {
|
|
218
255
|
return [];
|
|
256
|
+
}
|
|
219
257
|
const rect = el.getBoundingClientRect();
|
|
220
258
|
try {
|
|
221
259
|
return [
|
|
@@ -251,10 +289,12 @@ const POPUP_ROLES = new Set(['menu', 'menubar', 'listbox', 'dialog', 'alertdialo
|
|
|
251
289
|
const CLOSED_STATE = 'closed';
|
|
252
290
|
export function assertNotLeaving(el) {
|
|
253
291
|
for (let up = el.parentElement; up !== null; up = up.parentElement) {
|
|
254
|
-
if (CLOSED_STATE !== up.getAttribute('data-state'))
|
|
292
|
+
if (CLOSED_STATE !== up.getAttribute('data-state')) {
|
|
255
293
|
continue;
|
|
256
|
-
|
|
294
|
+
}
|
|
295
|
+
if (!POPUP_ROLES.has(up.getAttribute('role') ?? '')) {
|
|
257
296
|
continue;
|
|
297
|
+
}
|
|
258
298
|
throw new Error(`'${up.getAttribute('role') ?? 'popup'}' holding this element is closing (data-state="closed"), ` +
|
|
259
299
|
'so a person could not click it: look again once it has gone and act on what is open');
|
|
260
300
|
}
|
package/dist/actions/actions.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { ActionType, ActionWarning, clampHoldMs, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
|
|
2
|
-
import { isHiddenNativeOption } from '../dom/query.js';
|
|
1
|
+
import { ActionType, ActionWarning, clampHoldMs, clickInputProblem, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
|
|
2
|
+
import { isHiddenNativeOption } from '../dom/query-finders.js';
|
|
3
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
6
|
import { requiresDangerousConfirmation, assertOutwardAllowed, assertPaymentEntryAllowed, submitControlFor, } from './danger-context.js';
|
|
7
7
|
import { assertEditable, assertNotRichText, isAutofillSafeTarget, setNativeValue, } from './value-input.js';
|
|
8
|
-
import {
|
|
8
|
+
import { getRole, getStates } from '../dom/a11y.js';
|
|
9
|
+
import { isVisible } from '../dom/visibility.js';
|
|
10
|
+
import { selectInCombobox } from './select-combobox.js';
|
|
9
11
|
import { elementHasHoverHandlers } from '../registry/stores/adapters.js';
|
|
10
12
|
import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/realm.js';
|
|
11
13
|
import { asSettleBudget, nativeSetTimeout, settle } from '../timers/native/native-timers.js';
|
|
@@ -19,10 +21,12 @@ function asString(value, fallback = '') {
|
|
|
19
21
|
}
|
|
20
22
|
function requireElement(ref) {
|
|
21
23
|
const el = refs.resolve(ref);
|
|
22
|
-
if (null === el)
|
|
24
|
+
if (null === el) {
|
|
23
25
|
throw new Error(`ref '${echoRef(ref)}' no longer resolves to an element`);
|
|
24
|
-
|
|
26
|
+
}
|
|
27
|
+
if (!isActionTarget(el)) {
|
|
25
28
|
throw new Error(`ref '${echoRef(ref)}' is not an HTML or SVG element`);
|
|
29
|
+
}
|
|
26
30
|
return el;
|
|
27
31
|
}
|
|
28
32
|
const FILL_LIKE = new Set([
|
|
@@ -51,8 +55,9 @@ const GENERIC_ACTION_ARG_KEYS = new Set([
|
|
|
51
55
|
function assertUploadArgs(args) {
|
|
52
56
|
const keys = Object.keys(args);
|
|
53
57
|
const dropped = keys.filter((k) => !UPLOAD_ARG_KEYS.has(k) && !GENERIC_ACTION_ARG_KEYS.has(k));
|
|
54
|
-
if (0 === dropped.length && keys.some((k) => UPLOAD_ARG_KEYS.has(k)))
|
|
58
|
+
if (0 === dropped.length && keys.some((k) => UPLOAD_ARG_KEYS.has(k))) {
|
|
55
59
|
return;
|
|
60
|
+
}
|
|
56
61
|
const detail = 0 === dropped.length
|
|
57
62
|
? 'no file was described'
|
|
58
63
|
: `upload does not read ${dropped.join(', ')}, so it would be dropped`;
|
|
@@ -70,10 +75,12 @@ function assertActionAllowed(el, action, args) {
|
|
|
70
75
|
(action === ActionType.PRESS && 'Enter' === pressKey(args));
|
|
71
76
|
const dragTarget = action === ActionType.DRAG ? refs.resolve(dragTargetRef(args)) : null;
|
|
72
77
|
const submitter = action === ActionType.PRESS && 'Enter' === pressKey(args) ? submitControlFor(el) : null;
|
|
73
|
-
if (canTrigger)
|
|
78
|
+
if (canTrigger) {
|
|
74
79
|
assertOutwardAllowed([el, submitter, isActionTarget(dragTarget) ? dragTarget : null]);
|
|
75
|
-
|
|
80
|
+
}
|
|
81
|
+
if (action === ActionType.FILL || action === ActionType.TYPE) {
|
|
76
82
|
assertPaymentEntryAllowed(el);
|
|
83
|
+
}
|
|
77
84
|
const sourceDangerous = requiresDangerousConfirmation(el) ||
|
|
78
85
|
(submitter !== null && requiresDangerousConfirmation(submitter));
|
|
79
86
|
const targetDangerous = isActionTarget(dragTarget) && requiresDangerousConfirmation(dragTarget);
|
|
@@ -87,12 +94,13 @@ function assertActionAllowed(el, action, args) {
|
|
|
87
94
|
}
|
|
88
95
|
}
|
|
89
96
|
function optionValueFor(options, named) {
|
|
90
|
-
if (options.some((option) => option.value === named))
|
|
97
|
+
if (options.some((option) => option.value === named)) {
|
|
91
98
|
return named;
|
|
99
|
+
}
|
|
92
100
|
const byLabel = options.filter((option) => option.label.trim() === named.trim());
|
|
93
101
|
return 1 === byLabel.length ? (byLabel[0]?.value ?? named) : named;
|
|
94
102
|
}
|
|
95
|
-
import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, } from './actions-dom.js';
|
|
103
|
+
import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, clickInputOf, } from './actions-dom.js';
|
|
96
104
|
import { scrollFrom } from './scroll.js';
|
|
97
105
|
import { clickHitsNoControl } from './click-target.js';
|
|
98
106
|
function enabledOf(el) {
|
|
@@ -106,14 +114,15 @@ function valueOf(el) {
|
|
|
106
114
|
}
|
|
107
115
|
function activeRef(el) {
|
|
108
116
|
const active = el.ownerDocument.activeElement;
|
|
109
|
-
if (null === active || active === el.ownerDocument.body)
|
|
117
|
+
if (null === active || active === el.ownerDocument.body) {
|
|
110
118
|
return null;
|
|
119
|
+
}
|
|
111
120
|
return refs.refFor(active);
|
|
112
121
|
}
|
|
113
122
|
async function dispatchFor(el, action, args) {
|
|
114
123
|
if (ActionType.CLICK === action) {
|
|
115
124
|
const hold = clampHoldMs(args['holdMs']);
|
|
116
|
-
return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
|
|
125
|
+
return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() }, undefined, clickInputOf(args));
|
|
117
126
|
}
|
|
118
127
|
if (ActionType.TAP === action) {
|
|
119
128
|
const hold = clampHoldMs(args['holdMs']);
|
|
@@ -124,15 +133,27 @@ async function dispatchFor(el, action, args) {
|
|
|
124
133
|
export const ZOOM_NEEDS_REAL_BROWSER_MSG = 'cannot zoom from inside the page — CSS zoom changes how it LOOKS without changing the layout ' +
|
|
125
134
|
'viewport, visualViewport or media queries, so a layout that breaks at 200% would be reported as ' +
|
|
126
135
|
'checked and passing. Drive a real browser (reticle drive <url>, or reticle_lease) and zoom there.';
|
|
136
|
+
function isAriaCombobox(el) {
|
|
137
|
+
return 'combobox' === getRole(el) || 'listbox' === el.getAttribute('aria-haspopup');
|
|
138
|
+
}
|
|
139
|
+
const SELECT_ON_COMBOBOX_MSG = (tag) => `cannot select on a <${tag}> — it is an ARIA combobox, not a native <select>. ` +
|
|
140
|
+
'Click the combobox to open it, then click the option with { role: "option", name: "<label>" }.';
|
|
127
141
|
function asFiniteNumber(raw) {
|
|
128
142
|
return 'number' === typeof raw && Number.isFinite(raw) ? raw : undefined;
|
|
129
143
|
}
|
|
130
144
|
async function dispatchOther(el, action, args) {
|
|
131
145
|
switch (action) {
|
|
132
|
-
case ActionType.DBLCLICK:
|
|
133
|
-
|
|
134
|
-
await fireClickSequence(el, undefined,
|
|
135
|
-
|
|
146
|
+
case ActionType.DBLCLICK: {
|
|
147
|
+
const input = clickInputOf(args);
|
|
148
|
+
await fireClickSequence(el, undefined, 1, input);
|
|
149
|
+
await fireClickSequence(el, undefined, 2, input);
|
|
150
|
+
return !asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'dblclick', {
|
|
151
|
+
bubbles: true,
|
|
152
|
+
cancelable: true,
|
|
153
|
+
detail: 2,
|
|
154
|
+
...input.modifiers,
|
|
155
|
+
})));
|
|
156
|
+
}
|
|
136
157
|
case ActionType.HOVER: {
|
|
137
158
|
const doc = el.ownerDocument;
|
|
138
159
|
const from = doc.activeElement ?? doc.body;
|
|
@@ -143,8 +164,9 @@ async function dispatchOther(el, action, args) {
|
|
|
143
164
|
firePointer(el, 'pointermove', from);
|
|
144
165
|
const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true }));
|
|
145
166
|
const holdMs = clampHoldMs(args['holdMs']);
|
|
146
|
-
if (holdMs > 0)
|
|
167
|
+
if (holdMs > 0) {
|
|
147
168
|
await sleep(holdMs);
|
|
169
|
+
}
|
|
148
170
|
return !moved;
|
|
149
171
|
}
|
|
150
172
|
case ActionType.FOCUS:
|
|
@@ -168,8 +190,9 @@ async function dispatchOther(el, action, args) {
|
|
|
168
190
|
'To empty a field on purpose use the `clear` action, or fill with an explicit "".');
|
|
169
191
|
}
|
|
170
192
|
assertEditable(el, 'fill');
|
|
171
|
-
if (!isAutofillSafeTarget(el))
|
|
193
|
+
if (!isAutofillSafeTarget(el)) {
|
|
172
194
|
el.focus();
|
|
195
|
+
}
|
|
173
196
|
return setNativeValue(el, args['value']);
|
|
174
197
|
}
|
|
175
198
|
assertNotRichText(el, 'fill');
|
|
@@ -205,6 +228,12 @@ async function dispatchOther(el, action, args) {
|
|
|
205
228
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
206
229
|
return false;
|
|
207
230
|
}
|
|
231
|
+
if (isAriaCombobox(el)) {
|
|
232
|
+
if (typeof args['value'] !== 'string') {
|
|
233
|
+
throw new Error("select requires a string `value` — pass it nested, as args: { value: '…' }.");
|
|
234
|
+
}
|
|
235
|
+
return await selectInCombobox(el, args['value'], (target) => fireClickSequence(target), SELECT_ON_COMBOBOX_MSG(el.tagName.toLowerCase()));
|
|
236
|
+
}
|
|
208
237
|
throw new Error(`cannot select on a <${el.tagName.toLowerCase()}>`);
|
|
209
238
|
case ActionType.CHECK:
|
|
210
239
|
case ActionType.UNCHECK: {
|
|
@@ -221,8 +250,9 @@ async function dispatchOther(el, action, args) {
|
|
|
221
250
|
if ('radio' === el.type && action === ActionType.UNCHECK) {
|
|
222
251
|
throw new Error('cannot uncheck a radio button — a real user could not; select another radio in the group');
|
|
223
252
|
}
|
|
224
|
-
if (alreadyAtCheckedState(el, action))
|
|
253
|
+
if (alreadyAtCheckedState(el, action)) {
|
|
225
254
|
return false;
|
|
255
|
+
}
|
|
226
256
|
const event = mouseEventFor(el, 'click', {
|
|
227
257
|
bubbles: true,
|
|
228
258
|
cancelable: true,
|
|
@@ -233,25 +263,29 @@ async function dispatchOther(el, action, args) {
|
|
|
233
263
|
}
|
|
234
264
|
case ActionType.SUBMIT: {
|
|
235
265
|
const form = isForm(el) ? el : el.closest('form');
|
|
236
|
-
if (null === form)
|
|
266
|
+
if (null === form) {
|
|
237
267
|
throw new Error('no form to submit');
|
|
268
|
+
}
|
|
238
269
|
form.requestSubmit();
|
|
239
270
|
return false;
|
|
240
271
|
}
|
|
241
272
|
case ActionType.PRESS: {
|
|
242
273
|
const mods = pressModifiers(args);
|
|
243
274
|
const combo = pressKeys(args);
|
|
244
|
-
if (combo.length > 0)
|
|
275
|
+
if (combo.length > 0) {
|
|
245
276
|
return await pressCombo(el, combo, mods, clampHoldMs(args['holdMs']));
|
|
277
|
+
}
|
|
246
278
|
const key = pressKey(args);
|
|
247
279
|
const code = pressCode(args, key);
|
|
248
280
|
const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
|
|
249
281
|
closeModalOnEscape(el, key, down);
|
|
250
|
-
if (down)
|
|
282
|
+
if (down) {
|
|
251
283
|
dispatchKeypress(el, key, code, mods);
|
|
284
|
+
}
|
|
252
285
|
const hold = clampHoldMs(args['holdMs']);
|
|
253
|
-
if (hold > 0)
|
|
286
|
+
if (hold > 0) {
|
|
254
287
|
await holdKey(el, key, code, mods, hold);
|
|
288
|
+
}
|
|
255
289
|
asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
|
|
256
290
|
return !down;
|
|
257
291
|
}
|
|
@@ -277,8 +311,9 @@ async function dispatchOther(el, action, args) {
|
|
|
277
311
|
if (true === args['__base64']) {
|
|
278
312
|
const bin = atob(rawContent);
|
|
279
313
|
const arr = new Uint8Array(bin.length);
|
|
280
|
-
for (let i = 0; i < bin.length; i++)
|
|
314
|
+
for (let i = 0; i < bin.length; i++) {
|
|
281
315
|
arr[i] = bin.charCodeAt(i);
|
|
316
|
+
}
|
|
282
317
|
fileBody = arr.buffer.slice(0);
|
|
283
318
|
}
|
|
284
319
|
const file = new File([fileBody], asString(args['name'], 'file.txt'), {
|
|
@@ -293,8 +328,9 @@ async function dispatchOther(el, action, args) {
|
|
|
293
328
|
}
|
|
294
329
|
case ActionType.DRAG: {
|
|
295
330
|
const toRef = dragTargetRef(args);
|
|
296
|
-
if ('' === toRef)
|
|
331
|
+
if ('' === toRef) {
|
|
297
332
|
return await dragElement(el, null, args['data']);
|
|
333
|
+
}
|
|
298
334
|
const resolved = refs.resolve(toRef);
|
|
299
335
|
if (!isActionTarget(resolved)) {
|
|
300
336
|
throw new Error(`drag target '${echoRef(toRef)}' did not resolve to an element — pass a ref from ` +
|
|
@@ -308,11 +344,19 @@ async function dispatchOther(el, action, args) {
|
|
|
308
344
|
}
|
|
309
345
|
export async function executeAction(ref, action, args = {}) {
|
|
310
346
|
const named = 0 < ref.length;
|
|
347
|
+
if (ActionType.CLICK === action || ActionType.DBLCLICK === action) {
|
|
348
|
+
const problem = clickInputProblem(action, args);
|
|
349
|
+
if (problem !== undefined) {
|
|
350
|
+
throw new Error(`cannot ${action}: ${problem}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
311
353
|
const el = isReflessDocumentPress(ref, action, args) ? focusedOrDocument() : requireElement(ref);
|
|
312
|
-
if (named)
|
|
354
|
+
if (named) {
|
|
313
355
|
assertActionAllowed(el, action, args);
|
|
314
|
-
|
|
356
|
+
}
|
|
357
|
+
if (named) {
|
|
315
358
|
assertNotLeaving(el);
|
|
359
|
+
}
|
|
316
360
|
const anchor = named ? anchorOf(el) : {};
|
|
317
361
|
const visible = isVisible(el);
|
|
318
362
|
const enabled = enabledOf(el);
|
|
@@ -376,15 +420,19 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
376
420
|
...(alreadyAtValue ? { alreadyAtValue: true } : {}),
|
|
377
421
|
...canvasesOnScreen(),
|
|
378
422
|
};
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
423
|
+
let warning;
|
|
424
|
+
if (!named) {
|
|
425
|
+
warning = ActionWarning.GLOBAL_PRESS;
|
|
426
|
+
}
|
|
427
|
+
else if (geometry.occluded) {
|
|
428
|
+
warning = ActionWarning.CLICK_OCCLUDED;
|
|
429
|
+
}
|
|
430
|
+
else if (action === ActionType.HOVER && elementHasHoverHandlers(el)) {
|
|
431
|
+
warning = ActionWarning.HOVER_NATIVE_ENTER_LEAVE;
|
|
432
|
+
}
|
|
433
|
+
else if (hitsNoControl) {
|
|
434
|
+
warning = ActionWarning.CLICK_NOT_A_CONTROL;
|
|
435
|
+
}
|
|
388
436
|
const done = result(ref, action, effect, settled, settleReason, anchor, warning);
|
|
389
437
|
return isReticleUi(el) ? { ...done, reticleUi: true } : done;
|
|
390
438
|
}
|
|
@@ -406,20 +454,27 @@ export async function executeSequence(steps) {
|
|
|
406
454
|
settled: res.settled,
|
|
407
455
|
settleReason: res.settleReason,
|
|
408
456
|
};
|
|
409
|
-
if (res.testid !== undefined)
|
|
457
|
+
if (res.testid !== undefined) {
|
|
410
458
|
stepBase.testid = res.testid;
|
|
411
|
-
|
|
459
|
+
}
|
|
460
|
+
if (res.component !== undefined) {
|
|
412
461
|
stepBase.component = res.component;
|
|
413
|
-
|
|
462
|
+
}
|
|
463
|
+
if (res.role !== undefined) {
|
|
414
464
|
stepBase.role = res.role;
|
|
415
|
-
|
|
465
|
+
}
|
|
466
|
+
if (res.name !== undefined) {
|
|
416
467
|
stepBase.name = res.name;
|
|
417
|
-
|
|
468
|
+
}
|
|
469
|
+
if (res.source !== undefined) {
|
|
418
470
|
stepBase.source = res.source;
|
|
419
|
-
|
|
471
|
+
}
|
|
472
|
+
if (res.secret !== undefined) {
|
|
420
473
|
stepBase.secret = res.secret;
|
|
421
|
-
|
|
474
|
+
}
|
|
475
|
+
if (res.warning !== undefined) {
|
|
422
476
|
stepBase.warning = res.warning;
|
|
477
|
+
}
|
|
423
478
|
stepResults.push(stepBase);
|
|
424
479
|
}
|
|
425
480
|
return { ok: true, count: steps.length, effects, steps: stepResults };
|