@reticlehq/browser 3.7.0 → 3.8.1
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 +10 -0
- package/dist/actions/action-report.js +59 -14
- package/dist/actions/actions-dom.d.ts +4 -0
- package/dist/actions/actions-dom.js +20 -4
- package/dist/actions/actions.d.ts +1 -0
- package/dist/actions/actions.js +94 -46
- package/dist/actions/appeared-text.js +2 -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 +2 -1
- package/dist/actions/danger-context.js +43 -17
- package/dist/actions/scroll.d.ts +2 -0
- package/dist/actions/scroll.js +10 -2
- 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 +45 -17
- package/dist/connect-options.d.ts +12 -1
- package/dist/connect-options.js +18 -1
- package/dist/dom/a11y.d.ts +0 -2
- package/dist/dom/a11y.js +103 -363
- package/dist/dom/chart.d.ts +3 -2
- package/dist/dom/chart.js +3 -2
- package/dist/dom/dom-ignore.d.ts +6 -0
- package/dist/dom/dom-ignore.js +60 -5
- 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-finders.d.ts +6 -0
- package/dist/dom/query-finders.js +216 -0
- package/dist/dom/query.d.ts +1 -1
- package/dist/dom/query.js +154 -227
- 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 +87 -35
- 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/dom/visibility.d.ts +7 -0
- package/dist/dom/visibility.js +322 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +7 -1
- 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 +68 -19
- package/dist/observers/field.d.ts +11 -0
- package/dist/observers/field.js +117 -13
- 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 +36 -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 +111 -101
- package/dist/observers/route.d.ts +2 -0
- package/dist/observers/route.js +33 -4
- package/dist/presenter/chrome/presenter-report-copy.d.ts +0 -5
- package/dist/presenter/chrome/presenter-report-copy.js +0 -14
- package/dist/presenter/hud-telemetry.js +1 -1
- package/dist/presenter/presenter-chat-views.d.ts +3 -4
- package/dist/presenter/presenter-chat-views.js +47 -20
- package/dist/presenter/presenter-config.d.ts +1 -0
- package/dist/presenter/presenter-config.js +1 -0
- package/dist/presenter/presenter-frame.d.ts +3 -2
- package/dist/presenter/presenter-frame.js +24 -15
- package/dist/presenter/presenter-harness-row.d.ts +4 -15
- package/dist/presenter/presenter-harness-row.js +11 -50
- package/dist/presenter/presenter-report-styles.js +0 -17
- package/dist/presenter/presenter-report.d.ts +1 -2
- package/dist/presenter/presenter-report.js +4 -54
- package/dist/presenter/presenter-run-state.js +2 -1
- package/dist/presenter/presenter-settings.d.ts +2 -3
- package/dist/presenter/presenter-settings.js +14 -17
- package/dist/presenter/presenter-shell.d.ts +6 -4
- package/dist/presenter/presenter-shell.js +37 -7
- package/dist/presenter/presenter.js +32 -11
- package/dist/registry/store-adapters.d.ts +5 -2
- package/dist/registry/store-adapters.js +40 -10
- package/dist/reticle-inject.js +700 -645
- package/dist/reticle.d.ts +4 -2
- package/dist/reticle.js +36 -6
- 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 +1 -0
- package/dist/storage-keys.js +1 -0
- 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 +18 -0
- package/package.json +6 -2
|
@@ -13,9 +13,14 @@ export interface ActionEffect {
|
|
|
13
13
|
occluded: boolean;
|
|
14
14
|
occludedBy: string | null;
|
|
15
15
|
scrolledIntoView: boolean;
|
|
16
|
+
occluderName?: string;
|
|
17
|
+
noSize?: true;
|
|
18
|
+
offViewport?: true;
|
|
16
19
|
heldMs?: number;
|
|
17
20
|
alreadyAtValue?: true;
|
|
21
|
+
canvases?: number;
|
|
18
22
|
}
|
|
23
|
+
export declare function visibleCanvases(doc: Document): number;
|
|
19
24
|
export interface ActionResult {
|
|
20
25
|
ok: true;
|
|
21
26
|
ref: string;
|
|
@@ -25,6 +30,7 @@ export interface ActionResult {
|
|
|
25
30
|
settleReason: SettleReason | null;
|
|
26
31
|
effect: ActionEffect;
|
|
27
32
|
testid?: string;
|
|
33
|
+
testidOf?: number;
|
|
28
34
|
component?: string;
|
|
29
35
|
role?: string;
|
|
30
36
|
name?: string;
|
|
@@ -35,10 +41,13 @@ export interface ActionResult {
|
|
|
35
41
|
line: number;
|
|
36
42
|
column?: number;
|
|
37
43
|
};
|
|
44
|
+
secret?: true;
|
|
38
45
|
warning?: string;
|
|
46
|
+
reticleUi?: true;
|
|
39
47
|
}
|
|
40
48
|
export interface CapturedAnchor {
|
|
41
49
|
testid?: string;
|
|
50
|
+
testidOf?: number;
|
|
42
51
|
component?: string;
|
|
43
52
|
source?: {
|
|
44
53
|
file: string;
|
|
@@ -49,6 +58,7 @@ export interface CapturedAnchor {
|
|
|
49
58
|
name?: string;
|
|
50
59
|
nth?: number;
|
|
51
60
|
of?: number;
|
|
61
|
+
secret?: true;
|
|
52
62
|
}
|
|
53
63
|
export declare function anchorOf(el: Element): CapturedAnchor;
|
|
54
64
|
export declare const result: (ref: string, action: string, effect: ActionEffect, settled: boolean, settleReason: SettleReason | null, anchor: CapturedAnchor, warning?: string) => ActionResult;
|
|
@@ -1,25 +1,56 @@
|
|
|
1
1
|
import { readTestId } from '../dom/addressing/testid-attr.js';
|
|
2
2
|
import { QueryBy } from '@reticlehq/core';
|
|
3
3
|
import { elementsMatching } from '../dom/query.js';
|
|
4
|
-
import { getAccessibleName, getRole } from '../dom/a11y.js';
|
|
4
|
+
import { getAccessibleName, getRole, isSensitiveField } from '../dom/a11y.js';
|
|
5
5
|
import { identifyComponent } from '../registry/stores/adapters.js';
|
|
6
|
+
import { isReticleUi } from '../dom/dom-ignore.js';
|
|
7
|
+
const CANVAS_SELECTOR = 'canvas';
|
|
8
|
+
export function visibleCanvases(doc) {
|
|
9
|
+
const width = doc.defaultView?.innerWidth ?? 0;
|
|
10
|
+
const height = doc.defaultView?.innerHeight ?? 0;
|
|
11
|
+
let count = 0;
|
|
12
|
+
for (const canvas of doc.querySelectorAll(CANVAS_SELECTOR)) {
|
|
13
|
+
if (isReticleUi(canvas)) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
const box = canvas.getBoundingClientRect();
|
|
17
|
+
const sized = box.width > 0 && box.height > 0;
|
|
18
|
+
const onScreen = box.bottom > 0 && box.right > 0 && box.top < height && box.left < width;
|
|
19
|
+
if (sized && onScreen) {
|
|
20
|
+
count += 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return count;
|
|
24
|
+
}
|
|
6
25
|
export function anchorOf(el) {
|
|
7
26
|
const testid = readTestId(el) ?? undefined;
|
|
8
27
|
const info = identifyComponent(el);
|
|
9
28
|
const out = {};
|
|
10
|
-
if (testid !== undefined)
|
|
29
|
+
if (testid !== undefined) {
|
|
11
30
|
out.testid = testid;
|
|
31
|
+
const shared = elementsMatching({ by: QueryBy.TESTID, value: testid }).length;
|
|
32
|
+
if (1 < shared) {
|
|
33
|
+
out.testidOf = shared;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
12
36
|
const component = info?.componentStack[0];
|
|
13
|
-
if (component !== undefined)
|
|
37
|
+
if (component !== undefined) {
|
|
14
38
|
out.component = component;
|
|
15
|
-
|
|
39
|
+
}
|
|
40
|
+
if (info?.source !== undefined) {
|
|
16
41
|
out.source = info.source;
|
|
42
|
+
}
|
|
17
43
|
const role = getRole(el);
|
|
18
44
|
const name = getAccessibleName(el);
|
|
19
|
-
if (role.length > 0)
|
|
45
|
+
if (role.length > 0) {
|
|
20
46
|
out.role = role;
|
|
21
|
-
|
|
47
|
+
}
|
|
48
|
+
if (name.length > 0) {
|
|
22
49
|
out.name = name;
|
|
50
|
+
}
|
|
51
|
+
if (isSensitiveField(el)) {
|
|
52
|
+
out.secret = true;
|
|
53
|
+
}
|
|
23
54
|
if (role.length > 0 && name.length > 0) {
|
|
24
55
|
const same = elementsMatching({ by: QueryBy.ROLE, value: role, name });
|
|
25
56
|
const at = same.indexOf(el);
|
|
@@ -40,21 +71,35 @@ export const result = (ref, action, effect, settled, settleReason, anchor, warni
|
|
|
40
71
|
settleReason,
|
|
41
72
|
effect,
|
|
42
73
|
};
|
|
43
|
-
if (anchor.testid !== undefined)
|
|
74
|
+
if (anchor.testid !== undefined) {
|
|
44
75
|
base.testid = anchor.testid;
|
|
45
|
-
|
|
76
|
+
}
|
|
77
|
+
if (anchor.testidOf !== undefined) {
|
|
78
|
+
base.testidOf = anchor.testidOf;
|
|
79
|
+
}
|
|
80
|
+
if (anchor.component !== undefined) {
|
|
46
81
|
base.component = anchor.component;
|
|
47
|
-
|
|
82
|
+
}
|
|
83
|
+
if (anchor.source !== undefined) {
|
|
48
84
|
base.source = anchor.source;
|
|
49
|
-
|
|
85
|
+
}
|
|
86
|
+
if (anchor.role !== undefined) {
|
|
50
87
|
base.role = anchor.role;
|
|
51
|
-
|
|
88
|
+
}
|
|
89
|
+
if (anchor.name !== undefined) {
|
|
52
90
|
base.name = anchor.name;
|
|
53
|
-
|
|
91
|
+
}
|
|
92
|
+
if (anchor.nth !== undefined) {
|
|
54
93
|
base.nth = anchor.nth;
|
|
55
|
-
|
|
94
|
+
}
|
|
95
|
+
if (anchor.of !== undefined) {
|
|
56
96
|
base.of = anchor.of;
|
|
57
|
-
|
|
97
|
+
}
|
|
98
|
+
if (anchor.secret !== undefined) {
|
|
99
|
+
base.secret = anchor.secret;
|
|
100
|
+
}
|
|
101
|
+
if (warning !== undefined) {
|
|
58
102
|
base.warning = warning;
|
|
103
|
+
}
|
|
59
104
|
return base;
|
|
60
105
|
};
|
|
@@ -3,6 +3,9 @@ interface ClickGeometry {
|
|
|
3
3
|
occluded: boolean;
|
|
4
4
|
occludedBy: string | null;
|
|
5
5
|
scrolledIntoView: boolean;
|
|
6
|
+
occluderName?: string;
|
|
7
|
+
noSize?: true;
|
|
8
|
+
offViewport?: true;
|
|
6
9
|
}
|
|
7
10
|
export declare const NO_GEOMETRY: ClickGeometry;
|
|
8
11
|
export declare function mouseEventFor(el: Element, type: string, init?: MouseEventInit): MouseEvent;
|
|
@@ -14,6 +17,7 @@ export declare function fireClickSequence(el: ActionTarget, hold?: {
|
|
|
14
17
|
prevented: boolean;
|
|
15
18
|
heldMs: number;
|
|
16
19
|
}>;
|
|
20
|
+
export declare function isOffViewport(el: Element, rect: DOMRect): boolean;
|
|
17
21
|
export declare function clickGeometry(el: ActionTarget): ClickGeometry;
|
|
18
22
|
export declare function firePointer(el: Element, type: string, relatedTarget?: EventTarget | null): void;
|
|
19
23
|
export declare function firePointerNonBubbling(el: Element, type: string, relatedTarget?: EventTarget | null): void;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { refs } from '../dom/addressing/refs.js';
|
|
2
|
+
import { readTestId } from '../dom/addressing/testid-attr.js';
|
|
3
|
+
import { getAccessibleName, getRole } from '../dom/a11y.js';
|
|
2
4
|
import { hitTestOccluder } from '../dom/occlusion.js';
|
|
3
5
|
import { nativeFrame } from '../timers/native/native-timers.js';
|
|
4
6
|
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
@@ -58,7 +60,7 @@ export async function fireClickSequence(el, hold, detail) {
|
|
|
58
60
|
function isMeasurable(rect) {
|
|
59
61
|
return rect.width > 0 || rect.height > 0;
|
|
60
62
|
}
|
|
61
|
-
function isOffViewport(el, rect) {
|
|
63
|
+
export function isOffViewport(el, rect) {
|
|
62
64
|
const win = el.ownerDocument.defaultView;
|
|
63
65
|
if (null === win)
|
|
64
66
|
return false;
|
|
@@ -70,21 +72,35 @@ function hitTest(el, rect) {
|
|
|
70
72
|
const top = hitTestOccluder(el, rect);
|
|
71
73
|
return null === top
|
|
72
74
|
? { occluded: false, occludedBy: null }
|
|
73
|
-
: { occluded: true, occludedBy: refs.refFor(top) };
|
|
75
|
+
: { occluded: true, occludedBy: refs.refFor(top), occluderName: nameOf(top) };
|
|
76
|
+
}
|
|
77
|
+
function nameOf(el) {
|
|
78
|
+
const testid = readTestId(el);
|
|
79
|
+
if (testid !== null && testid.length > 0)
|
|
80
|
+
return `[data-testid="${testid}"]`;
|
|
81
|
+
const name = getAccessibleName(el).trim();
|
|
82
|
+
return name.length > 0 ? `${getRole(el)} "${name.slice(0, 40)}"` : el.tagName.toLowerCase();
|
|
83
|
+
}
|
|
84
|
+
function hasNoSize(el) {
|
|
85
|
+
if (typeof el.getClientRects !== 'function')
|
|
86
|
+
return false;
|
|
87
|
+
const rects = Array.from(el.getClientRects());
|
|
88
|
+
return rects.length > 0 && rects.every((r) => 0 === r.width || 0 === r.height);
|
|
74
89
|
}
|
|
75
90
|
export function clickGeometry(el) {
|
|
76
91
|
if (typeof el.getBoundingClientRect !== 'function')
|
|
77
92
|
return NO_GEOMETRY;
|
|
78
93
|
let rect = el.getBoundingClientRect();
|
|
79
94
|
if (!isMeasurable(rect))
|
|
80
|
-
return NO_GEOMETRY;
|
|
95
|
+
return hasNoSize(el) ? { ...NO_GEOMETRY, noSize: true } : NO_GEOMETRY;
|
|
81
96
|
let scrolledIntoView = false;
|
|
82
97
|
if (isOffViewport(el, rect) && 'function' === typeof el.scrollIntoView) {
|
|
83
98
|
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
84
99
|
scrolledIntoView = true;
|
|
85
100
|
rect = el.getBoundingClientRect();
|
|
86
101
|
}
|
|
87
|
-
|
|
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 })));
|
package/dist/actions/actions.js
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
import { ActionType, ActionWarning, clampHoldMs, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
|
|
2
|
+
import { isHiddenNativeOption } from '../dom/query-finders.js';
|
|
3
|
+
import { isReticleUi } from '../dom/dom-ignore.js';
|
|
2
4
|
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
3
5
|
import { echoRef, refs } from '../dom/addressing/refs.js';
|
|
4
|
-
import {
|
|
6
|
+
import { requiresDangerousConfirmation, assertOutwardAllowed, assertPaymentEntryAllowed, submitControlFor, } from './danger-context.js';
|
|
5
7
|
import { assertEditable, assertNotRichText, isAutofillSafeTarget, setNativeValue, } from './value-input.js';
|
|
6
|
-
import {
|
|
8
|
+
import { getStates } from '../dom/a11y.js';
|
|
9
|
+
import { isVisible } from '../dom/visibility.js';
|
|
7
10
|
import { elementHasHoverHandlers } from '../registry/stores/adapters.js';
|
|
8
11
|
import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/realm.js';
|
|
9
|
-
import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
|
|
12
|
+
import { asSettleBudget, nativeSetTimeout, settle } from '../timers/native/native-timers.js';
|
|
13
|
+
import { publishSettledField, watchActedField } from '../observers/field.js';
|
|
10
14
|
import { AppearedText } from './appeared-text.js';
|
|
11
15
|
import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
|
|
12
16
|
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
|
|
13
|
-
import { anchorOf, result } from './action-report.js';
|
|
17
|
+
import { anchorOf, result, visibleCanvases, } from './action-report.js';
|
|
14
18
|
function asString(value, fallback = '') {
|
|
15
19
|
return 'string' === typeof value ? value : fallback;
|
|
16
20
|
}
|
|
17
21
|
function requireElement(ref) {
|
|
18
22
|
const el = refs.resolve(ref);
|
|
19
|
-
if (null === el)
|
|
23
|
+
if (null === el) {
|
|
20
24
|
throw new Error(`ref '${echoRef(ref)}' no longer resolves to an element`);
|
|
21
|
-
|
|
25
|
+
}
|
|
26
|
+
if (!isActionTarget(el)) {
|
|
22
27
|
throw new Error(`ref '${echoRef(ref)}' is not an HTML or SVG element`);
|
|
28
|
+
}
|
|
23
29
|
return el;
|
|
24
30
|
}
|
|
25
31
|
const FILL_LIKE = new Set([
|
|
@@ -48,8 +54,9 @@ const GENERIC_ACTION_ARG_KEYS = new Set([
|
|
|
48
54
|
function assertUploadArgs(args) {
|
|
49
55
|
const keys = Object.keys(args);
|
|
50
56
|
const dropped = keys.filter((k) => !UPLOAD_ARG_KEYS.has(k) && !GENERIC_ACTION_ARG_KEYS.has(k));
|
|
51
|
-
if (0 === dropped.length && keys.some((k) => UPLOAD_ARG_KEYS.has(k)))
|
|
57
|
+
if (0 === dropped.length && keys.some((k) => UPLOAD_ARG_KEYS.has(k))) {
|
|
52
58
|
return;
|
|
59
|
+
}
|
|
53
60
|
const detail = 0 === dropped.length
|
|
54
61
|
? 'no file was described'
|
|
55
62
|
: `upload does not read ${dropped.join(', ')}, so it would be dropped`;
|
|
@@ -67,27 +74,34 @@ function assertActionAllowed(el, action, args) {
|
|
|
67
74
|
(action === ActionType.PRESS && 'Enter' === pressKey(args));
|
|
68
75
|
const dragTarget = action === ActionType.DRAG ? refs.resolve(dragTargetRef(args)) : null;
|
|
69
76
|
const submitter = action === ActionType.PRESS && 'Enter' === pressKey(args) ? submitControlFor(el) : null;
|
|
70
|
-
if (canTrigger)
|
|
77
|
+
if (canTrigger) {
|
|
71
78
|
assertOutwardAllowed([el, submitter, isActionTarget(dragTarget) ? dragTarget : null]);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
}
|
|
80
|
+
if (action === ActionType.FILL || action === ActionType.TYPE) {
|
|
81
|
+
assertPaymentEntryAllowed(el);
|
|
82
|
+
}
|
|
83
|
+
const sourceDangerous = requiresDangerousConfirmation(el) ||
|
|
84
|
+
(submitter !== null && requiresDangerousConfirmation(submitter));
|
|
85
|
+
const targetDangerous = isActionTarget(dragTarget) && requiresDangerousConfirmation(dragTarget);
|
|
77
86
|
if (canTrigger &&
|
|
78
87
|
(sourceDangerous || targetDangerous) &&
|
|
79
88
|
args[DANGEROUS_ACTION_CONFIRM_ARG] !== true) {
|
|
80
89
|
throw new Error(`potentially destructive action blocked; retry with args.${DANGEROUS_ACTION_CONFIRM_ARG}=true`);
|
|
81
90
|
}
|
|
91
|
+
if (CLICK_LIKE.has(action) && el instanceof HTMLElement && isHiddenNativeOption(el)) {
|
|
92
|
+
throw new Error('cannot click a native <option> inside a visually-hidden <select> — open the dropdown and target the visible option');
|
|
93
|
+
}
|
|
82
94
|
}
|
|
83
95
|
function optionValueFor(options, named) {
|
|
84
|
-
if (options.some((option) => option.value === named))
|
|
96
|
+
if (options.some((option) => option.value === named)) {
|
|
85
97
|
return named;
|
|
98
|
+
}
|
|
86
99
|
const byLabel = options.filter((option) => option.label.trim() === named.trim());
|
|
87
100
|
return 1 === byLabel.length ? (byLabel[0]?.value ?? named) : named;
|
|
88
101
|
}
|
|
89
102
|
import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, } from './actions-dom.js';
|
|
90
103
|
import { scrollFrom } from './scroll.js';
|
|
104
|
+
import { clickHitsNoControl } from './click-target.js';
|
|
91
105
|
function enabledOf(el) {
|
|
92
106
|
return !getStates(el).includes(ElementState.DISABLED);
|
|
93
107
|
}
|
|
@@ -99,8 +113,9 @@ function valueOf(el) {
|
|
|
99
113
|
}
|
|
100
114
|
function activeRef(el) {
|
|
101
115
|
const active = el.ownerDocument.activeElement;
|
|
102
|
-
if (null === active || active === el.ownerDocument.body)
|
|
116
|
+
if (null === active || active === el.ownerDocument.body) {
|
|
103
117
|
return null;
|
|
118
|
+
}
|
|
104
119
|
return refs.refFor(active);
|
|
105
120
|
}
|
|
106
121
|
async function dispatchFor(el, action, args) {
|
|
@@ -136,8 +151,9 @@ async function dispatchOther(el, action, args) {
|
|
|
136
151
|
firePointer(el, 'pointermove', from);
|
|
137
152
|
const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true }));
|
|
138
153
|
const holdMs = clampHoldMs(args['holdMs']);
|
|
139
|
-
if (holdMs > 0)
|
|
154
|
+
if (holdMs > 0) {
|
|
140
155
|
await sleep(holdMs);
|
|
156
|
+
}
|
|
141
157
|
return !moved;
|
|
142
158
|
}
|
|
143
159
|
case ActionType.FOCUS:
|
|
@@ -161,8 +177,9 @@ async function dispatchOther(el, action, args) {
|
|
|
161
177
|
'To empty a field on purpose use the `clear` action, or fill with an explicit "".');
|
|
162
178
|
}
|
|
163
179
|
assertEditable(el, 'fill');
|
|
164
|
-
if (!isAutofillSafeTarget(el))
|
|
180
|
+
if (!isAutofillSafeTarget(el)) {
|
|
165
181
|
el.focus();
|
|
182
|
+
}
|
|
166
183
|
return setNativeValue(el, args['value']);
|
|
167
184
|
}
|
|
168
185
|
assertNotRichText(el, 'fill');
|
|
@@ -214,8 +231,9 @@ async function dispatchOther(el, action, args) {
|
|
|
214
231
|
if ('radio' === el.type && action === ActionType.UNCHECK) {
|
|
215
232
|
throw new Error('cannot uncheck a radio button — a real user could not; select another radio in the group');
|
|
216
233
|
}
|
|
217
|
-
if (alreadyAtCheckedState(el, action))
|
|
234
|
+
if (alreadyAtCheckedState(el, action)) {
|
|
218
235
|
return false;
|
|
236
|
+
}
|
|
219
237
|
const event = mouseEventFor(el, 'click', {
|
|
220
238
|
bubbles: true,
|
|
221
239
|
cancelable: true,
|
|
@@ -226,25 +244,29 @@ async function dispatchOther(el, action, args) {
|
|
|
226
244
|
}
|
|
227
245
|
case ActionType.SUBMIT: {
|
|
228
246
|
const form = isForm(el) ? el : el.closest('form');
|
|
229
|
-
if (null === form)
|
|
247
|
+
if (null === form) {
|
|
230
248
|
throw new Error('no form to submit');
|
|
249
|
+
}
|
|
231
250
|
form.requestSubmit();
|
|
232
251
|
return false;
|
|
233
252
|
}
|
|
234
253
|
case ActionType.PRESS: {
|
|
235
254
|
const mods = pressModifiers(args);
|
|
236
255
|
const combo = pressKeys(args);
|
|
237
|
-
if (combo.length > 0)
|
|
256
|
+
if (combo.length > 0) {
|
|
238
257
|
return await pressCombo(el, combo, mods, clampHoldMs(args['holdMs']));
|
|
258
|
+
}
|
|
239
259
|
const key = pressKey(args);
|
|
240
260
|
const code = pressCode(args, key);
|
|
241
261
|
const down = asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keydown', { key, code, bubbles: true, cancelable: true, ...mods })));
|
|
242
262
|
closeModalOnEscape(el, key, down);
|
|
243
|
-
if (down)
|
|
263
|
+
if (down) {
|
|
244
264
|
dispatchKeypress(el, key, code, mods);
|
|
265
|
+
}
|
|
245
266
|
const hold = clampHoldMs(args['holdMs']);
|
|
246
|
-
if (hold > 0)
|
|
267
|
+
if (hold > 0) {
|
|
247
268
|
await holdKey(el, key, code, mods, hold);
|
|
269
|
+
}
|
|
248
270
|
asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
|
|
249
271
|
return !down;
|
|
250
272
|
}
|
|
@@ -270,8 +292,9 @@ async function dispatchOther(el, action, args) {
|
|
|
270
292
|
if (true === args['__base64']) {
|
|
271
293
|
const bin = atob(rawContent);
|
|
272
294
|
const arr = new Uint8Array(bin.length);
|
|
273
|
-
for (let i = 0; i < bin.length; i++)
|
|
295
|
+
for (let i = 0; i < bin.length; i++) {
|
|
274
296
|
arr[i] = bin.charCodeAt(i);
|
|
297
|
+
}
|
|
275
298
|
fileBody = arr.buffer.slice(0);
|
|
276
299
|
}
|
|
277
300
|
const file = new File([fileBody], asString(args['name'], 'file.txt'), {
|
|
@@ -286,8 +309,9 @@ async function dispatchOther(el, action, args) {
|
|
|
286
309
|
}
|
|
287
310
|
case ActionType.DRAG: {
|
|
288
311
|
const toRef = dragTargetRef(args);
|
|
289
|
-
if ('' === toRef)
|
|
312
|
+
if ('' === toRef) {
|
|
290
313
|
return await dragElement(el, null, args['data']);
|
|
314
|
+
}
|
|
291
315
|
const resolved = refs.resolve(toRef);
|
|
292
316
|
if (!isActionTarget(resolved)) {
|
|
293
317
|
throw new Error(`drag target '${echoRef(toRef)}' did not resolve to an element — pass a ref from ` +
|
|
@@ -299,26 +323,24 @@ async function dispatchOther(el, action, args) {
|
|
|
299
323
|
throw new Error(`unknown action '${action}'`);
|
|
300
324
|
}
|
|
301
325
|
}
|
|
302
|
-
function asSettleBudget(raw) {
|
|
303
|
-
if ('number' !== typeof raw || !Number.isFinite(raw))
|
|
304
|
-
return undefined;
|
|
305
|
-
return Math.max(0, Math.min(raw, FRAME_BUDGET_MS_CEILING));
|
|
306
|
-
}
|
|
307
|
-
const FRAME_BUDGET_MS_CEILING = 200;
|
|
308
326
|
export async function executeAction(ref, action, args = {}) {
|
|
309
327
|
const named = 0 < ref.length;
|
|
310
328
|
const el = isReflessDocumentPress(ref, action, args) ? focusedOrDocument() : requireElement(ref);
|
|
311
|
-
if (named)
|
|
329
|
+
if (named) {
|
|
312
330
|
assertActionAllowed(el, action, args);
|
|
313
|
-
|
|
331
|
+
}
|
|
332
|
+
if (named) {
|
|
314
333
|
assertNotLeaving(el);
|
|
334
|
+
}
|
|
315
335
|
const anchor = named ? anchorOf(el) : {};
|
|
316
336
|
const visible = isVisible(el);
|
|
317
337
|
const enabled = enabledOf(el);
|
|
318
338
|
const prevFocus = activeRef(el);
|
|
319
339
|
const valueBefore = valueOf(el);
|
|
340
|
+
const actedField = watchActedField(el);
|
|
320
341
|
const geometry = CLICK_LIKE.has(action) ? clickGeometry(el) : NO_GEOMETRY;
|
|
321
342
|
const alreadyAtValue = alreadyAtCheckedState(el, action);
|
|
343
|
+
const hitsNoControl = clickHitsNoControl(el, action);
|
|
322
344
|
let mutated = 0;
|
|
323
345
|
const said = new AppearedText();
|
|
324
346
|
const obs = new MutationObserver((records) => {
|
|
@@ -346,9 +368,11 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
346
368
|
settled = outcome.settled;
|
|
347
369
|
settleReason = outcome.settled ? null : SettleReason.TIMEOUT;
|
|
348
370
|
obs.disconnect();
|
|
371
|
+
actedField?.stop();
|
|
349
372
|
}
|
|
350
373
|
assertAriaToggleLanded(el, action, alreadyAtValue);
|
|
351
374
|
const valueAfter = valueOf(el);
|
|
375
|
+
publishSettledField(el, actedField);
|
|
352
376
|
const nextFocus = activeRef(el);
|
|
353
377
|
const effect = {
|
|
354
378
|
dispatched: true,
|
|
@@ -364,17 +388,32 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
364
388
|
occluded: geometry.occluded,
|
|
365
389
|
occludedBy: geometry.occludedBy,
|
|
366
390
|
scrolledIntoView: geometry.scrolledIntoView,
|
|
391
|
+
...(geometry.occluderName === undefined ? {} : { occluderName: geometry.occluderName }),
|
|
392
|
+
...(true === geometry.noSize ? { noSize: true } : {}),
|
|
393
|
+
...(true === geometry.offViewport ? { offViewport: true } : {}),
|
|
367
394
|
...(heldMs > 0 ? { heldMs } : {}),
|
|
368
395
|
...(alreadyAtValue ? { alreadyAtValue: true } : {}),
|
|
396
|
+
...canvasesOnScreen(),
|
|
369
397
|
};
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
398
|
+
let warning;
|
|
399
|
+
if (!named) {
|
|
400
|
+
warning = ActionWarning.GLOBAL_PRESS;
|
|
401
|
+
}
|
|
402
|
+
else if (geometry.occluded) {
|
|
403
|
+
warning = ActionWarning.CLICK_OCCLUDED;
|
|
404
|
+
}
|
|
405
|
+
else if (action === ActionType.HOVER && elementHasHoverHandlers(el)) {
|
|
406
|
+
warning = ActionWarning.HOVER_NATIVE_ENTER_LEAVE;
|
|
407
|
+
}
|
|
408
|
+
else if (hitsNoControl) {
|
|
409
|
+
warning = ActionWarning.CLICK_NOT_A_CONTROL;
|
|
410
|
+
}
|
|
411
|
+
const done = result(ref, action, effect, settled, settleReason, anchor, warning);
|
|
412
|
+
return isReticleUi(el) ? { ...done, reticleUi: true } : done;
|
|
413
|
+
}
|
|
414
|
+
function canvasesOnScreen() {
|
|
415
|
+
const canvases = visibleCanvases(document);
|
|
416
|
+
return canvases > 0 ? { canvases } : {};
|
|
378
417
|
}
|
|
379
418
|
const sleep = (ms) => new Promise((r) => nativeSetTimeout(r, ms));
|
|
380
419
|
export async function executeSequence(steps) {
|
|
@@ -390,18 +429,27 @@ export async function executeSequence(steps) {
|
|
|
390
429
|
settled: res.settled,
|
|
391
430
|
settleReason: res.settleReason,
|
|
392
431
|
};
|
|
393
|
-
if (res.testid !== undefined)
|
|
432
|
+
if (res.testid !== undefined) {
|
|
394
433
|
stepBase.testid = res.testid;
|
|
395
|
-
|
|
434
|
+
}
|
|
435
|
+
if (res.component !== undefined) {
|
|
396
436
|
stepBase.component = res.component;
|
|
397
|
-
|
|
437
|
+
}
|
|
438
|
+
if (res.role !== undefined) {
|
|
398
439
|
stepBase.role = res.role;
|
|
399
|
-
|
|
440
|
+
}
|
|
441
|
+
if (res.name !== undefined) {
|
|
400
442
|
stepBase.name = res.name;
|
|
401
|
-
|
|
443
|
+
}
|
|
444
|
+
if (res.source !== undefined) {
|
|
402
445
|
stepBase.source = res.source;
|
|
403
|
-
|
|
446
|
+
}
|
|
447
|
+
if (res.secret !== undefined) {
|
|
448
|
+
stepBase.secret = res.secret;
|
|
449
|
+
}
|
|
450
|
+
if (res.warning !== undefined) {
|
|
404
451
|
stepBase.warning = res.warning;
|
|
452
|
+
}
|
|
405
453
|
stepResults.push(stepBase);
|
|
406
454
|
}
|
|
407
455
|
return { ok: true, count: steps.length, effects, steps: stepResults };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isWordsIgnored } from '../dom/dom-ignore.js';
|
|
2
2
|
import { isSensitiveField } from '../dom/a11y.js';
|
|
3
3
|
const APPEARED_MAX = 200;
|
|
4
4
|
const JOIN = ' | ';
|
|
@@ -45,7 +45,7 @@ export class AppearedText {
|
|
|
45
45
|
return this.#length > APPEARED_MAX;
|
|
46
46
|
}
|
|
47
47
|
#add(raw, owner) {
|
|
48
|
-
if (null !== owner &&
|
|
48
|
+
if (null !== owner && isWordsIgnored(owner))
|
|
49
49
|
return;
|
|
50
50
|
if (null !== owner && isSensitiveField(owner))
|
|
51
51
|
return;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function clickHitsNoControl(el: Element, action: string): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ActionType } from '@reticlehq/core';
|
|
2
|
+
import { getRole } from '../dom/a11y.js';
|
|
3
|
+
import { parentAcrossShadowBoundary } from '../dom/shadow-registry.js';
|
|
4
|
+
import { INTERACTIVE } from '../dom/snapshot.js';
|
|
5
|
+
const CLICK_FORWARDING = 'label, summary, [onclick], [tabindex], [contenteditable]:not([contenteditable="false"])';
|
|
6
|
+
export function clickHitsNoControl(el, action) {
|
|
7
|
+
if (ActionType.CLICK !== action && ActionType.DBLCLICK !== action)
|
|
8
|
+
return false;
|
|
9
|
+
for (let node = el; node !== null; node = parentAcrossShadowBoundary(node)) {
|
|
10
|
+
if (INTERACTIVE.has(getRole(node)) || node.matches(CLICK_FORWARDING))
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return 'pointer' !== getComputedStyle(el).cursor;
|
|
14
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ActionTarget } from '../dom/realm.js';
|
|
2
2
|
export declare function dangerousActionContext(el: ActionTarget): string;
|
|
3
3
|
export declare function submitControlFor(el: ActionTarget): HTMLElement | null;
|
|
4
|
-
export declare function requiresDangerousConfirmation(
|
|
4
|
+
export declare function requiresDangerousConfirmation(el: ActionTarget): boolean;
|
|
5
5
|
export declare function setOutwardPolicy(allowed: readonly string[] | undefined): void;
|
|
6
6
|
export declare function assertOutwardAllowed(targets: readonly (ActionTarget | null)[]): void;
|
|
7
|
+
export declare function assertPaymentEntryAllowed(el: ActionTarget): void;
|