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