@reticlehq/browser 2.4.0 → 2.5.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.js +10 -10
- package/dist/actions/actions.d.ts +13 -0
- package/dist/actions/actions.js +45 -15
- package/dist/actions/scroll.js +1 -1
- package/dist/commands/commands.js +27 -22
- package/dist/connect-options.d.ts +6 -0
- package/dist/dom/a11y.js +20 -20
- package/dist/dom/auto-anchor.js +1 -1
- package/dist/dom/blind-spots.js +1 -1
- package/dist/dom/chart.js +5 -5
- package/dist/dom/desktop-capture.d.ts +2 -1
- package/dist/dom/desktop-capture.js +4 -4
- package/dist/dom/occlusion.js +2 -2
- package/dist/dom/query.js +28 -18
- package/dist/dom/realm.js +13 -13
- package/dist/dom/refs.d.ts +14 -0
- package/dist/dom/refs.js +20 -1
- package/dist/dom/shadow-registry.js +2 -2
- package/dist/dom/snapshot.js +7 -7
- package/dist/dom/source.js +3 -3
- package/dist/dom/theme.js +6 -6
- package/dist/dom/virtualized.js +1 -1
- package/dist/observers/blind-spots.js +2 -2
- package/dist/observers/console.js +3 -3
- package/dist/observers/dom.js +5 -5
- package/dist/observers/download.d.ts +2 -1
- package/dist/observers/download.js +2 -2
- package/dist/observers/focus.js +1 -1
- package/dist/observers/frames.js +1 -1
- package/dist/observers/health.js +70 -2
- package/dist/observers/install-all.d.ts +2 -1
- package/dist/observers/ipc.d.ts +2 -7
- package/dist/observers/ipc.js +7 -7
- package/dist/observers/network-body.js +2 -2
- package/dist/observers/network-redact.js +5 -5
- package/dist/observers/network-stream.js +2 -2
- package/dist/observers/network.d.ts +3 -3
- package/dist/observers/network.js +9 -9
- package/dist/observers/perf.js +1 -1
- package/dist/observers/scroll.js +1 -1
- package/dist/observers/state.js +1 -1
- package/dist/observers/storage.js +7 -7
- package/dist/presenter/presenter-brand.d.ts +19 -0
- package/dist/presenter/presenter-brand.js +21 -0
- package/dist/presenter/presenter-config.d.ts +17 -0
- package/dist/presenter/presenter-config.js +21 -0
- package/dist/presenter/presenter-controls.d.ts +1 -11
- package/dist/presenter/presenter-controls.js +6 -7
- package/dist/presenter/presenter-log.js +2 -2
- package/dist/presenter/presenter-run-state.js +1 -1
- package/dist/presenter/presenter-styles.d.ts +1 -1
- package/dist/presenter/presenter-styles.js +20 -9
- package/dist/presenter/presenter-tally.js +3 -3
- package/dist/presenter/presenter-test-helpers.d.ts +2 -1
- package/dist/presenter/presenter-test-helpers.js +2 -2
- package/dist/presenter/presenter-verbs.js +16 -11
- package/dist/presenter/presenter.js +13 -10
- package/dist/recorder/recorder.js +10 -9
- package/dist/registry/store-adapters.js +1 -1
- package/dist/registry/stores.js +3 -3
- package/dist/reticle-presenter-helpers.js +3 -3
- package/dist/reticle.js +31 -13
- package/dist/review/annotator.js +7 -7
- package/dist/security/serialization.js +9 -9
- package/dist/session-continuity.d.ts +17 -0
- package/dist/session-continuity.js +42 -0
- package/dist/timers/clock.js +4 -4
- package/dist/timers/native-console.js +1 -1
- package/dist/timers/native-timers.js +4 -4
- package/dist/transport/transport.js +5 -5
- package/package.json +6 -4
|
@@ -17,7 +17,7 @@ export function fireClickSequence(el) {
|
|
|
17
17
|
const from = doc.activeElement ?? doc.body;
|
|
18
18
|
firePointer(el, 'pointerdown', from);
|
|
19
19
|
el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
|
|
20
|
-
if (el.tabIndex >= 0 && typeof el.focus
|
|
20
|
+
if (el.tabIndex >= 0 && 'function' === typeof el.focus)
|
|
21
21
|
el.focus();
|
|
22
22
|
firePointer(el, 'pointerup', from);
|
|
23
23
|
el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
|
|
@@ -31,7 +31,7 @@ function isMeasurable(rect) {
|
|
|
31
31
|
/** The center of `rect` falls outside the visible viewport. */
|
|
32
32
|
function isOffViewport(el, rect) {
|
|
33
33
|
const win = el.ownerDocument.defaultView;
|
|
34
|
-
if (
|
|
34
|
+
if (null === win)
|
|
35
35
|
return false;
|
|
36
36
|
const cx = rect.left + rect.width / 2;
|
|
37
37
|
const cy = rect.top + rect.height / 2;
|
|
@@ -40,7 +40,7 @@ function isOffViewport(el, rect) {
|
|
|
40
40
|
/** Hit-test the center: occluded iff the top NON-Reticle element is a foreign subtree (not target/ancestor/descendant). */
|
|
41
41
|
function hitTest(el, rect) {
|
|
42
42
|
const top = hitTestOccluder(el, rect);
|
|
43
|
-
return
|
|
43
|
+
return null === top
|
|
44
44
|
? { occluded: false, occludedBy: null }
|
|
45
45
|
: { occluded: true, occludedBy: refs.refFor(top) };
|
|
46
46
|
}
|
|
@@ -56,7 +56,7 @@ export function clickGeometry(el) {
|
|
|
56
56
|
if (!isMeasurable(rect))
|
|
57
57
|
return NO_GEOMETRY;
|
|
58
58
|
let scrolledIntoView = false;
|
|
59
|
-
if (isOffViewport(el, rect) && typeof el.scrollIntoView
|
|
59
|
+
if (isOffViewport(el, rect) && 'function' === typeof el.scrollIntoView) {
|
|
60
60
|
el.scrollIntoView({ block: 'center', inline: 'center' });
|
|
61
61
|
scrolledIntoView = true;
|
|
62
62
|
rect = el.getBoundingClientRect();
|
|
@@ -64,7 +64,7 @@ export function clickGeometry(el) {
|
|
|
64
64
|
return { ...hitTest(el, rect), scrolledIntoView };
|
|
65
65
|
}
|
|
66
66
|
export function firePointer(el, type, relatedTarget = null) {
|
|
67
|
-
if (typeof PointerEvent
|
|
67
|
+
if ('function' === typeof PointerEvent) {
|
|
68
68
|
el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true, relatedTarget }));
|
|
69
69
|
}
|
|
70
70
|
else {
|
|
@@ -73,7 +73,7 @@ export function firePointer(el, type, relatedTarget = null) {
|
|
|
73
73
|
}
|
|
74
74
|
/** Enter/leave pointer events are non-bubbling per spec; keep them so to avoid double-firing. */
|
|
75
75
|
export function firePointerNonBubbling(el, type, relatedTarget = null) {
|
|
76
|
-
if (typeof PointerEvent
|
|
76
|
+
if ('function' === typeof PointerEvent) {
|
|
77
77
|
el.dispatchEvent(new PointerEvent(type, { bubbles: false, cancelable: true, relatedTarget }));
|
|
78
78
|
}
|
|
79
79
|
else {
|
|
@@ -87,9 +87,9 @@ function makeDataTransfer(data) {
|
|
|
87
87
|
// data: { mime, value } or [{ mime, value }, …]
|
|
88
88
|
const entries = Array.isArray(data) ? data : data !== undefined ? [data] : [];
|
|
89
89
|
for (const entry of entries) {
|
|
90
|
-
if (typeof entry
|
|
90
|
+
if ('object' === typeof entry && entry !== null) {
|
|
91
91
|
const e = entry;
|
|
92
|
-
if (typeof e.mime
|
|
92
|
+
if ('string' === typeof e.mime && 'string' === typeof e.value)
|
|
93
93
|
dt.setData(e.mime, e.value);
|
|
94
94
|
}
|
|
95
95
|
}
|
|
@@ -102,7 +102,7 @@ function makeDataTransfer(data) {
|
|
|
102
102
|
export async function dragElement(source, target, data) {
|
|
103
103
|
const dest = target ?? source;
|
|
104
104
|
const fire = (el, type) => {
|
|
105
|
-
if (typeof PointerEvent
|
|
105
|
+
if ('function' === typeof PointerEvent && type.startsWith('pointer')) {
|
|
106
106
|
el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true }));
|
|
107
107
|
}
|
|
108
108
|
else {
|
|
@@ -118,7 +118,7 @@ export async function dragElement(source, target, data) {
|
|
|
118
118
|
fire(dest, 'pointerup');
|
|
119
119
|
fire(dest, 'mouseup');
|
|
120
120
|
let dropPrevented = false;
|
|
121
|
-
if (typeof DragEvent
|
|
121
|
+
if ('function' === typeof DragEvent) {
|
|
122
122
|
const dataTransfer = makeDataTransfer(data);
|
|
123
123
|
const init = { bubbles: true, cancelable: true };
|
|
124
124
|
if (dataTransfer !== null)
|
|
@@ -81,7 +81,20 @@ interface SequenceStepResult {
|
|
|
81
81
|
dispatched: boolean;
|
|
82
82
|
settled: boolean;
|
|
83
83
|
settleReason: SettleReason | null;
|
|
84
|
+
/**
|
|
85
|
+
* The SAME anchors a single act reports. Reporting only the testid meant a testid-less app
|
|
86
|
+
* compiled every sub-step to a volatile ref, so the saved flow carried the degraded sentinel and
|
|
87
|
+
* every replay drifted.
|
|
88
|
+
*/
|
|
84
89
|
testid?: string;
|
|
90
|
+
component?: string;
|
|
91
|
+
role?: string;
|
|
92
|
+
name?: string;
|
|
93
|
+
source?: {
|
|
94
|
+
file: string;
|
|
95
|
+
line: number;
|
|
96
|
+
column?: number;
|
|
97
|
+
};
|
|
85
98
|
/** best-effort caveat for this step (e.g. synthetic hover may not fire enter/leave). */
|
|
86
99
|
warning?: string;
|
|
87
100
|
}
|
package/dist/actions/actions.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { ActionType, ActionWarning, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, isDangerousActionText, SettleReason, } from '@reticlehq/core';
|
|
2
|
-
import { refs } from '../dom/refs.js';
|
|
2
|
+
import { echoRef, refs } from '../dom/refs.js';
|
|
3
3
|
import { assertEditable, assertNotRichText, setNativeValue } from './value-input.js';
|
|
4
4
|
import { getAccessibleName, getRole, isVisible, getStates } from '../dom/a11y.js';
|
|
5
5
|
import { elementHasHoverHandlers, identifyComponent } from '../registry/adapters.js';
|
|
6
6
|
import { isForm, isHtmlElement, isInput, isSelect, isTextArea } from '../dom/realm.js';
|
|
7
7
|
import { nativeSetTimeout, settle } from '../timers/native-timers.js';
|
|
8
8
|
function asString(value, fallback = '') {
|
|
9
|
-
return typeof value
|
|
9
|
+
return 'string' === typeof value ? value : fallback;
|
|
10
10
|
}
|
|
11
11
|
function requireElement(ref) {
|
|
12
12
|
const el = refs.resolve(ref);
|
|
13
|
-
if (
|
|
14
|
-
throw new Error(`ref '${ref}' no longer resolves to an element`);
|
|
13
|
+
if (null === el)
|
|
14
|
+
throw new Error(`ref '${echoRef(ref)}' no longer resolves to an element`);
|
|
15
15
|
if (!isHtmlElement(el))
|
|
16
|
-
throw new Error(`ref '${ref}' is not an HTMLElement`);
|
|
16
|
+
throw new Error(`ref '${echoRef(ref)}' is not an HTMLElement`);
|
|
17
17
|
return el;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
@@ -39,12 +39,20 @@ function anchorOf(el) {
|
|
|
39
39
|
out.component = component;
|
|
40
40
|
if (info?.source !== undefined)
|
|
41
41
|
out.source = info.source;
|
|
42
|
+
// Reported INDEPENDENTLY. They used to be all-or-nothing, so an element with a role and no
|
|
43
|
+
// accessible name — an icon button, a clickable div, a control labelled by an SVG — came back with
|
|
44
|
+
// no identity at all once identifyComponent (React-specific) also found nothing. Two features read
|
|
45
|
+
// this and both silently degrade without it: the flow recorder anchors a step by it, and coverage
|
|
46
|
+
// recognises a control it already drove across a re-render by it.
|
|
47
|
+
//
|
|
48
|
+
// A role alone is not a unique anchor, and the flow compiler still requires role AND name before it
|
|
49
|
+
// will anchor a step that way. But it is real information, and reporting it beats reporting nothing.
|
|
42
50
|
const role = getRole(el);
|
|
43
51
|
const name = getAccessibleName(el);
|
|
44
|
-
if (role.length > 0
|
|
52
|
+
if (role.length > 0)
|
|
45
53
|
out.role = role;
|
|
54
|
+
if (name.length > 0)
|
|
46
55
|
out.name = name;
|
|
47
|
-
}
|
|
48
56
|
return out;
|
|
49
57
|
}
|
|
50
58
|
const result = (ref, action, effect, settled, settleReason, anchor, warning) => {
|
|
@@ -108,7 +116,7 @@ function assertActionAllowed(el, action, args) {
|
|
|
108
116
|
action === ActionType.DBLCLICK ||
|
|
109
117
|
action === ActionType.DRAG ||
|
|
110
118
|
action === ActionType.SUBMIT ||
|
|
111
|
-
(action === ActionType.PRESS && asString(args['key'], 'Enter')
|
|
119
|
+
(action === ActionType.PRESS && 'Enter' === asString(args['key'], 'Enter'));
|
|
112
120
|
const dragTarget = action === ActionType.DRAG ? refs.resolve(asString(args['toRef'])) : null;
|
|
113
121
|
const context = isHtmlElement(dragTarget)
|
|
114
122
|
? `${dangerousActionContext(el)} ${dangerousActionContext(dragTarget)}`
|
|
@@ -135,7 +143,7 @@ function valueOf(el) {
|
|
|
135
143
|
/** Ref of the currently-focused element, treating body/null as "no focus". */
|
|
136
144
|
function activeRef(el) {
|
|
137
145
|
const active = el.ownerDocument.activeElement;
|
|
138
|
-
if (
|
|
146
|
+
if (null === active || active === el.ownerDocument.body)
|
|
139
147
|
return null;
|
|
140
148
|
return refs.refFor(active);
|
|
141
149
|
}
|
|
@@ -165,7 +173,7 @@ async function dispatchFor(el, action, args) {
|
|
|
165
173
|
firePointer(el, 'pointermove', from);
|
|
166
174
|
const moved = el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true }));
|
|
167
175
|
// hover-dwell: keep "hovering" for holdMs so timer-gated reveals can mount.
|
|
168
|
-
const holdMs = typeof args['holdMs']
|
|
176
|
+
const holdMs = 'number' === typeof args['holdMs'] ? args['holdMs'] : 0;
|
|
169
177
|
if (holdMs > 0)
|
|
170
178
|
await sleep(holdMs);
|
|
171
179
|
return !moved;
|
|
@@ -224,13 +232,27 @@ async function dispatchFor(el, action, args) {
|
|
|
224
232
|
if (isSelect(el)) {
|
|
225
233
|
// A MISSING value assigned '' to the <select>. No option carries that, so the browser sets
|
|
226
234
|
// selectedIndex to -1 — deselecting everything — from a call that was simply malformed.
|
|
227
|
-
// (Selecting a value that no option carries is a different case and is deliberately NOT
|
|
228
|
-
// refused: `actions.effect.test.ts` pins it as a detectable no-op, where the reported
|
|
229
|
-
// valueChanged delta is what tells the agent the option never took.)
|
|
230
235
|
if (typeof args['value'] !== 'string') {
|
|
231
236
|
throw new Error("select requires a string `value` — pass it nested, as args: { value: '…' }.");
|
|
232
237
|
}
|
|
233
|
-
|
|
238
|
+
const wanted = args['value'];
|
|
239
|
+
const options = Array.from(el.options);
|
|
240
|
+
// REFUSE an option that does not exist, before touching the element.
|
|
241
|
+
//
|
|
242
|
+
// This used to assign anyway and report the resulting `valueChanged` delta as proof the
|
|
243
|
+
// option never took. That only works if nobody is listening. An unmatched value drives
|
|
244
|
+
// selectedIndex to -1, so `el.value` becomes '' — and we then dispatched `change`. Reported
|
|
245
|
+
// from a real session: the app read that empty value in its change handler and persisted it,
|
|
246
|
+
// corrupting a stored language setting. The "detectable no-op" both mutated the app into a
|
|
247
|
+
// state no user could reach AND did it through the app's own handler.
|
|
248
|
+
//
|
|
249
|
+
// Same rule as the readonly/disabled refusal: if a real user could not do it, forcing it is
|
|
250
|
+
// not a test, it is damage.
|
|
251
|
+
if (!options.some((option) => option.value === wanted)) {
|
|
252
|
+
const available = options.map((option) => `${option.value} (${option.label})`).join(', ');
|
|
253
|
+
throw new Error(`no <option> with value '${echoRef(wanted)}' — available: ${0 === options.length ? '(none)' : available}`);
|
|
254
|
+
}
|
|
255
|
+
el.value = wanted;
|
|
234
256
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
235
257
|
return false; // change is not cancelable.
|
|
236
258
|
}
|
|
@@ -245,7 +267,7 @@ async function dispatchFor(el, action, args) {
|
|
|
245
267
|
throw new Error(`cannot (un)check a <${el.tagName.toLowerCase()}>`);
|
|
246
268
|
case ActionType.SUBMIT: {
|
|
247
269
|
const form = isForm(el) ? el : el.closest('form');
|
|
248
|
-
if (
|
|
270
|
+
if (null === form)
|
|
249
271
|
throw new Error('no form to submit');
|
|
250
272
|
form.requestSubmit();
|
|
251
273
|
return false; // requestSubmit returns void; the internal submit event is unobservable.
|
|
@@ -377,6 +399,14 @@ export async function executeSequence(steps) {
|
|
|
377
399
|
};
|
|
378
400
|
if (res.testid !== undefined)
|
|
379
401
|
stepBase.testid = res.testid;
|
|
402
|
+
if (res.component !== undefined)
|
|
403
|
+
stepBase.component = res.component;
|
|
404
|
+
if (res.role !== undefined)
|
|
405
|
+
stepBase.role = res.role;
|
|
406
|
+
if (res.name !== undefined)
|
|
407
|
+
stepBase.name = res.name;
|
|
408
|
+
if (res.source !== undefined)
|
|
409
|
+
stepBase.source = res.source;
|
|
380
410
|
if (res.warning !== undefined)
|
|
381
411
|
stepBase.warning = res.warning;
|
|
382
412
|
stepResults.push(stepBase);
|
package/dist/actions/scroll.js
CHANGED
|
@@ -6,7 +6,7 @@ function nearestScrollable(el) {
|
|
|
6
6
|
let cur = el;
|
|
7
7
|
while (cur !== null && cur !== document.body) {
|
|
8
8
|
const oy = getComputedStyle(cur).overflowY;
|
|
9
|
-
if ((
|
|
9
|
+
if (('auto' === oy || 'scroll' === oy) && cur.scrollHeight > cur.clientHeight)
|
|
10
10
|
return cur;
|
|
11
11
|
cur = cur.parentElement;
|
|
12
12
|
}
|
|
@@ -5,7 +5,7 @@ import { executeAction, executeSequence, dispatchWebMcp, } from '../actions/acti
|
|
|
5
5
|
import { describe } from '../dom/a11y.js';
|
|
6
6
|
import { sourceFor, formatSource } from '../dom/source.js';
|
|
7
7
|
import { themeReport } from '../dom/theme.js';
|
|
8
|
-
import { refs } from '../dom/refs.js';
|
|
8
|
+
import { echoRef, refs } from '../dom/refs.js';
|
|
9
9
|
import { isButton, isInput } from '../dom/realm.js';
|
|
10
10
|
import { hitTestOccluder } from '../dom/occlusion.js';
|
|
11
11
|
import { readStorage } from '../observers/storage.js';
|
|
@@ -19,16 +19,16 @@ import { scrollContainer } from '../actions/scroll.js';
|
|
|
19
19
|
/** Query param appended on a hard reload to bypass the browser cache. */
|
|
20
20
|
export const RELOAD_CACHE_BUST_PARAM = '_reticle_reload';
|
|
21
21
|
function str(value) {
|
|
22
|
-
return typeof value
|
|
22
|
+
return 'string' === typeof value ? value : undefined;
|
|
23
23
|
}
|
|
24
24
|
function num(value) {
|
|
25
|
-
return typeof value
|
|
25
|
+
return 'number' === typeof value ? value : undefined;
|
|
26
26
|
}
|
|
27
27
|
function record(value) {
|
|
28
|
-
return typeof value
|
|
28
|
+
return 'object' === typeof value && value !== null ? value : {};
|
|
29
29
|
}
|
|
30
30
|
function sourceLocation(value) {
|
|
31
|
-
if (typeof value !== 'object' ||
|
|
31
|
+
if (typeof value !== 'object' || null === value)
|
|
32
32
|
return undefined;
|
|
33
33
|
const obj = value;
|
|
34
34
|
if (typeof obj['file'] !== 'string' || typeof obj['line'] !== 'number')
|
|
@@ -49,15 +49,20 @@ function queryFromArgs(args) {
|
|
|
49
49
|
component: str(args['component']),
|
|
50
50
|
scope: str(args['scope']),
|
|
51
51
|
attrs: Array.isArray(args['attrs'])
|
|
52
|
-
? args['attrs'].filter((a) => typeof a
|
|
52
|
+
? args['attrs'].filter((a) => 'string' === typeof a)
|
|
53
53
|
: undefined,
|
|
54
54
|
source: sourceLocation(args['source']),
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
function inspect(ref) {
|
|
58
58
|
const el = refs.resolve(ref);
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// THROW, exactly as executeAction does. Returning an error payload made it a SUCCESSFUL command
|
|
60
|
+
// result, which the server then handed to reticle_inspect's outputSchema (ref/role/name/states/
|
|
61
|
+
// visible all required) — so the MCP layer answered -32602 Output validation error for the most
|
|
62
|
+
// ordinary thing that follows a click. The wording matches actions.ts because the server's
|
|
63
|
+
// recovery table keys off /no longer resolves to an element/ to attach the stale-ref recovery.
|
|
64
|
+
if (null === el)
|
|
65
|
+
throw new Error(`ref '${echoRef(ref)}' no longer resolves to an element`);
|
|
61
66
|
const rect = el.getBoundingClientRect();
|
|
62
67
|
const component = identifyComponent(el);
|
|
63
68
|
const view = el.ownerDocument.defaultView;
|
|
@@ -104,7 +109,7 @@ function isOccluded(el, rect) {
|
|
|
104
109
|
}
|
|
105
110
|
/** Narrowing guard: an adapter returned a ComponentStateResult (has a boolean `ok`). */
|
|
106
111
|
function isComponentStateResult(value) {
|
|
107
|
-
return (typeof value
|
|
112
|
+
return ('object' === typeof value && value !== null && 'ok' in value && 'boolean' === typeof value.ok);
|
|
108
113
|
}
|
|
109
114
|
const COMPONENT_UNAVAILABLE = {
|
|
110
115
|
ok: false,
|
|
@@ -158,7 +163,7 @@ function readState(ref, store, path, depth) {
|
|
|
158
163
|
result.truncation = truncation;
|
|
159
164
|
if (ref !== undefined && ref.length > 0) {
|
|
160
165
|
const el = refs.resolve(ref);
|
|
161
|
-
if (
|
|
166
|
+
if (null === el) {
|
|
162
167
|
result.component = COMPONENT_UNAVAILABLE;
|
|
163
168
|
}
|
|
164
169
|
else {
|
|
@@ -184,11 +189,11 @@ function listAnimations() {
|
|
|
184
189
|
return { animations };
|
|
185
190
|
}
|
|
186
191
|
export function resolveNavigationUrl(rawUrl, baseUrl) {
|
|
187
|
-
if (rawUrl.length
|
|
192
|
+
if (0 === rawUrl.length || rawUrl.length > TRANSPORT_LIMITS.MAX_URL_LENGTH)
|
|
188
193
|
return null;
|
|
189
194
|
try {
|
|
190
195
|
const url = new URL(rawUrl, baseUrl);
|
|
191
|
-
return
|
|
196
|
+
return 'http:' === url.protocol || 'https:' === url.protocol ? url.toString() : null;
|
|
192
197
|
}
|
|
193
198
|
catch {
|
|
194
199
|
return null;
|
|
@@ -203,14 +208,14 @@ export function createCommandRegistry() {
|
|
|
203
208
|
}));
|
|
204
209
|
reg.set(ReticleCommand.QUERY, (args) => {
|
|
205
210
|
const limit = args['limit'];
|
|
206
|
-
return runQuery(queryFromArgs(args), typeof limit
|
|
211
|
+
return runQuery(queryFromArgs(args), 'number' === typeof limit ? limit : undefined);
|
|
207
212
|
});
|
|
208
213
|
reg.set(ReticleCommand.MATCH, (args) => matchQuery(ElementQuerySchema.parse(record(args['query'])), str(args['state'])));
|
|
209
214
|
reg.set(ReticleCommand.ACT, (args) => {
|
|
210
215
|
const action = str(args['action']) ?? '';
|
|
211
216
|
if (action === ActionType.WEBMCP) {
|
|
212
217
|
const inner = record(args['args']);
|
|
213
|
-
return dispatchWebMcp(str(inner['tool']) ?? '', record(inner['params']), inner[DANGEROUS_ACTION_CONFIRM_ARG]
|
|
218
|
+
return dispatchWebMcp(str(inner['tool']) ?? '', record(inner['params']), true === inner[DANGEROUS_ACTION_CONFIRM_ARG]);
|
|
214
219
|
}
|
|
215
220
|
return executeAction(str(args['ref']) ?? '', action, record(args['args']));
|
|
216
221
|
});
|
|
@@ -218,14 +223,14 @@ export function createCommandRegistry() {
|
|
|
218
223
|
reg.set(ReticleCommand.INSPECT, (args) => inspect(str(args['ref']) ?? ''));
|
|
219
224
|
reg.set(ReticleCommand.ANIMATIONS, () => listAnimations());
|
|
220
225
|
reg.set(ReticleCommand.CLOCK, (args) => {
|
|
221
|
-
if (args['reset']
|
|
226
|
+
if (true === args['reset']) {
|
|
222
227
|
resetClock();
|
|
223
228
|
}
|
|
224
229
|
else {
|
|
225
|
-
if (args['freeze']
|
|
230
|
+
if (true === args['freeze'])
|
|
226
231
|
freezeClock();
|
|
227
232
|
const adv = args['advanceMs'];
|
|
228
|
-
if (typeof adv
|
|
233
|
+
if ('number' === typeof adv)
|
|
229
234
|
advanceClock(adv);
|
|
230
235
|
}
|
|
231
236
|
return { frozen: isClockFrozen() };
|
|
@@ -233,24 +238,24 @@ export function createCommandRegistry() {
|
|
|
233
238
|
reg.set(ReticleCommand.STATE_READ, (args) => readState(str(args['ref']), str(args['store']), str(args['path']), num(args['depth'])));
|
|
234
239
|
reg.set(ReticleCommand.STORAGE_READ, (args) => readStorage(str(args['area'])));
|
|
235
240
|
reg.set(ReticleCommand.CAPABILITIES, () => getCapabilities());
|
|
236
|
-
reg.set(ReticleCommand.CAPTURE, (args) => captureDesktopWindow(args['fullPage']
|
|
241
|
+
reg.set(ReticleCommand.CAPTURE, (args) => captureDesktopWindow(true === args['fullPage']));
|
|
237
242
|
reg.set(ReticleCommand.SCROLL, (args) => {
|
|
238
243
|
const dy = args['dy'];
|
|
239
244
|
const fraction = args['fraction'];
|
|
240
|
-
return scrollContainer(str(args['ref']), typeof dy
|
|
245
|
+
return scrollContainer(str(args['ref']), 'number' === typeof dy ? dy : undefined, 'number' === typeof fraction ? fraction : undefined);
|
|
241
246
|
});
|
|
242
247
|
reg.set(ReticleCommand.NAVIGATE, (args) => {
|
|
243
248
|
const rawUrl = str(args['url']);
|
|
244
|
-
if (rawUrl === undefined || rawUrl.length
|
|
249
|
+
if (rawUrl === undefined || 0 === rawUrl.length)
|
|
245
250
|
return { ok: false, reason: 'url required' };
|
|
246
251
|
const url = resolveNavigationUrl(rawUrl, window.location.href);
|
|
247
|
-
if (
|
|
252
|
+
if (null === url)
|
|
248
253
|
return { ok: false, reason: 'only http(s) navigation is allowed' };
|
|
249
254
|
window.location.assign(url);
|
|
250
255
|
return { ok: true, url };
|
|
251
256
|
});
|
|
252
257
|
reg.set(ReticleCommand.REFRESH, (args) => {
|
|
253
|
-
if (args['hard']
|
|
258
|
+
if (true === args['hard']) {
|
|
254
259
|
// Hard reload: navigate to self with a cache-busting param then replace history.
|
|
255
260
|
const url = new URL(window.location.href);
|
|
256
261
|
url.searchParams.set(RELOAD_CACHE_BUST_PARAM, String(Date.now()));
|
|
@@ -45,6 +45,12 @@ export interface ReticleConnectOptions {
|
|
|
45
45
|
* and the TypeScript cast that needed shipped into a `.jsx` file and broke the build.
|
|
46
46
|
*/
|
|
47
47
|
root?: string;
|
|
48
|
+
/**
|
|
49
|
+
* The SDK's own package version, so a version-skewed pair against the daemon can name itself
|
|
50
|
+
* rather than surfacing as a bare `-32000`. Build plugins supply it automatically; pass it here
|
|
51
|
+
* only for a hand-wired connect that has no plugin.
|
|
52
|
+
*/
|
|
53
|
+
sdkVersion?: string;
|
|
48
54
|
/** Presenter mode: glow border, animated cursor, click/hover effects, narration HUD. */
|
|
49
55
|
present?: boolean;
|
|
50
56
|
/** Per-action pacing (ms) in presenter mode so a human can follow. Default 450. */
|
package/dist/dom/a11y.js
CHANGED
|
@@ -45,15 +45,15 @@ function inputRole(input) {
|
|
|
45
45
|
const type = input.type.toLowerCase();
|
|
46
46
|
if (INPUT_TEXT_TYPES.has(type))
|
|
47
47
|
return 'textbox';
|
|
48
|
-
if (
|
|
48
|
+
if ('checkbox' === type)
|
|
49
49
|
return 'checkbox';
|
|
50
|
-
if (
|
|
50
|
+
if ('radio' === type)
|
|
51
51
|
return 'radio';
|
|
52
|
-
if (
|
|
52
|
+
if ('range' === type)
|
|
53
53
|
return 'slider';
|
|
54
|
-
if (
|
|
54
|
+
if ('number' === type)
|
|
55
55
|
return 'spinbutton';
|
|
56
|
-
if (
|
|
56
|
+
if ('submit' === type || 'button' === type || 'reset' === type)
|
|
57
57
|
return 'button';
|
|
58
58
|
return 'textbox';
|
|
59
59
|
}
|
|
@@ -115,7 +115,7 @@ function collapse(text) {
|
|
|
115
115
|
}
|
|
116
116
|
function labelledByText(el) {
|
|
117
117
|
const ids = el.getAttribute('aria-labelledby');
|
|
118
|
-
if (
|
|
118
|
+
if (null === ids)
|
|
119
119
|
return null;
|
|
120
120
|
const parts = [];
|
|
121
121
|
for (const id of ids.split(/\s+/)) {
|
|
@@ -144,7 +144,7 @@ function textWithoutHidden(node) {
|
|
|
144
144
|
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
145
145
|
return '';
|
|
146
146
|
const el = node;
|
|
147
|
-
if (el.getAttribute('aria-hidden')
|
|
147
|
+
if ('true' === el.getAttribute('aria-hidden'))
|
|
148
148
|
return '';
|
|
149
149
|
let out = '';
|
|
150
150
|
for (const child of el.childNodes)
|
|
@@ -191,9 +191,9 @@ export function getAccessibleName(el) {
|
|
|
191
191
|
}
|
|
192
192
|
function ariaBool(el, attr) {
|
|
193
193
|
const value = el.getAttribute(attr);
|
|
194
|
-
if (
|
|
194
|
+
if (null === value)
|
|
195
195
|
return undefined;
|
|
196
|
-
return
|
|
196
|
+
return 'true' === value;
|
|
197
197
|
}
|
|
198
198
|
/**
|
|
199
199
|
* The set of states relevant to assertions. `visible` is an O(depth) forced-style walk; callers that
|
|
@@ -203,12 +203,12 @@ export function getStates(el, visible = isVisible(el)) {
|
|
|
203
203
|
const states = [ElementState.PRESENT];
|
|
204
204
|
states.push(visible ? ElementState.VISIBLE : ElementState.HIDDEN);
|
|
205
205
|
const disabledProp = (isButton(el) || isInput(el) || isSelect(el) || isTextArea(el)) && el.disabled;
|
|
206
|
-
const disabled = disabledProp || ariaBool(el, 'aria-disabled')
|
|
206
|
+
const disabled = disabledProp || true === ariaBool(el, 'aria-disabled');
|
|
207
207
|
states.push(disabled ? ElementState.DISABLED : ElementState.ENABLED);
|
|
208
|
-
const checkedProp = isInput(el) && (
|
|
209
|
-
if (checkedProp || ariaBool(el, 'aria-checked')
|
|
208
|
+
const checkedProp = isInput(el) && ('checkbox' === el.type || 'radio' === el.type) && el.checked;
|
|
209
|
+
if (checkedProp || true === ariaBool(el, 'aria-checked'))
|
|
210
210
|
states.push(ElementState.CHECKED);
|
|
211
|
-
if (ariaBool(el, 'aria-expanded')
|
|
211
|
+
if (true === ariaBool(el, 'aria-expanded'))
|
|
212
212
|
states.push(ElementState.EXPANDED);
|
|
213
213
|
if (el.ownerDocument.activeElement === el)
|
|
214
214
|
states.push(ElementState.FOCUSED);
|
|
@@ -232,7 +232,7 @@ export function isSensitiveField(el) {
|
|
|
232
232
|
el.getAttribute('aria-label') ?? '',
|
|
233
233
|
];
|
|
234
234
|
const sensitiveAutocomplete = /current-password|new-password|cc-number|cc-csc|one-time-code/i.test(autocomplete);
|
|
235
|
-
return ((isInput(el) && el.type.toLowerCase()
|
|
235
|
+
return ((isInput(el) && 'password' === el.type.toLowerCase()) ||
|
|
236
236
|
sensitiveAutocomplete ||
|
|
237
237
|
identifiers.some(isSensitiveKey));
|
|
238
238
|
}
|
|
@@ -247,19 +247,19 @@ export function getValue(el) {
|
|
|
247
247
|
}
|
|
248
248
|
/** Whether the element's OWN box hides it — one forced-style resolution, no ancestor walk. */
|
|
249
249
|
function selfHidden(el) {
|
|
250
|
-
if (el.getAttribute('aria-hidden')
|
|
250
|
+
if ('true' === el.getAttribute('aria-hidden'))
|
|
251
251
|
return true;
|
|
252
252
|
if (isHtmlElement(el) && el.hidden)
|
|
253
253
|
return true;
|
|
254
254
|
const view = el.ownerDocument.defaultView;
|
|
255
255
|
if (view !== null) {
|
|
256
256
|
const style = view.getComputedStyle(el);
|
|
257
|
-
if (style.display
|
|
258
|
-
style.visibility
|
|
259
|
-
style.visibility
|
|
257
|
+
if ('none' === style.display ||
|
|
258
|
+
'hidden' === style.visibility ||
|
|
259
|
+
'collapse' === style.visibility) {
|
|
260
260
|
return true;
|
|
261
261
|
}
|
|
262
|
-
if (Number.parseFloat(style.opacity || '1')
|
|
262
|
+
if (0 === Number.parseFloat(style.opacity || '1'))
|
|
263
263
|
return true;
|
|
264
264
|
}
|
|
265
265
|
return false;
|
|
@@ -282,7 +282,7 @@ export function isVisible(el, memo) {
|
|
|
282
282
|
// Each cached boolean already folds in that node's own aria-hidden/[hidden]/display/visibility/opacity,
|
|
283
283
|
// so inherited visibility composes by AND up the chain and a sibling short-circuits at the first
|
|
284
284
|
// cached ancestor.
|
|
285
|
-
const result = !selfHidden(el) && (
|
|
285
|
+
const result = !selfHidden(el) && (null === parent || isVisible(parent, memo));
|
|
286
286
|
if (memo !== undefined)
|
|
287
287
|
memo.set(el, result);
|
|
288
288
|
return result;
|
package/dist/dom/auto-anchor.js
CHANGED
|
@@ -32,7 +32,7 @@ export const AnchorStrategy = {
|
|
|
32
32
|
POSITION: 'position',
|
|
33
33
|
};
|
|
34
34
|
function nonEmpty(s) {
|
|
35
|
-
return typeof s
|
|
35
|
+
return 'string' === typeof s && s.length > 0;
|
|
36
36
|
}
|
|
37
37
|
/** Source as a compact `file:line` suffix; basename only to stay terse and path-independent. */
|
|
38
38
|
function sourceTag(source) {
|
package/dist/dom/blind-spots.js
CHANGED
|
@@ -10,7 +10,7 @@ export function classifyCrossOriginFrames(frames) {
|
|
|
10
10
|
}
|
|
11
11
|
/** Probe the live document: count iframes whose document is unreachable (cross-origin). */
|
|
12
12
|
export function countCrossOriginFrames() {
|
|
13
|
-
if (typeof document
|
|
13
|
+
if ('undefined' === typeof document)
|
|
14
14
|
return 0;
|
|
15
15
|
const frames = [];
|
|
16
16
|
for (const frame of document.querySelectorAll('iframe')) {
|
package/dist/dom/chart.js
CHANGED
|
@@ -54,12 +54,12 @@ export function hasNonFiniteCoordinate(value) {
|
|
|
54
54
|
/** True when the geometry encloses no area — no coordinates, or every coordinate pair identical. */
|
|
55
55
|
export function isDegenerate(value) {
|
|
56
56
|
const nums = geometryNumbers(value).filter((n) => Number.isFinite(n));
|
|
57
|
-
if (nums.length
|
|
57
|
+
if (0 === nums.length)
|
|
58
58
|
return true;
|
|
59
59
|
// A polyline of one repeated point draws nothing. Compare pairs, not raw numbers, so a legitimately
|
|
60
60
|
// flat line (varying x, constant y) is NOT flagged — that is a real chart of constant data.
|
|
61
|
-
const xs = nums.filter((_, i) => i % 2
|
|
62
|
-
const ys = nums.filter((_, i) => i % 2
|
|
61
|
+
const xs = nums.filter((_, i) => 0 === i % 2);
|
|
62
|
+
const ys = nums.filter((_, i) => 1 === i % 2);
|
|
63
63
|
const allSame = (arr) => arr.every((v) => v === arr[0]);
|
|
64
64
|
return xs.length > 1 && allSame(xs) && allSame(ys);
|
|
65
65
|
}
|
|
@@ -74,14 +74,14 @@ export function inspectChart(root) {
|
|
|
74
74
|
for (const el of Array.from(root.querySelectorAll(tag))) {
|
|
75
75
|
for (const attr of GEOMETRY_ATTRS) {
|
|
76
76
|
const value = el.getAttribute(attr);
|
|
77
|
-
if (
|
|
77
|
+
if (null === value)
|
|
78
78
|
continue;
|
|
79
79
|
examined += 1;
|
|
80
80
|
const sample = value.length > SAMPLE_MAX ? `${value.slice(0, SAMPLE_MAX)}…` : value;
|
|
81
81
|
if (hasNonFiniteCoordinate(value)) {
|
|
82
82
|
findings.push({ kind: 'non-finite-coordinates', tag, attr, sample });
|
|
83
83
|
}
|
|
84
|
-
else if (value.trim().length
|
|
84
|
+
else if (0 === value.trim().length) {
|
|
85
85
|
findings.push({ kind: 'empty-geometry', tag, attr, sample });
|
|
86
86
|
}
|
|
87
87
|
else if (isDegenerate(value)) {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* (`reticle-tauri`). Absent that, this returns `{ ok: false }` and the tool reports no-provider
|
|
21
21
|
* rather than guessing.
|
|
22
22
|
*/
|
|
23
|
-
|
|
23
|
+
interface CaptureResult {
|
|
24
24
|
ok: boolean;
|
|
25
25
|
/**
|
|
26
26
|
* Filesystem path of the captured PNG, written by the desktop shell.
|
|
@@ -34,3 +34,4 @@ export interface CaptureResult {
|
|
|
34
34
|
reason?: string;
|
|
35
35
|
}
|
|
36
36
|
export declare function captureDesktopWindow(fullPage?: boolean): Promise<CaptureResult>;
|
|
37
|
+
export {};
|
|
@@ -48,8 +48,8 @@ function tauriCapture() {
|
|
|
48
48
|
return undefined;
|
|
49
49
|
const invoke = internals.invoke.bind(internals);
|
|
50
50
|
return async (fullPage) => {
|
|
51
|
-
const path = await invoke(RETICLE_TAURI_CAPTURE_COMMAND, { fullPage:
|
|
52
|
-
return typeof path
|
|
51
|
+
const path = await invoke(RETICLE_TAURI_CAPTURE_COMMAND, { fullPage: true === fullPage });
|
|
52
|
+
return 'string' === typeof path ? path : null;
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
@@ -94,13 +94,13 @@ async function withReticleUiHidden(capture) {
|
|
|
94
94
|
}
|
|
95
95
|
export async function captureDesktopWindow(fullPage = false) {
|
|
96
96
|
const channel = window[RETICLE_IPC_GLOBAL];
|
|
97
|
-
const capture = typeof channel?.capture
|
|
97
|
+
const capture = 'function' === typeof channel?.capture ? channel.capture : tauriCapture();
|
|
98
98
|
if (capture === undefined) {
|
|
99
99
|
return { ok: false, reason: 'no desktop capture helper installed' };
|
|
100
100
|
}
|
|
101
101
|
try {
|
|
102
102
|
const path = await withReticleUiHidden(() => capture(fullPage));
|
|
103
|
-
return typeof path
|
|
103
|
+
return 'string' === typeof path && path.length > 0
|
|
104
104
|
? { ok: true, path }
|
|
105
105
|
: { ok: false, reason: 'capture returned no image' };
|
|
106
106
|
}
|
package/dist/dom/occlusion.js
CHANGED
|
@@ -7,13 +7,13 @@ import { isReticleUi } from './dom-ignore.js';
|
|
|
7
7
|
* we never false-positive an occlusion. Reticle's own HUD is never treated as an occluder.
|
|
8
8
|
*/
|
|
9
9
|
export function hitTestOccluder(el, rect) {
|
|
10
|
-
if (rect.width
|
|
10
|
+
if (0 === rect.width || 0 === rect.height)
|
|
11
11
|
return null;
|
|
12
12
|
const doc = el.ownerDocument;
|
|
13
13
|
if (typeof doc.elementFromPoint !== 'function')
|
|
14
14
|
return null;
|
|
15
15
|
const top = doc.elementFromPoint(rect.left + rect.width / 2, rect.top + rect.height / 2);
|
|
16
|
-
if (
|
|
16
|
+
if (null === top || isReticleUi(top))
|
|
17
17
|
return null;
|
|
18
18
|
const lands = top === el || el.contains(top) || top.contains(el);
|
|
19
19
|
return lands ? null : top;
|