@reticlehq/browser 3.2.0 → 3.4.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/actions-dom.d.ts +6 -5
- package/dist/actions/actions-dom.js +5 -4
- package/dist/actions/actions-press.d.ts +3 -3
- package/dist/actions/actions-press.js +0 -3
- package/dist/actions/actions.d.ts +0 -1
- package/dist/actions/actions.js +17 -13
- package/dist/actions/check-state.d.ts +5 -0
- package/dist/actions/check-state.js +24 -0
- package/dist/actions/danger-context.d.ts +3 -2
- package/dist/actions/danger-context.js +2 -1
- package/dist/actions/value-input.d.ts +2 -1
- package/dist/actions/value-input.js +15 -6
- package/dist/connect-options.d.ts +1 -0
- package/dist/dom/a11y.js +43 -3
- package/dist/dom/addressing/source.d.ts +2 -1
- package/dist/dom/dom-ignore.d.ts +1 -0
- package/dist/dom/dom-ignore.js +13 -0
- package/dist/dom/query.js +6 -4
- package/dist/dom/realm.d.ts +6 -0
- package/dist/dom/realm.js +5 -0
- package/dist/dom/snapshot.d.ts +1 -1
- package/dist/dom/snapshot.js +8 -2
- package/dist/observers/console.js +38 -13
- package/dist/observers/dom.js +3 -1
- package/dist/observers/download.js +3 -2
- package/dist/observers/error-repeats.d.ts +18 -0
- package/dist/observers/error-repeats.js +41 -0
- package/dist/observers/field.d.ts +2 -0
- package/dist/observers/field.js +86 -0
- package/dist/observers/frames.js +3 -4
- package/dist/observers/install-all.js +2 -0
- package/dist/observers/net-detail/network-body.d.ts +2 -0
- package/dist/observers/net-detail/network-body.js +24 -7
- package/dist/observers/network.js +14 -1
- package/dist/observers/route.js +4 -4
- package/dist/observers/state.js +21 -8
- package/dist/presenter/carousel/carousel.d.ts +13 -0
- package/dist/presenter/carousel/carousel.js +126 -0
- package/dist/presenter/carousel/offer-card.d.ts +21 -0
- package/dist/presenter/carousel/offer-card.js +55 -0
- package/dist/presenter/carousel/panel-slides.d.ts +7 -0
- package/dist/presenter/carousel/panel-slides.js +10 -0
- package/dist/presenter/carousel/talk-card.d.ts +8 -0
- package/dist/presenter/carousel/talk-card.js +20 -0
- package/dist/presenter/chrome/presenter-log.d.ts +2 -1
- package/dist/presenter/chrome/presenter-log.js +14 -3
- package/dist/presenter/chrome/presenter-report-copy.d.ts +0 -1
- package/dist/presenter/chrome/presenter-report-copy.js +1 -1
- package/dist/presenter/hud-telemetry.d.ts +3 -0
- package/dist/presenter/hud-telemetry.js +87 -0
- package/dist/presenter/presenter-account.js +1 -1
- package/dist/presenter/presenter-config.d.ts +5 -1
- package/dist/presenter/presenter-config.js +4 -1
- package/dist/presenter/presenter-controls.d.ts +1 -0
- package/dist/presenter/presenter-controls.js +12 -1
- package/dist/presenter/presenter-report.js +6 -6
- package/dist/presenter/presenter-settings-styles.js +2 -1
- package/dist/presenter/presenter-settings.d.ts +2 -4
- package/dist/presenter/presenter-settings.js +13 -4
- package/dist/presenter/presenter-shell-styles.js +4 -0
- package/dist/presenter/presenter-shell.d.ts +4 -1
- package/dist/presenter/presenter-shell.js +49 -11
- package/dist/presenter/presenter-styles.js +5 -1
- package/dist/presenter/presenter-workspace.d.ts +0 -7
- package/dist/presenter/presenter-workspace.js +7 -7
- package/dist/presenter/presenter.js +43 -10
- package/dist/presenter/tour/tour.d.ts +3 -0
- package/dist/presenter/tour/tour.js +5 -0
- package/dist/recorder/recorder.js +25 -22
- package/dist/registry/store-adapters.js +32 -2
- package/dist/registry/stores/adapters.js +8 -2
- package/dist/reticle-inject.d.ts +2 -0
- package/dist/reticle-inject.js +1337 -0
- package/dist/reticle.js +6 -0
- package/dist/review/annotator.d.ts +3 -1
- package/dist/review/annotator.js +37 -40
- package/dist/review/marks-for-agent.d.ts +9 -0
- package/dist/review/marks-for-agent.js +12 -0
- package/dist/util/captured-method.d.ts +0 -1
- package/dist/util/captured-method.js +1 -1
- package/package.json +15 -11
- package/dist/presenter/presenter-offer.d.ts +0 -25
- package/dist/presenter/presenter-offer.js +0 -87
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ActionTarget } from '../dom/realm.js';
|
|
1
2
|
interface ClickGeometry {
|
|
2
3
|
occluded: boolean;
|
|
3
4
|
occludedBy: string | null;
|
|
@@ -5,19 +6,19 @@ interface ClickGeometry {
|
|
|
5
6
|
}
|
|
6
7
|
export declare const NO_GEOMETRY: ClickGeometry;
|
|
7
8
|
export declare function mouseEventFor(el: Element, type: string, init?: MouseEventInit): MouseEvent;
|
|
8
|
-
export declare function fireClickSequence(el:
|
|
9
|
+
export declare function fireClickSequence(el: ActionTarget, hold?: {
|
|
9
10
|
ms: number;
|
|
10
11
|
sleep: (ms: number) => Promise<void>;
|
|
11
12
|
now: () => number;
|
|
12
|
-
}): Promise<{
|
|
13
|
+
}, detail?: number): Promise<{
|
|
13
14
|
prevented: boolean;
|
|
14
15
|
heldMs: number;
|
|
15
16
|
}>;
|
|
16
|
-
export declare function clickGeometry(el:
|
|
17
|
+
export declare function clickGeometry(el: ActionTarget): ClickGeometry;
|
|
17
18
|
export declare function firePointer(el: Element, type: string, relatedTarget?: EventTarget | null): void;
|
|
18
19
|
export declare function firePointerNonBubbling(el: Element, type: string, relatedTarget?: EventTarget | null): void;
|
|
19
|
-
export declare function dragElement(source:
|
|
20
|
-
export declare function fireTapSequence(el:
|
|
20
|
+
export declare function dragElement(source: ActionTarget, target: ActionTarget | null, data: unknown): Promise<boolean>;
|
|
21
|
+
export declare function fireTapSequence(el: ActionTarget, hold: {
|
|
21
22
|
ms: number;
|
|
22
23
|
sleep: (ms: number) => Promise<void>;
|
|
23
24
|
now: () => number;
|
|
@@ -16,11 +16,12 @@ function pointerEventFor(el, type, init) {
|
|
|
16
16
|
? new PointerEvent(type, full)
|
|
17
17
|
: new MouseEvent(type, full);
|
|
18
18
|
}
|
|
19
|
-
export async function fireClickSequence(el, hold) {
|
|
19
|
+
export async function fireClickSequence(el, hold, detail) {
|
|
20
20
|
const doc = el.ownerDocument;
|
|
21
21
|
const from = doc.activeElement ?? doc.body;
|
|
22
|
+
const init = { bubbles: true, cancelable: true, detail: detail ?? 0 };
|
|
22
23
|
firePointer(el, 'pointerdown', from);
|
|
23
|
-
asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mousedown',
|
|
24
|
+
asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mousedown', init)));
|
|
24
25
|
if (el.tabIndex >= 0 && 'function' === typeof el.focus)
|
|
25
26
|
el.focus();
|
|
26
27
|
let heldMs = 0;
|
|
@@ -30,8 +31,8 @@ export async function fireClickSequence(el, hold) {
|
|
|
30
31
|
heldMs = hold.now() - startedAt;
|
|
31
32
|
}
|
|
32
33
|
firePointer(el, 'pointerup', from);
|
|
33
|
-
asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mouseup',
|
|
34
|
-
const notPrevented = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click',
|
|
34
|
+
asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'mouseup', init)));
|
|
35
|
+
const notPrevented = asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'click', init)));
|
|
35
36
|
return { prevented: !notPrevented, heldMs };
|
|
36
37
|
}
|
|
37
38
|
function isMeasurable(rect) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ActionTarget } from '../dom/realm.js';
|
|
1
2
|
export declare function pressKey(args: Record<string, unknown>): string;
|
|
2
3
|
export declare function pressModifiers(args: Record<string, unknown>): {
|
|
3
4
|
metaKey: boolean;
|
|
@@ -9,6 +10,5 @@ export declare function pressCode(args: Record<string, unknown>, key: string): s
|
|
|
9
10
|
export declare function focusedOrDocument(): HTMLElement;
|
|
10
11
|
export declare function isReflessDocumentPress(ref: string, action: string, args: Record<string, unknown>): boolean;
|
|
11
12
|
export declare function pressKeys(args: Record<string, unknown>): string[];
|
|
12
|
-
export declare function holdKey(el:
|
|
13
|
-
export declare function pressCombo(el:
|
|
14
|
-
export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
|
|
13
|
+
export declare function holdKey(el: ActionTarget, key: string, code: string, mods: Record<string, boolean>, ms: number): Promise<void>;
|
|
14
|
+
export declare function pressCombo(el: ActionTarget, keys: readonly string[], mods: Record<string, boolean>, holdMs: number): Promise<boolean>;
|
|
@@ -118,6 +118,3 @@ export async function pressCombo(el, keys, mods, holdMs) {
|
|
|
118
118
|
}
|
|
119
119
|
return prevented;
|
|
120
120
|
}
|
|
121
|
-
export const ZOOM_NEEDS_REAL_BROWSER_MSG = 'cannot zoom from inside the page — CSS zoom changes how it LOOKS without changing the layout ' +
|
|
122
|
-
'viewport, visualViewport or media queries, so a layout that breaks at 200% would be reported as ' +
|
|
123
|
-
'checked and passing. Drive a real browser (reticle drive <url>, or reticle_lease) and zoom there.';
|
|
@@ -34,7 +34,6 @@ interface ActionResult {
|
|
|
34
34
|
};
|
|
35
35
|
warning?: string;
|
|
36
36
|
}
|
|
37
|
-
export declare const MAX_HOLD_MS = 30000;
|
|
38
37
|
export declare const ZOOM_NEEDS_REAL_BROWSER_MSG: string;
|
|
39
38
|
export declare function executeAction(ref: string, action: string, args?: Record<string, unknown>): Promise<ActionResult>;
|
|
40
39
|
export declare function dispatchWebMcp(tool: string, params: Record<string, unknown>, confirmDangerous?: boolean): Promise<unknown>;
|
package/dist/actions/actions.js
CHANGED
|
@@ -5,9 +5,10 @@ import { dangerousActionContext, requiresDangerousConfirmation, submitControlFor
|
|
|
5
5
|
import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
|
|
6
6
|
import { getAccessibleName, getRole, isVisible, getStates } from '../dom/a11y.js';
|
|
7
7
|
import { elementHasHoverHandlers, identifyComponent } from '../registry/stores/adapters.js';
|
|
8
|
-
import {
|
|
8
|
+
import { isActionTarget, isForm, isInput, isSelect, isTextArea, } from '../dom/realm.js';
|
|
9
9
|
import { nativeSetTimeout, settle } from '../timers/native/native-timers.js';
|
|
10
10
|
import { AppearedText } from './appeared-text.js';
|
|
11
|
+
import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
|
|
11
12
|
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, } from './actions-press.js';
|
|
12
13
|
function asString(value, fallback = '') {
|
|
13
14
|
return 'string' === typeof value ? value : fallback;
|
|
@@ -16,8 +17,8 @@ function requireElement(ref) {
|
|
|
16
17
|
const el = refs.resolve(ref);
|
|
17
18
|
if (null === el)
|
|
18
19
|
throw new Error(`ref '${echoRef(ref)}' no longer resolves to an element`);
|
|
19
|
-
if (!
|
|
20
|
-
throw new Error(`ref '${echoRef(ref)}' is not an
|
|
20
|
+
if (!isActionTarget(el))
|
|
21
|
+
throw new Error(`ref '${echoRef(ref)}' is not an HTML or SVG element`);
|
|
21
22
|
return el;
|
|
22
23
|
}
|
|
23
24
|
function anchorOf(el) {
|
|
@@ -76,11 +77,6 @@ const CLICK_LIKE = new Set([
|
|
|
76
77
|
ActionType.CHECK,
|
|
77
78
|
ActionType.UNCHECK,
|
|
78
79
|
]);
|
|
79
|
-
function alreadyAtCheckedState(el, action) {
|
|
80
|
-
if (action !== ActionType.CHECK && action !== ActionType.UNCHECK)
|
|
81
|
-
return false;
|
|
82
|
-
return isInput(el) && el.checked === (action === ActionType.CHECK);
|
|
83
|
-
}
|
|
84
80
|
function dragTargetRef(args) {
|
|
85
81
|
const named = asString(args['toRef']);
|
|
86
82
|
return '' !== named ? named : asString(args['target']);
|
|
@@ -116,7 +112,7 @@ function assertActionAllowed(el, action, args) {
|
|
|
116
112
|
const sourceDangerous = requiresDangerousConfirmation(dangerousActionContext(el), getRole(el)) ||
|
|
117
113
|
(submitter !== null &&
|
|
118
114
|
requiresDangerousConfirmation(dangerousActionContext(submitter), getRole(submitter)));
|
|
119
|
-
const targetDangerous =
|
|
115
|
+
const targetDangerous = isActionTarget(dragTarget) &&
|
|
120
116
|
requiresDangerousConfirmation(dangerousActionContext(dragTarget), getRole(dragTarget));
|
|
121
117
|
if (canTrigger &&
|
|
122
118
|
(sourceDangerous || targetDangerous) &&
|
|
@@ -141,7 +137,7 @@ function activeRef(el) {
|
|
|
141
137
|
return null;
|
|
142
138
|
return refs.refFor(active);
|
|
143
139
|
}
|
|
144
|
-
|
|
140
|
+
const MAX_HOLD_MS = 30_000;
|
|
145
141
|
function clampHold(raw) {
|
|
146
142
|
if ('number' !== typeof raw || !Number.isFinite(raw) || raw <= 0)
|
|
147
143
|
return 0;
|
|
@@ -167,7 +163,9 @@ function asFiniteNumber(raw) {
|
|
|
167
163
|
async function dispatchOther(el, action, args) {
|
|
168
164
|
switch (action) {
|
|
169
165
|
case ActionType.DBLCLICK:
|
|
170
|
-
|
|
166
|
+
await fireClickSequence(el, undefined, 1);
|
|
167
|
+
await fireClickSequence(el, undefined, 2);
|
|
168
|
+
return !asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'dblclick', { bubbles: true, cancelable: true, detail: 2 })));
|
|
171
169
|
case ActionType.HOVER: {
|
|
172
170
|
const doc = el.ownerDocument;
|
|
173
171
|
const from = doc.activeElement ?? doc.body;
|
|
@@ -242,11 +240,16 @@ async function dispatchOther(el, action, args) {
|
|
|
242
240
|
throw new Error(`cannot select on a <${el.tagName.toLowerCase()}>`);
|
|
243
241
|
case ActionType.CHECK:
|
|
244
242
|
case ActionType.UNCHECK: {
|
|
245
|
-
|
|
243
|
+
const aria = isAriaToggle(el);
|
|
244
|
+
if (!isInput(el) && !aria) {
|
|
246
245
|
throw new Error(`cannot (un)check a <${el.tagName.toLowerCase()}>`);
|
|
246
|
+
}
|
|
247
247
|
if (!enabledOf(el)) {
|
|
248
248
|
throw new Error(`cannot ${action} a disabled control — a real user could not, so neither will Reticle`);
|
|
249
249
|
}
|
|
250
|
+
if (!isInput(el)) {
|
|
251
|
+
return alreadyAtCheckedState(el, action) ? false : (await fireClickSequence(el)).prevented;
|
|
252
|
+
}
|
|
250
253
|
if ('radio' === el.type && action === ActionType.UNCHECK) {
|
|
251
254
|
throw new Error('cannot uncheck a radio button — a real user could not; select another radio in the group');
|
|
252
255
|
}
|
|
@@ -322,7 +325,7 @@ async function dispatchOther(el, action, args) {
|
|
|
322
325
|
if ('' === toRef)
|
|
323
326
|
return await dragElement(el, null, args['data']);
|
|
324
327
|
const resolved = refs.resolve(toRef);
|
|
325
|
-
if (!
|
|
328
|
+
if (!isActionTarget(resolved)) {
|
|
326
329
|
throw new Error(`drag target '${echoRef(toRef)}' did not resolve to an element — pass a ref from ` +
|
|
327
330
|
'reticle_snapshot or reticle_query as args.toRef (alias: args.target)');
|
|
328
331
|
}
|
|
@@ -378,6 +381,7 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
378
381
|
settleReason = outcome.settled ? null : SettleReason.TIMEOUT;
|
|
379
382
|
obs.disconnect();
|
|
380
383
|
}
|
|
384
|
+
assertAriaToggleLanded(el, action, alreadyAtValue);
|
|
381
385
|
const valueAfter = valueOf(el);
|
|
382
386
|
const nextFocus = activeRef(el);
|
|
383
387
|
const effect = {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type ActionTarget } from '../dom/realm.js';
|
|
2
|
+
export declare const isAriaToggle: (el: Element) => boolean;
|
|
3
|
+
export declare const ariaChecked: (el: Element) => boolean;
|
|
4
|
+
export declare function assertAriaToggleLanded(el: Element, action: string, skipped: boolean): void;
|
|
5
|
+
export declare function alreadyAtCheckedState(el: ActionTarget, action: string): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ActionType } from '@reticlehq/core';
|
|
2
|
+
import { isInput } from '../dom/realm.js';
|
|
3
|
+
const ARIA_TOGGLE_ROLES = new Set(['checkbox', 'switch']);
|
|
4
|
+
const ARIA_CHECKED = 'aria-checked';
|
|
5
|
+
export const isAriaToggle = (el) => !isInput(el) && ARIA_TOGGLE_ROLES.has(el.getAttribute('role') ?? '');
|
|
6
|
+
export const ariaChecked = (el) => 'true' === el.getAttribute(ARIA_CHECKED);
|
|
7
|
+
export function assertAriaToggleLanded(el, action, skipped) {
|
|
8
|
+
if (skipped || !isAriaToggle(el))
|
|
9
|
+
return;
|
|
10
|
+
if (action !== ActionType.CHECK && action !== ActionType.UNCHECK)
|
|
11
|
+
return;
|
|
12
|
+
const wanted = action === ActionType.CHECK;
|
|
13
|
+
if (ariaChecked(el) === wanted)
|
|
14
|
+
return;
|
|
15
|
+
throw new Error(`${action} clicked the ${el.getAttribute('role') ?? ''} but ${ARIA_CHECKED} is still "${String(!wanted)}" — the app did not toggle it`);
|
|
16
|
+
}
|
|
17
|
+
export function alreadyAtCheckedState(el, action) {
|
|
18
|
+
if (action !== ActionType.CHECK && action !== ActionType.UNCHECK)
|
|
19
|
+
return false;
|
|
20
|
+
const wanted = action === ActionType.CHECK;
|
|
21
|
+
if (isInput(el))
|
|
22
|
+
return el.checked === wanted;
|
|
23
|
+
return isAriaToggle(el) && ariaChecked(el) === wanted;
|
|
24
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export declare function
|
|
1
|
+
import { type ActionTarget } from '../dom/realm.js';
|
|
2
|
+
export declare function dangerousActionContext(el: ActionTarget): string;
|
|
3
|
+
export declare function submitControlFor(el: ActionTarget): HTMLElement | null;
|
|
3
4
|
export declare function requiresDangerousConfirmation(text: string, role?: string): boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isDangerousActionText } from '@reticlehq/core';
|
|
2
2
|
import { getAccessibleName } from '../dom/a11y.js';
|
|
3
|
+
import { isHtmlElement } from '../dom/realm.js';
|
|
3
4
|
const LABEL_VALUED_INPUT_TYPES = new Set([
|
|
4
5
|
'submit',
|
|
5
6
|
'button',
|
|
@@ -7,7 +8,7 @@ const LABEL_VALUED_INPUT_TYPES = new Set([
|
|
|
7
8
|
'image',
|
|
8
9
|
]);
|
|
9
10
|
function holdsUserText(el) {
|
|
10
|
-
if (el.isContentEditable)
|
|
11
|
+
if (isHtmlElement(el) && el.isContentEditable)
|
|
11
12
|
return true;
|
|
12
13
|
if (el instanceof HTMLTextAreaElement)
|
|
13
14
|
return true;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ActionTarget } from '../dom/realm.js';
|
|
1
2
|
export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): boolean;
|
|
2
|
-
export declare function assertNotRichText(el:
|
|
3
|
+
export declare function assertNotRichText(el: ActionTarget, action: string): void;
|
|
3
4
|
export declare function assertEditable(el: HTMLInputElement | HTMLTextAreaElement, action: string): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { captureValueSetter } from '../patching/capture-method.js';
|
|
2
|
-
import { valuePrototypeOf } from '../dom/realm.js';
|
|
2
|
+
import { isHtmlElement, valuePrototypeOf } from '../dom/realm.js';
|
|
3
3
|
export function setNativeValue(el, value) {
|
|
4
4
|
const proto = valuePrototypeOf(el);
|
|
5
5
|
const setter = proto === undefined ? undefined : captureValueSetter(proto);
|
|
@@ -13,12 +13,21 @@ export function setNativeValue(el, value) {
|
|
|
13
13
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
14
14
|
return !notPrevented;
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
function isRichText(el) {
|
|
17
17
|
const flag = el.getAttribute('contenteditable');
|
|
18
|
-
if (el.isContentEditable || (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
if ((isHtmlElement(el) && el.isContentEditable) || (null !== flag && 'false' !== flag))
|
|
19
|
+
return true;
|
|
20
|
+
if ('textbox' === el.getAttribute('role'))
|
|
21
|
+
return true;
|
|
22
|
+
if (!('editContext' in el))
|
|
23
|
+
return false;
|
|
24
|
+
const surface = el;
|
|
25
|
+
return null !== surface.editContext && undefined !== surface.editContext;
|
|
26
|
+
}
|
|
27
|
+
export function assertNotRichText(el, action) {
|
|
28
|
+
if (!isRichText(el))
|
|
29
|
+
return;
|
|
30
|
+
throw new Error(`cannot ${action} a contenteditable or EditContext editor; use press or an input`);
|
|
22
31
|
}
|
|
23
32
|
export function assertEditable(el, action) {
|
|
24
33
|
if (el.disabled) {
|
package/dist/dom/a11y.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { ElementState, REDACTED_VALUE } from '@reticlehq/core';
|
|
2
|
-
import { isButton, isHtmlElement, isImage, isInput, isSelect, isTextArea } from './realm.js';
|
|
2
|
+
import { isButton, isHtmlElement, isImage, isInput, isMeter, isOutput, isProgress, isSelect, isTextArea, } from './realm.js';
|
|
3
3
|
import { refs } from './addressing/refs.js';
|
|
4
4
|
import { inspectChart } from './chart.js';
|
|
5
5
|
import { isSensitiveKey } from '../security/serialization.js';
|
|
6
6
|
import { formatSource, sourceFromDom } from './addressing/source.js';
|
|
7
|
+
const HTML_DETAILS_TAG = 'details';
|
|
8
|
+
const HTML_DETAILS_OPEN_ATTRIBUTE = 'open';
|
|
9
|
+
const HTML_DIRECT_SUMMARY_SELECTOR = ':scope > summary';
|
|
7
10
|
const NAME_FROM_CONTENT = new Set([
|
|
8
11
|
'button',
|
|
9
12
|
'link',
|
|
@@ -190,7 +193,14 @@ export function getAccessibleName(el) {
|
|
|
190
193
|
if (alt !== null)
|
|
191
194
|
return alt.trim();
|
|
192
195
|
}
|
|
193
|
-
|
|
196
|
+
const isLabelable = isInput(el) ||
|
|
197
|
+
isTextArea(el) ||
|
|
198
|
+
isSelect(el) ||
|
|
199
|
+
isButton(el) ||
|
|
200
|
+
isMeter(el) ||
|
|
201
|
+
isOutput(el) ||
|
|
202
|
+
isProgress(el);
|
|
203
|
+
if (isLabelable) {
|
|
194
204
|
const labels = el.labels;
|
|
195
205
|
if (labels !== null && labels.length > 0) {
|
|
196
206
|
const text = [...labels]
|
|
@@ -200,6 +210,16 @@ export function getAccessibleName(el) {
|
|
|
200
210
|
if (text.length > 0)
|
|
201
211
|
return text;
|
|
202
212
|
}
|
|
213
|
+
}
|
|
214
|
+
if ('fieldset' === el.tagName.toLowerCase()) {
|
|
215
|
+
const legend = [...el.children].find((child) => 'legend' === child.tagName.toLowerCase());
|
|
216
|
+
if (legend !== undefined) {
|
|
217
|
+
const text = collapse(textWithoutHidden(legend));
|
|
218
|
+
if (text.length > 0)
|
|
219
|
+
return text;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (isInput(el) || isTextArea(el) || isSelect(el)) {
|
|
203
223
|
if (isInput(el)) {
|
|
204
224
|
const type = el.type.toLowerCase();
|
|
205
225
|
if ('submit' === type || 'button' === type || 'reset' === type) {
|
|
@@ -241,6 +261,8 @@ export function getStates(el, visible = isVisible(el)) {
|
|
|
241
261
|
states.push(ElementState.CHECKED);
|
|
242
262
|
if (true === ariaBool(el, 'aria-expanded'))
|
|
243
263
|
states.push(ElementState.EXPANDED);
|
|
264
|
+
if (true === ariaBool(el, 'aria-pressed'))
|
|
265
|
+
states.push(ElementState.PRESSED);
|
|
244
266
|
if (el.ownerDocument.activeElement === el)
|
|
245
267
|
states.push(ElementState.FOCUSED);
|
|
246
268
|
return states;
|
|
@@ -270,11 +292,23 @@ export function getValue(el) {
|
|
|
270
292
|
const valueNow = el.getAttribute('aria-valuenow');
|
|
271
293
|
return valueNow ?? undefined;
|
|
272
294
|
}
|
|
295
|
+
function hiddenInsideClosedDetails(el) {
|
|
296
|
+
const parent = el.parentElement;
|
|
297
|
+
if (null === parent)
|
|
298
|
+
return false;
|
|
299
|
+
const details = parent.closest(HTML_DETAILS_TAG);
|
|
300
|
+
if (null === details || details.hasAttribute(HTML_DETAILS_OPEN_ATTRIBUTE))
|
|
301
|
+
return false;
|
|
302
|
+
const summary = details.querySelector(HTML_DIRECT_SUMMARY_SELECTOR);
|
|
303
|
+
return null === summary || !summary.contains(el);
|
|
304
|
+
}
|
|
273
305
|
function selfHidden(el) {
|
|
274
306
|
if ('true' === el.getAttribute('aria-hidden'))
|
|
275
307
|
return true;
|
|
276
308
|
if (isHtmlElement(el) && el.hidden)
|
|
277
309
|
return true;
|
|
310
|
+
if (hiddenInsideClosedDetails(el))
|
|
311
|
+
return true;
|
|
278
312
|
const view = el.ownerDocument.defaultView;
|
|
279
313
|
if (view !== null) {
|
|
280
314
|
const style = view.getComputedStyle(el);
|
|
@@ -288,6 +322,12 @@ function selfHidden(el) {
|
|
|
288
322
|
}
|
|
289
323
|
return false;
|
|
290
324
|
}
|
|
325
|
+
function parentAcrossShadowBoundary(el) {
|
|
326
|
+
if (null !== el.parentElement)
|
|
327
|
+
return el.parentElement;
|
|
328
|
+
const host = el.getRootNode().host;
|
|
329
|
+
return host ?? null;
|
|
330
|
+
}
|
|
291
331
|
export function isInViewport(el, memo) {
|
|
292
332
|
if (!isVisible(el, memo))
|
|
293
333
|
return false;
|
|
@@ -305,7 +345,7 @@ export function isVisible(el, memo) {
|
|
|
305
345
|
const cached = memo?.get(el);
|
|
306
346
|
if (cached !== undefined)
|
|
307
347
|
return cached;
|
|
308
|
-
const parent = el
|
|
348
|
+
const parent = parentAcrossShadowBoundary(el);
|
|
309
349
|
const result = !selfHidden(el) && (null === parent || isVisible(parent, memo));
|
|
310
350
|
if (memo !== undefined)
|
|
311
351
|
memo.set(el, result);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
interface SourceLocation {
|
|
2
2
|
file: string;
|
|
3
3
|
line: number;
|
|
4
4
|
}
|
|
@@ -7,3 +7,4 @@ export declare function sourceFromDom(el: Element): SourceLocation | undefined;
|
|
|
7
7
|
export declare function sourceFor(el: Element, adapterSource: SourceLocation | undefined): SourceLocation | undefined;
|
|
8
8
|
export declare function documentHasSourceStamps(doc: Document): boolean;
|
|
9
9
|
export declare function formatSource(source: SourceLocation | undefined): string | undefined;
|
|
10
|
+
export {};
|
package/dist/dom/dom-ignore.d.ts
CHANGED
|
@@ -5,3 +5,4 @@ export declare function setIgnoreSelectors(selectors: string[]): void;
|
|
|
5
5
|
export declare function isReticleOverlay(el: Element): boolean;
|
|
6
6
|
export declare function isReticleUi(node: Element | null): boolean;
|
|
7
7
|
export declare function isIgnored(el: Element): boolean;
|
|
8
|
+
export declare function appModalOpen(doc: Document): boolean;
|
package/dist/dom/dom-ignore.js
CHANGED
|
@@ -22,3 +22,16 @@ export function isIgnored(el) {
|
|
|
22
22
|
const sel = [ours, DEV_OVERLAYS, extraIgnore].filter((part) => part.length > 0).join(',');
|
|
23
23
|
return el.closest(sel) !== null;
|
|
24
24
|
}
|
|
25
|
+
const APP_MODAL = 'dialog[open], [aria-modal="true"]';
|
|
26
|
+
export function appModalOpen(doc) {
|
|
27
|
+
for (const el of Array.from(doc.querySelectorAll(APP_MODAL))) {
|
|
28
|
+
if (el.closest(RETICLE_OVERLAY) !== null)
|
|
29
|
+
continue;
|
|
30
|
+
if (el instanceof HTMLElement && (el.hidden || null !== el.closest('[hidden]')))
|
|
31
|
+
continue;
|
|
32
|
+
if (el instanceof HTMLElement && 'none' === getComputedStyle(el).display)
|
|
33
|
+
continue;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
package/dist/dom/query.js
CHANGED
|
@@ -409,15 +409,16 @@ function buildEmptyHint(query) {
|
|
|
409
409
|
const container = resolveContainer(query.scope).container ?? document.body;
|
|
410
410
|
const all = container.querySelectorAll(`[${TESTID_ATTR}]`);
|
|
411
411
|
const present = [];
|
|
412
|
+
const seen = new Set();
|
|
412
413
|
for (const el of Array.from(all)) {
|
|
413
414
|
if (isIgnored(el))
|
|
414
415
|
continue;
|
|
415
416
|
const id = el.getAttribute(TESTID_ATTR);
|
|
416
|
-
if (id
|
|
417
|
+
if (null === id || 0 === id.length || seen.has(id))
|
|
418
|
+
continue;
|
|
419
|
+
seen.add(id);
|
|
420
|
+
if (present.length < MAX_PRESENT_TESTIDS)
|
|
417
421
|
present.push(id);
|
|
418
|
-
if (present.length >= MAX_PRESENT_TESTIDS)
|
|
419
|
-
break;
|
|
420
|
-
}
|
|
421
422
|
}
|
|
422
423
|
const registered = declaredTestids();
|
|
423
424
|
const knownEmptyState = present.some((id) => registered.includes(id));
|
|
@@ -425,6 +426,7 @@ function buildEmptyHint(query) {
|
|
|
425
426
|
const hint = {
|
|
426
427
|
route,
|
|
427
428
|
presentTestids: present,
|
|
429
|
+
...(seen.size > present.length ? { presentTestidsTotal: seen.size } : {}),
|
|
428
430
|
presentRegions: buildPresentRegions(query),
|
|
429
431
|
knownEmptyState,
|
|
430
432
|
};
|
package/dist/dom/realm.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
export declare const isElement: (n: unknown) => n is Element;
|
|
2
2
|
export declare const isImage: (n: unknown) => n is HTMLImageElement;
|
|
3
3
|
export declare const isHtmlElement: (n: unknown) => n is HTMLElement;
|
|
4
|
+
export declare const isSvgElement: (n: unknown) => n is SVGElement;
|
|
5
|
+
export type ActionTarget = HTMLElement | SVGElement;
|
|
6
|
+
export declare const isActionTarget: (n: unknown) => n is ActionTarget;
|
|
4
7
|
export declare const isInput: (n: unknown) => n is HTMLInputElement;
|
|
5
8
|
export declare const isTextArea: (n: unknown) => n is HTMLTextAreaElement;
|
|
6
9
|
export declare const isSelect: (n: unknown) => n is HTMLSelectElement;
|
|
7
10
|
export declare const isButton: (n: unknown) => n is HTMLButtonElement;
|
|
11
|
+
export declare const isMeter: (n: unknown) => n is HTMLMeterElement;
|
|
12
|
+
export declare const isOutput: (n: unknown) => n is HTMLOutputElement;
|
|
13
|
+
export declare const isProgress: (n: unknown) => n is HTMLProgressElement;
|
|
8
14
|
export declare const isForm: (n: unknown) => n is HTMLFormElement;
|
|
9
15
|
export declare const isFrame: (n: unknown) => n is HTMLIFrameElement;
|
|
10
16
|
export declare function valuePrototypeOf(el: Element): object | undefined;
|
package/dist/dom/realm.js
CHANGED
|
@@ -14,10 +14,15 @@ function isIn(node, name, ambient) {
|
|
|
14
14
|
export const isElement = (n) => isIn(n, 'Element', 'undefined' === typeof Element ? undefined : Element);
|
|
15
15
|
export const isImage = (n) => isIn(n, 'HTMLImageElement', 'undefined' === typeof HTMLImageElement ? undefined : HTMLImageElement);
|
|
16
16
|
export const isHtmlElement = (n) => isIn(n, 'HTMLElement', 'undefined' === typeof HTMLElement ? undefined : HTMLElement);
|
|
17
|
+
export const isSvgElement = (n) => isIn(n, 'SVGElement', 'undefined' === typeof SVGElement ? undefined : SVGElement);
|
|
18
|
+
export const isActionTarget = (n) => isHtmlElement(n) || isSvgElement(n);
|
|
17
19
|
export const isInput = (n) => isIn(n, 'HTMLInputElement', 'undefined' === typeof HTMLInputElement ? undefined : HTMLInputElement);
|
|
18
20
|
export const isTextArea = (n) => isIn(n, 'HTMLTextAreaElement', 'undefined' === typeof HTMLTextAreaElement ? undefined : HTMLTextAreaElement);
|
|
19
21
|
export const isSelect = (n) => isIn(n, 'HTMLSelectElement', 'undefined' === typeof HTMLSelectElement ? undefined : HTMLSelectElement);
|
|
20
22
|
export const isButton = (n) => isIn(n, 'HTMLButtonElement', 'undefined' === typeof HTMLButtonElement ? undefined : HTMLButtonElement);
|
|
23
|
+
export const isMeter = (n) => isIn(n, 'HTMLMeterElement', 'undefined' === typeof HTMLMeterElement ? undefined : HTMLMeterElement);
|
|
24
|
+
export const isOutput = (n) => isIn(n, 'HTMLOutputElement', 'undefined' === typeof HTMLOutputElement ? undefined : HTMLOutputElement);
|
|
25
|
+
export const isProgress = (n) => isIn(n, 'HTMLProgressElement', 'undefined' === typeof HTMLProgressElement ? undefined : HTMLProgressElement);
|
|
21
26
|
export const isForm = (n) => isIn(n, 'HTMLFormElement', 'undefined' === typeof HTMLFormElement ? undefined : HTMLFormElement);
|
|
22
27
|
export const isFrame = (n) => isIn(n, 'HTMLIFrameElement', 'undefined' === typeof HTMLIFrameElement ? undefined : HTMLIFrameElement);
|
|
23
28
|
export function valuePrototypeOf(el) {
|
package/dist/dom/snapshot.d.ts
CHANGED
package/dist/dom/snapshot.js
CHANGED
|
@@ -54,6 +54,7 @@ function stateSuffix(el) {
|
|
|
54
54
|
const states = getStates(el).filter((s) => s === ElementState.DISABLED ||
|
|
55
55
|
s === ElementState.CHECKED ||
|
|
56
56
|
s === ElementState.EXPANDED ||
|
|
57
|
+
s === ElementState.PRESSED ||
|
|
57
58
|
s === ElementState.FOCUSED);
|
|
58
59
|
return states.length > 0 ? ` [${states.join(',')}]` : '';
|
|
59
60
|
}
|
|
@@ -142,9 +143,14 @@ function visit(child, depth, ctx, inLive) {
|
|
|
142
143
|
}
|
|
143
144
|
}
|
|
144
145
|
function walk(parent, depth, ctx, inLive = false) {
|
|
145
|
-
if (depth > ctx.maxDepth)
|
|
146
|
-
return;
|
|
147
146
|
const children = pierceChildren(parent);
|
|
147
|
+
if (depth > ctx.maxDepth) {
|
|
148
|
+
if (children.length > 0) {
|
|
149
|
+
ctx.truncated = true;
|
|
150
|
+
recordUnread([parent], ctx);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
148
154
|
for (let index = 0; index < children.length; index += 1) {
|
|
149
155
|
if (ctx.nodes >= ctx.maxNodes) {
|
|
150
156
|
ctx.truncated = true;
|
|
@@ -2,6 +2,9 @@ import { EventType, TRANSPORT_LIMITS } from '@reticlehq/core';
|
|
|
2
2
|
import { observeSafely } from './types.js';
|
|
3
3
|
import { safeStringify } from '../security/serialization.js';
|
|
4
4
|
import { requireCapturedMethod } from '../util/captured-method.js';
|
|
5
|
+
import { createRepeatLimiter } from './error-repeats.js';
|
|
6
|
+
const CSP_VIOLATION_EVENT = 'securitypolicyviolation';
|
|
7
|
+
const REPORT_ONLY = 'report';
|
|
5
8
|
const METHOD_EVENT = {
|
|
6
9
|
log: EventType.CONSOLE_LOG,
|
|
7
10
|
warn: EventType.CONSOLE_WARN,
|
|
@@ -54,6 +57,16 @@ export function installConsole(emit) {
|
|
|
54
57
|
patched.set(method, wrapper);
|
|
55
58
|
console[method] = wrapper;
|
|
56
59
|
}
|
|
60
|
+
const repeats = createRepeatLimiter();
|
|
61
|
+
const emitError = (payload) => {
|
|
62
|
+
const decision = repeats.admit(payload);
|
|
63
|
+
if (!decision.emit)
|
|
64
|
+
return;
|
|
65
|
+
emit(EventType.ERROR_UNCAUGHT, {
|
|
66
|
+
...payload,
|
|
67
|
+
...(undefined === decision.repeats ? {} : { repeats: decision.repeats }),
|
|
68
|
+
});
|
|
69
|
+
};
|
|
57
70
|
const onError = (event) => {
|
|
58
71
|
if (!(event instanceof ErrorEvent)) {
|
|
59
72
|
const target = event.target;
|
|
@@ -62,43 +75,55 @@ export function installConsole(emit) {
|
|
|
62
75
|
const tag = target.tagName.toLowerCase();
|
|
63
76
|
const url = target.getAttribute('src') ?? target.getAttribute('href') ?? '';
|
|
64
77
|
observeSafely(() => {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
kind: 'resource',
|
|
68
|
-
...(0 === url.length ? {} : { source: url }),
|
|
69
|
-
});
|
|
78
|
+
const message = `<${tag}> failed to load${0 === url.length ? '' : `: ${url}`}`;
|
|
79
|
+
emitError({ message, kind: 'resource', ...(0 === url.length ? {} : { source: url }) });
|
|
70
80
|
});
|
|
71
81
|
return;
|
|
72
82
|
}
|
|
73
83
|
observeSafely(() => {
|
|
74
84
|
const stack = capStack(event.error instanceof Error ? event.error.stack : undefined);
|
|
75
|
-
|
|
85
|
+
const payload = {
|
|
76
86
|
message: event.message,
|
|
77
87
|
source: event.filename,
|
|
78
88
|
line: event.lineno,
|
|
79
89
|
...(stack === undefined ? {} : { stack }),
|
|
80
|
-
}
|
|
90
|
+
};
|
|
91
|
+
emitError(payload);
|
|
81
92
|
});
|
|
82
93
|
};
|
|
83
94
|
const onRejection = (event) => {
|
|
84
95
|
observeSafely(() => {
|
|
85
96
|
const reason = event.reason;
|
|
86
97
|
const stack = capStack(reason instanceof Error ? reason.stack : undefined);
|
|
87
|
-
|
|
98
|
+
const payload = {
|
|
88
99
|
message: reason instanceof Error ? reason.message : String(reason),
|
|
89
100
|
kind: 'unhandledrejection',
|
|
90
101
|
...(stack === undefined ? {} : { stack }),
|
|
91
|
-
}
|
|
102
|
+
};
|
|
103
|
+
emitError(payload);
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
const onViolation = (event) => {
|
|
107
|
+
const what = 0 === event.blockedURI.length ? 'inline content' : event.blockedURI;
|
|
108
|
+
const reportOnly = REPORT_ONLY === event.disposition;
|
|
109
|
+
emit(reportOnly ? EventType.CONSOLE_WARN : EventType.CONSOLE_ERROR, {
|
|
110
|
+
message: reportOnly
|
|
111
|
+
? `[Report Only] Content Security Policy directive: ${event.violatedDirective} would block ${what}`
|
|
112
|
+
: `Content Security Policy directive: ${event.violatedDirective} blocked ${what}`,
|
|
113
|
+
kind: CSP_VIOLATION_EVENT,
|
|
114
|
+
...(0 === event.blockedURI.length ? {} : { source: event.blockedURI }),
|
|
92
115
|
});
|
|
93
116
|
};
|
|
94
|
-
|
|
95
|
-
|
|
117
|
+
const listeners = new AbortController();
|
|
118
|
+
const { signal } = listeners;
|
|
119
|
+
window.addEventListener('error', onError, { capture: true, signal });
|
|
120
|
+
window.addEventListener('unhandledrejection', onRejection, { signal });
|
|
121
|
+
window.addEventListener(CSP_VIOLATION_EVENT, onViolation, { capture: true, signal });
|
|
96
122
|
return () => {
|
|
97
123
|
for (const [method, original] of originals) {
|
|
98
124
|
if (console[method] === patched.get(method))
|
|
99
125
|
console[method] = original;
|
|
100
126
|
}
|
|
101
|
-
|
|
102
|
-
window.removeEventListener('unhandledrejection', onRejection);
|
|
127
|
+
listeners.abort();
|
|
103
128
|
};
|
|
104
129
|
}
|
package/dist/observers/dom.js
CHANGED
|
@@ -153,8 +153,10 @@ export function installDom(emit) {
|
|
|
153
153
|
dropped += 1;
|
|
154
154
|
continue;
|
|
155
155
|
}
|
|
156
|
-
if (isReticleOverlay(node)
|
|
156
|
+
if (isReticleOverlay(node) ||
|
|
157
|
+
(isElement(record.target) && isReticleOverlay(record.target))) {
|
|
157
158
|
continue;
|
|
159
|
+
}
|
|
158
160
|
const gone = describeMount(node);
|
|
159
161
|
if (gone === undefined)
|
|
160
162
|
continue;
|