@reticlehq/browser 3.9.0 → 3.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/action-report.d.ts +14 -0
- package/dist/actions/action-report.js +95 -22
- package/dist/actions/actions-dom.d.ts +16 -6
- package/dist/actions/actions-dom.js +94 -46
- package/dist/actions/actions-press.d.ts +10 -0
- package/dist/actions/actions-press.js +146 -2
- package/dist/actions/actions.js +52 -22
- package/dist/actions/insert-text.d.ts +3 -0
- package/dist/actions/insert-text.js +90 -0
- package/dist/actions/scroll.d.ts +1 -1
- package/dist/actions/scroll.js +25 -1
- package/dist/actions/value-input.d.ts +2 -0
- package/dist/actions/value-input.js +30 -9
- package/dist/commands/commands.js +82 -6
- package/dist/connect-options.d.ts +11 -1
- package/dist/connect-options.js +27 -1
- package/dist/dom/a11y.js +4 -3
- package/dist/dom/chart-geometry.d.ts +7 -0
- package/dist/dom/chart-geometry.js +39 -0
- package/dist/dom/occlusion.js +21 -0
- package/dist/dom/query-finders.d.ts +1 -1
- package/dist/dom/query-finders.js +10 -7
- package/dist/dom/query.d.ts +1 -0
- package/dist/dom/query.js +78 -51
- package/dist/dom/scope-roots.d.ts +8 -0
- package/dist/dom/scope-roots.js +44 -0
- package/dist/dom/snapshot.d.ts +3 -0
- package/dist/dom/snapshot.js +9 -2
- package/dist/dom/text-match.js +4 -3
- package/dist/dom/theme.js +6 -7
- package/dist/dom/visibility.d.ts +2 -0
- package/dist/dom/visibility.js +40 -8
- package/dist/observers/canvas.d.ts +1 -0
- package/dist/observers/canvas.js +10 -1
- package/dist/observers/console.js +46 -35
- package/dist/observers/dom.js +28 -4
- package/dist/observers/net-detail/resource-timing.d.ts +12 -0
- package/dist/observers/net-detail/resource-timing.js +69 -0
- package/dist/observers/network.js +12 -45
- package/dist/observers/perf.js +15 -1
- package/dist/presenter/chrome/presenter-verbs.js +1 -0
- package/dist/presenter/presenter-config.d.ts +1 -0
- package/dist/presenter/presenter-config.js +1 -0
- package/dist/presenter/presenter-shell.d.ts +1 -0
- package/dist/presenter/presenter-shell.js +6 -3
- package/dist/presenter/tour/tour.js +4 -0
- package/dist/recorder/recorder.js +82 -39
- package/dist/reticle-inject.js +597 -597
- package/dist/reticle.d.ts +3 -19
- package/dist/reticle.js +74 -108
- package/dist/security/serialization.js +35 -0
- package/dist/storage-keys.js +2 -1
- package/dist/timers/native/native-timers.js +21 -4
- package/dist/transport/bridge-url.d.ts +8 -0
- package/dist/transport/bridge-url.js +52 -0
- package/package.json +10 -10
package/dist/actions/actions.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { ActionType, ActionWarning, clampHoldMs, clickInputProblem, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
|
|
1
|
+
import { ACT_POINT_ARG, ActionType, ActionWarning, actPointProblem, boxFractionFromArgs, clampHoldMs, clickInputProblem, DRAG_TO_POINT_ARG, DANGEROUS_ACTION_CONFIRM_ARG, ElementState, NATIVE_INPUT_ARG, SettleReason, } from '@reticlehq/core';
|
|
2
2
|
import { isHiddenNativeOption } from '../dom/query-finders.js';
|
|
3
|
-
import { isReticleUi } from '../dom/dom-ignore.js';
|
|
3
|
+
import { isReticleUi, isUnobserved } from '../dom/dom-ignore.js';
|
|
4
4
|
import { asSyntheticInput } from './synthetic/synthetic-input.js';
|
|
5
5
|
import { echoRef, refs } from '../dom/addressing/refs.js';
|
|
6
6
|
import { requiresDangerousConfirmation, assertOutwardAllowed, assertPaymentEntryAllowed, submitControlFor, } from './danger-context.js';
|
|
7
|
-
import { assertEditable, assertNotRichText, isAutofillSafeTarget, setNativeValue, } from './value-input.js';
|
|
7
|
+
import { assertEditable, assertNotEditorTextArea, assertNotRichText, isAutofillSafeTarget, isRichText, setNativeValue, } from './value-input.js';
|
|
8
8
|
import { getRole, getStates } from '../dom/a11y.js';
|
|
9
9
|
import { isVisible } from '../dom/visibility.js';
|
|
10
10
|
import { selectInCombobox } from './select-combobox.js';
|
|
@@ -14,7 +14,7 @@ import { asSettleBudget, nativeSetTimeout, settle } from '../timers/native/nativ
|
|
|
14
14
|
import { publishSettledField, watchActedField } from '../observers/field.js';
|
|
15
15
|
import { AppearedText } from './appeared-text.js';
|
|
16
16
|
import { alreadyAtCheckedState, assertAriaToggleLanded, isAriaToggle } from './check-state.js';
|
|
17
|
-
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, } from './actions-press.js';
|
|
17
|
+
import { focusedOrDocument, isReflessDocumentPress, pressCode, pressKey, pressModifiers, pressKeys, holdKey, pressCombo, dispatchKeypress, closeModalOnEscape, keyboardEvent, runKeydownDefault, runKeyupDefault, SPACE_KEY, } from './actions-press.js';
|
|
18
18
|
import { anchorOf, result, visibleCanvases, } from './action-report.js';
|
|
19
19
|
function asString(value, fallback = '') {
|
|
20
20
|
return 'string' === typeof value ? value : fallback;
|
|
@@ -34,6 +34,7 @@ const FILL_LIKE = new Set([
|
|
|
34
34
|
ActionType.TYPE,
|
|
35
35
|
ActionType.CLEAR,
|
|
36
36
|
ActionType.SELECT,
|
|
37
|
+
ActionType.INSERT_TEXT,
|
|
37
38
|
]);
|
|
38
39
|
const isFillLike = (action) => FILL_LIKE.has(action);
|
|
39
40
|
const CLICK_LIKE = new Set([
|
|
@@ -72,7 +73,7 @@ function assertActionAllowed(el, action, args) {
|
|
|
72
73
|
action === ActionType.UNCHECK ||
|
|
73
74
|
action === ActionType.DRAG ||
|
|
74
75
|
action === ActionType.SUBMIT ||
|
|
75
|
-
(action === ActionType.PRESS && 'Enter' === pressKey(args));
|
|
76
|
+
(action === ActionType.PRESS && ('Enter' === pressKey(args) || SPACE_KEY === pressKey(args)));
|
|
76
77
|
const dragTarget = action === ActionType.DRAG ? refs.resolve(dragTargetRef(args)) : null;
|
|
77
78
|
const submitter = action === ActionType.PRESS && 'Enter' === pressKey(args) ? submitControlFor(el) : null;
|
|
78
79
|
if (canTrigger) {
|
|
@@ -100,7 +101,7 @@ function optionValueFor(options, named) {
|
|
|
100
101
|
const byLabel = options.filter((option) => option.label.trim() === named.trim());
|
|
101
102
|
return 1 === byLabel.length ? (byLabel[0]?.value ?? named) : named;
|
|
102
103
|
}
|
|
103
|
-
import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, dragElement, mouseEventFor, assertNotLeaving, scrollIntoViewNow, clickInputOf, } from './actions-dom.js';
|
|
104
|
+
import { NO_GEOMETRY, fireClickSequence, fireTapSequence, clickGeometry, firePointer, firePointerNonBubbling, leavePreviousHover, dragElement, mouseEventFor, assertNotLeaving, scrollIntoViewNow, clickInputOf, coordsIn, } from './actions-dom.js';
|
|
104
105
|
import { scrollFrom } from './scroll.js';
|
|
105
106
|
import { clickHitsNoControl } from './click-target.js';
|
|
106
107
|
function enabledOf(el) {
|
|
@@ -110,6 +111,9 @@ function valueOf(el) {
|
|
|
110
111
|
if (isInput(el) || isTextArea(el) || isSelect(el)) {
|
|
111
112
|
return el.value;
|
|
112
113
|
}
|
|
114
|
+
if (isRichText(el)) {
|
|
115
|
+
return el.textContent ?? '';
|
|
116
|
+
}
|
|
113
117
|
return undefined;
|
|
114
118
|
}
|
|
115
119
|
function activeRef(el) {
|
|
@@ -122,11 +126,11 @@ function activeRef(el) {
|
|
|
122
126
|
async function dispatchFor(el, action, args) {
|
|
123
127
|
if (ActionType.CLICK === action) {
|
|
124
128
|
const hold = clampHoldMs(args['holdMs']);
|
|
125
|
-
return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() }, undefined, clickInputOf(args));
|
|
129
|
+
return await fireClickSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() }, undefined, clickInputOf(args), boxFractionFromArgs(args, ACT_POINT_ARG));
|
|
126
130
|
}
|
|
127
131
|
if (ActionType.TAP === action) {
|
|
128
132
|
const hold = clampHoldMs(args['holdMs']);
|
|
129
|
-
return await fireTapSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() });
|
|
133
|
+
return await fireTapSequence(el, 0 === hold ? undefined : { ms: hold, sleep, now: () => Date.now() }, boxFractionFromArgs(args, ACT_POINT_ARG));
|
|
130
134
|
}
|
|
131
135
|
return { prevented: await dispatchOther(el, action, args), heldMs: 0 };
|
|
132
136
|
}
|
|
@@ -145,24 +149,26 @@ async function dispatchOther(el, action, args) {
|
|
|
145
149
|
switch (action) {
|
|
146
150
|
case ActionType.DBLCLICK: {
|
|
147
151
|
const input = clickInputOf(args);
|
|
148
|
-
|
|
149
|
-
await fireClickSequence(el, undefined,
|
|
152
|
+
const point = boxFractionFromArgs(args, ACT_POINT_ARG);
|
|
153
|
+
await fireClickSequence(el, undefined, 1, input, point);
|
|
154
|
+
await fireClickSequence(el, undefined, 2, input, point);
|
|
150
155
|
return !asSyntheticInput(() => el.dispatchEvent(mouseEventFor(el, 'dblclick', {
|
|
151
156
|
bubbles: true,
|
|
152
157
|
cancelable: true,
|
|
153
158
|
detail: 2,
|
|
154
159
|
...input.modifiers,
|
|
160
|
+
...coordsIn(el, point),
|
|
155
161
|
})));
|
|
156
162
|
}
|
|
157
163
|
case ActionType.HOVER: {
|
|
158
|
-
const
|
|
159
|
-
const from =
|
|
160
|
-
firePointer(el, 'pointerover', from);
|
|
161
|
-
el.dispatchEvent(mouseEventFor(el, 'mouseover', { bubbles: true, relatedTarget: from }));
|
|
162
|
-
firePointerNonBubbling(el, 'pointerenter', from);
|
|
163
|
-
el.dispatchEvent(mouseEventFor(el, 'mouseenter', { relatedTarget: from }));
|
|
164
|
-
firePointer(el, 'pointermove', from);
|
|
165
|
-
const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true }));
|
|
164
|
+
const coords = coordsIn(el, boxFractionFromArgs(args, ACT_POINT_ARG));
|
|
165
|
+
const from = leavePreviousHover(el);
|
|
166
|
+
firePointer(el, 'pointerover', from, coords);
|
|
167
|
+
el.dispatchEvent(mouseEventFor(el, 'mouseover', { bubbles: true, relatedTarget: from, ...coords }));
|
|
168
|
+
firePointerNonBubbling(el, 'pointerenter', from, coords);
|
|
169
|
+
el.dispatchEvent(mouseEventFor(el, 'mouseenter', { relatedTarget: from, ...coords }));
|
|
170
|
+
firePointer(el, 'pointermove', from, coords);
|
|
171
|
+
const moved = el.dispatchEvent(mouseEventFor(el, 'mousemove', { bubbles: true, cancelable: true, ...coords }));
|
|
166
172
|
const holdMs = clampHoldMs(args['holdMs']);
|
|
167
173
|
if (holdMs > 0) {
|
|
168
174
|
await sleep(holdMs);
|
|
@@ -184,6 +190,7 @@ async function dispatchOther(el, action, args) {
|
|
|
184
190
|
return false;
|
|
185
191
|
}
|
|
186
192
|
case ActionType.FILL:
|
|
193
|
+
assertNotEditorTextArea(el, 'fill');
|
|
187
194
|
if (isInput(el) || isTextArea(el)) {
|
|
188
195
|
if (typeof args['value'] !== 'string') {
|
|
189
196
|
throw new Error("fill requires a string `value` — pass it nested, as args: { value: '…' }. " +
|
|
@@ -198,6 +205,7 @@ async function dispatchOther(el, action, args) {
|
|
|
198
205
|
assertNotRichText(el, 'fill');
|
|
199
206
|
throw new Error(`cannot fill a <${el.tagName.toLowerCase()}>`);
|
|
200
207
|
case ActionType.TYPE:
|
|
208
|
+
assertNotEditorTextArea(el, 'type into');
|
|
201
209
|
if (isInput(el) || isTextArea(el)) {
|
|
202
210
|
if (typeof args['text'] !== 'string') {
|
|
203
211
|
throw new Error("type requires a string `text` — pass it nested, as args: { text: '…' }.");
|
|
@@ -209,10 +217,15 @@ async function dispatchOther(el, action, args) {
|
|
|
209
217
|
assertNotRichText(el, 'type into');
|
|
210
218
|
throw new Error(`cannot type into a <${el.tagName.toLowerCase()}>`);
|
|
211
219
|
case ActionType.CLEAR:
|
|
220
|
+
assertNotEditorTextArea(el, 'clear');
|
|
212
221
|
if (isInput(el) || isTextArea(el)) {
|
|
213
222
|
return setNativeValue(el, '');
|
|
214
223
|
}
|
|
215
224
|
throw new Error(`cannot clear a <${el.tagName.toLowerCase()}>`);
|
|
225
|
+
case ActionType.INSERT_TEXT: {
|
|
226
|
+
const { insertText } = await import('./insert-text.js');
|
|
227
|
+
return insertText(el, args['text']);
|
|
228
|
+
}
|
|
216
229
|
case ActionType.SELECT:
|
|
217
230
|
if (isSelect(el)) {
|
|
218
231
|
if (typeof args['value'] !== 'string') {
|
|
@@ -281,12 +294,16 @@ async function dispatchOther(el, action, args) {
|
|
|
281
294
|
closeModalOnEscape(el, key, down);
|
|
282
295
|
if (down) {
|
|
283
296
|
dispatchKeypress(el, key, code, mods);
|
|
297
|
+
runKeydownDefault(el, key, mods);
|
|
284
298
|
}
|
|
285
299
|
const hold = clampHoldMs(args['holdMs']);
|
|
286
300
|
if (hold > 0) {
|
|
287
301
|
await holdKey(el, key, code, mods, hold);
|
|
288
302
|
}
|
|
289
303
|
asSyntheticInput(() => el.dispatchEvent(keyboardEvent('keyup', { key, code, bubbles: true, ...mods })));
|
|
304
|
+
if (down) {
|
|
305
|
+
runKeyupDefault(el, key, mods);
|
|
306
|
+
}
|
|
290
307
|
return !down;
|
|
291
308
|
}
|
|
292
309
|
case ActionType.SCROLL_INTO_VIEW:
|
|
@@ -298,7 +315,7 @@ async function dispatchOther(el, action, args) {
|
|
|
298
315
|
const dy = asFiniteNumber(args['dy']);
|
|
299
316
|
const dx = asFiniteNumber(args['dx']);
|
|
300
317
|
const fraction = asFiniteNumber(args['fraction']);
|
|
301
|
-
scrollFrom(el, dy, fraction, dx);
|
|
318
|
+
scrollFrom(el, dy, fraction, dx, true === args['wheel']);
|
|
302
319
|
return false;
|
|
303
320
|
}
|
|
304
321
|
case ActionType.UPLOAD: {
|
|
@@ -328,22 +345,35 @@ async function dispatchOther(el, action, args) {
|
|
|
328
345
|
}
|
|
329
346
|
case ActionType.DRAG: {
|
|
330
347
|
const toRef = dragTargetRef(args);
|
|
348
|
+
const points = {
|
|
349
|
+
at: boxFractionFromArgs(args, ACT_POINT_ARG),
|
|
350
|
+
to: boxFractionFromArgs(args, DRAG_TO_POINT_ARG),
|
|
351
|
+
};
|
|
331
352
|
if ('' === toRef) {
|
|
332
|
-
return await dragElement(el, null, args['data']);
|
|
353
|
+
return await dragElement(el, null, args['data'], points, clickInputOf(args));
|
|
333
354
|
}
|
|
334
355
|
const resolved = refs.resolve(toRef);
|
|
335
356
|
if (!isActionTarget(resolved)) {
|
|
336
357
|
throw new Error(`drag target '${echoRef(toRef)}' did not resolve to an element — pass a ref from ` +
|
|
337
358
|
'reticle_snapshot or reticle_query as args.toRef (alias: args.target)');
|
|
338
359
|
}
|
|
339
|
-
return await dragElement(el, resolved, args['data']);
|
|
360
|
+
return await dragElement(el, resolved, args['data'], points, clickInputOf(args));
|
|
340
361
|
}
|
|
341
362
|
default:
|
|
342
363
|
throw new Error(`unknown action '${action}'`);
|
|
343
364
|
}
|
|
344
365
|
}
|
|
366
|
+
function isPageMutation(record) {
|
|
367
|
+
const target = record.target;
|
|
368
|
+
const el = target instanceof Element ? target : target.parentElement;
|
|
369
|
+
return null === el || !isUnobserved(el);
|
|
370
|
+
}
|
|
345
371
|
export async function executeAction(ref, action, args = {}) {
|
|
346
372
|
const named = 0 < ref.length;
|
|
373
|
+
const pointProblem = actPointProblem(action, args);
|
|
374
|
+
if (pointProblem !== undefined) {
|
|
375
|
+
throw new Error(`cannot ${action}: ${pointProblem}`);
|
|
376
|
+
}
|
|
347
377
|
if (ActionType.CLICK === action || ActionType.DBLCLICK === action) {
|
|
348
378
|
const problem = clickInputProblem(action, args);
|
|
349
379
|
if (problem !== undefined) {
|
|
@@ -379,7 +409,7 @@ export async function executeAction(ref, action, args = {}) {
|
|
|
379
409
|
let mutated = 0;
|
|
380
410
|
const said = new AppearedText();
|
|
381
411
|
const obs = new MutationObserver((records) => {
|
|
382
|
-
mutated += records.length;
|
|
412
|
+
mutated += records.filter(isPageMutation).length;
|
|
383
413
|
said.collect(records);
|
|
384
414
|
});
|
|
385
415
|
obs.observe(el.ownerDocument.documentElement, {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { isRichText } from './value-input.js';
|
|
2
|
+
export function insertText(el, text) {
|
|
3
|
+
if (typeof text !== 'string') {
|
|
4
|
+
throw new Error("insertText requires a string `text` — pass it nested, as args: { text: '…' }.");
|
|
5
|
+
}
|
|
6
|
+
if (!isRichText(el)) {
|
|
7
|
+
throw new Error(`cannot insertText into a <${el.tagName.toLowerCase()}> — insertText needs a contenteditable rich-text editor (Tiptap, ProseMirror, Quill), not this surface`);
|
|
8
|
+
}
|
|
9
|
+
if (hasEditContext(el)) {
|
|
10
|
+
throw new Error('cannot insertText into an EditContext editor (Monaco, CodeMirror 6): code that calls updateText does not fire textupdate, so the editor model would keep the old text. Use press for a key.');
|
|
11
|
+
}
|
|
12
|
+
return insertTextInto(el, text);
|
|
13
|
+
}
|
|
14
|
+
function hasEditContext(el) {
|
|
15
|
+
if (!('editContext' in el)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const surface = el;
|
|
19
|
+
return null !== surface.editContext && undefined !== surface.editContext;
|
|
20
|
+
}
|
|
21
|
+
export function insertTextInto(el, text) {
|
|
22
|
+
el.focus();
|
|
23
|
+
const selection = window.getSelection();
|
|
24
|
+
if (selection !== null && !selectionInside(selection, el)) {
|
|
25
|
+
const range = document.createRange();
|
|
26
|
+
range.selectNodeContents(el);
|
|
27
|
+
range.collapse(false);
|
|
28
|
+
selection.removeAllRanges();
|
|
29
|
+
selection.addRange(range);
|
|
30
|
+
}
|
|
31
|
+
const beforeInput = new InputEvent('beforeinput', {
|
|
32
|
+
inputType: 'insertText',
|
|
33
|
+
data: text,
|
|
34
|
+
bubbles: true,
|
|
35
|
+
cancelable: true,
|
|
36
|
+
});
|
|
37
|
+
if (!el.dispatchEvent(beforeInput)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if ('function' !== typeof el.ownerDocument.execCommand) {
|
|
41
|
+
insertAtCaret(el, text);
|
|
42
|
+
const input = new InputEvent('input', {
|
|
43
|
+
inputType: 'insertText',
|
|
44
|
+
data: text,
|
|
45
|
+
bubbles: true,
|
|
46
|
+
cancelable: true,
|
|
47
|
+
});
|
|
48
|
+
return !el.dispatchEvent(input);
|
|
49
|
+
}
|
|
50
|
+
let sawInput = false;
|
|
51
|
+
const recordInput = (event) => {
|
|
52
|
+
if ('input' === event.type) {
|
|
53
|
+
sawInput = true;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
window.addEventListener('input', recordInput, false);
|
|
57
|
+
try {
|
|
58
|
+
if (el.ownerDocument.execCommand('insertText', false, text)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
window.removeEventListener('input', recordInput, false);
|
|
66
|
+
}
|
|
67
|
+
if (sawInput) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
throw new Error('the editor did not accept the text: the native insertText command failed, so the editor model is unchanged. Check that the surface is editable and focused.');
|
|
71
|
+
}
|
|
72
|
+
function insertAtCaret(el, text) {
|
|
73
|
+
const selection = window.getSelection();
|
|
74
|
+
const range = selection !== null && 0 < selection.rangeCount ? selection.getRangeAt(0) : null;
|
|
75
|
+
if (range !== null && el.contains(range.commonAncestorContainer)) {
|
|
76
|
+
range.deleteContents();
|
|
77
|
+
const node = el.ownerDocument.createTextNode(text);
|
|
78
|
+
range.insertNode(node);
|
|
79
|
+
range.setStartAfter(node);
|
|
80
|
+
range.collapse(true);
|
|
81
|
+
selection?.removeAllRanges();
|
|
82
|
+
selection?.addRange(range);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
el.appendChild(el.ownerDocument.createTextNode(text));
|
|
86
|
+
}
|
|
87
|
+
function selectionInside(selection, el) {
|
|
88
|
+
const anchor = selection.anchorNode;
|
|
89
|
+
return anchor !== null && el.contains(anchor);
|
|
90
|
+
}
|
package/dist/actions/scroll.d.ts
CHANGED
|
@@ -14,5 +14,5 @@ interface ScrollResult {
|
|
|
14
14
|
export declare const SCROLL_FRACTION_RANGE_ERROR_PREFIX = "scroll fraction must be between 0 and 1, got ";
|
|
15
15
|
export declare function fractionOutOfRangeMessage(fraction: number): string;
|
|
16
16
|
export declare function scrollContainer(ref: string | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
|
|
17
|
-
export declare function scrollFrom(base: Element | undefined, dy: number | undefined, fraction?: number, dx?: number): ScrollResult;
|
|
17
|
+
export declare function scrollFrom(base: Element | undefined, dy: number | undefined, fraction?: number, dx?: number, wheel?: boolean): ScrollResult;
|
|
18
18
|
export {};
|
package/dist/actions/scroll.js
CHANGED
|
@@ -2,6 +2,7 @@ import { refs } from '../dom/addressing/refs.js';
|
|
|
2
2
|
const FALLBACK_STEP_PX = 400;
|
|
3
3
|
const VIEWPORT_FRACTION = 0.8;
|
|
4
4
|
export const SCROLL_FRACTION_RANGE_ERROR_PREFIX = 'scroll fraction must be between 0 and 1, got ';
|
|
5
|
+
const SCROLL_WHEEL_FRACTION_ERROR = 'scroll with wheel: true cannot use fraction; pass dy or dx';
|
|
5
6
|
export function fractionOutOfRangeMessage(fraction) {
|
|
6
7
|
return `${SCROLL_FRACTION_RANGE_ERROR_PREFIX}${fraction}`;
|
|
7
8
|
}
|
|
@@ -21,7 +22,7 @@ export function scrollContainer(ref, dy, fraction, dx) {
|
|
|
21
22
|
const base = ref !== undefined ? refs.resolve(ref) : null;
|
|
22
23
|
return scrollFrom(base instanceof Element ? base : undefined, dy, fraction, dx);
|
|
23
24
|
}
|
|
24
|
-
export function scrollFrom(base, dy, fraction, dx) {
|
|
25
|
+
export function scrollFrom(base, dy, fraction, dx, wheel) {
|
|
25
26
|
if (fraction !== undefined && (fraction < 0 || fraction > 1 || Number.isNaN(fraction))) {
|
|
26
27
|
throw new RangeError(fractionOutOfRangeMessage(fraction));
|
|
27
28
|
}
|
|
@@ -31,6 +32,26 @@ export function scrollFrom(base, dy, fraction, dx) {
|
|
|
31
32
|
: (document.scrollingElement ?? document.documentElement);
|
|
32
33
|
const before = target.scrollTop;
|
|
33
34
|
const beforeLeft = target.scrollLeft;
|
|
35
|
+
if (true === wheel) {
|
|
36
|
+
if (fraction !== undefined) {
|
|
37
|
+
throw new RangeError(SCROLL_WHEEL_FRACTION_ERROR);
|
|
38
|
+
}
|
|
39
|
+
const deltaY = dy ?? (Math.round(target.clientHeight * VIEWPORT_FRACTION) || FALLBACK_STEP_PX);
|
|
40
|
+
const deltaX = dx ?? 0;
|
|
41
|
+
const wheelTarget = base ?? target;
|
|
42
|
+
const rect = wheelTarget.getBoundingClientRect();
|
|
43
|
+
wheelTarget.dispatchEvent(new WheelEvent('wheel', {
|
|
44
|
+
deltaX,
|
|
45
|
+
deltaY,
|
|
46
|
+
deltaMode: WheelEvent.DOM_DELTA_PIXEL,
|
|
47
|
+
clientX: rect.width > 0 ? rect.left + rect.width / 2 : 0,
|
|
48
|
+
clientY: rect.height > 0 ? rect.top + rect.height / 2 : 0,
|
|
49
|
+
bubbles: true,
|
|
50
|
+
cancelable: true,
|
|
51
|
+
composed: true,
|
|
52
|
+
}));
|
|
53
|
+
return scrollResult(target, before, beforeLeft);
|
|
54
|
+
}
|
|
34
55
|
if (fraction !== undefined) {
|
|
35
56
|
target.scrollTop = Math.round(target.scrollHeight * fraction);
|
|
36
57
|
}
|
|
@@ -44,6 +65,9 @@ export function scrollFrom(base, dy, fraction, dx) {
|
|
|
44
65
|
}
|
|
45
66
|
}
|
|
46
67
|
target.dispatchEvent(new Event('scroll', { bubbles: false }));
|
|
68
|
+
return scrollResult(target, before, beforeLeft);
|
|
69
|
+
}
|
|
70
|
+
function scrollResult(target, before, beforeLeft) {
|
|
47
71
|
const after = target.scrollTop;
|
|
48
72
|
const afterLeft = target.scrollLeft;
|
|
49
73
|
return {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type ActionTarget } from '../dom/realm.js';
|
|
2
2
|
export declare function isAutofillSafeTarget(el: Element, ua?: string): boolean;
|
|
3
3
|
export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string): boolean;
|
|
4
|
+
export declare function isRichText(el: Element): boolean;
|
|
4
5
|
export declare function assertNotRichText(el: ActionTarget, action: string): void;
|
|
6
|
+
export declare function assertNotEditorTextArea(el: ActionTarget, action: string): void;
|
|
5
7
|
export declare function assertEditable(el: HTMLInputElement | HTMLTextAreaElement, action: string): void;
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { captureValueSetter } from '../patching/capture-method.js';
|
|
2
|
-
import { isHtmlElement, valuePrototypeOf } from '../dom/realm.js';
|
|
2
|
+
import { isHtmlElement, isTextArea, valuePrototypeOf } from '../dom/realm.js';
|
|
3
3
|
const PASSWORD_AUTOCOMPLETE = /\b(current|new)-password\b/;
|
|
4
4
|
const NOT_WEBKIT = /Chrome|Chromium|Edg\/|Firefox|Gecko\//;
|
|
5
5
|
export function isAutofillSafeTarget(el, ua = navigator.userAgent) {
|
|
6
|
-
if (!(el instanceof HTMLInputElement))
|
|
6
|
+
if (!(el instanceof HTMLInputElement)) {
|
|
7
7
|
return false;
|
|
8
|
+
}
|
|
8
9
|
const password = 'password' === el.type.toLowerCase() ||
|
|
9
10
|
PASSWORD_AUTOCOMPLETE.test(el.getAttribute('autocomplete') ?? '');
|
|
10
|
-
if (!password || NOT_WEBKIT.test(ua))
|
|
11
|
+
if (!password || NOT_WEBKIT.test(ua)) {
|
|
11
12
|
return false;
|
|
13
|
+
}
|
|
12
14
|
return 600 <= Number(/AppleWebKit\/(\d+)/.exec(ua)?.[1] ?? 0);
|
|
13
15
|
}
|
|
14
16
|
export function setNativeValue(el, value) {
|
|
@@ -24,21 +26,40 @@ export function setNativeValue(el, value) {
|
|
|
24
26
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
25
27
|
return !notPrevented;
|
|
26
28
|
}
|
|
27
|
-
function isRichText(el) {
|
|
29
|
+
export function isRichText(el) {
|
|
28
30
|
const flag = el.getAttribute('contenteditable');
|
|
29
|
-
if ((isHtmlElement(el) && el.isContentEditable) || (null !== flag && 'false' !== flag))
|
|
31
|
+
if ((isHtmlElement(el) && el.isContentEditable) || (null !== flag && 'false' !== flag)) {
|
|
30
32
|
return true;
|
|
31
|
-
|
|
33
|
+
}
|
|
34
|
+
if ('textbox' === el.getAttribute('role')) {
|
|
32
35
|
return true;
|
|
33
|
-
|
|
36
|
+
}
|
|
37
|
+
if (!('editContext' in el)) {
|
|
34
38
|
return false;
|
|
39
|
+
}
|
|
35
40
|
const surface = el;
|
|
36
41
|
return null !== surface.editContext && undefined !== surface.editContext;
|
|
37
42
|
}
|
|
43
|
+
const EDITOR_ROOT = '.monaco-editor, .CodeMirror, .cm-editor';
|
|
44
|
+
function isEditorTextArea(el) {
|
|
45
|
+
if (!isTextArea(el)) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return 'textbox' === el.getAttribute('role') || null !== el.closest(EDITOR_ROOT);
|
|
49
|
+
}
|
|
50
|
+
function refuseEditor(action) {
|
|
51
|
+
throw new Error(`cannot ${action} a contenteditable or EditContext editor; use insertText, or press for a key`);
|
|
52
|
+
}
|
|
38
53
|
export function assertNotRichText(el, action) {
|
|
39
|
-
if (!isRichText(el))
|
|
54
|
+
if (!isRichText(el)) {
|
|
40
55
|
return;
|
|
41
|
-
|
|
56
|
+
}
|
|
57
|
+
refuseEditor(action);
|
|
58
|
+
}
|
|
59
|
+
export function assertNotEditorTextArea(el, action) {
|
|
60
|
+
if (isEditorTextArea(el)) {
|
|
61
|
+
refuseEditor(action);
|
|
62
|
+
}
|
|
42
63
|
}
|
|
43
64
|
export function assertEditable(el, action) {
|
|
44
65
|
if (el.disabled) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ComponentStateReason, ElementQuerySchema, INSPECT_IN_VIEWPORT_FIELD, INSPECT_REVEAL_ARG, MatchArg, ReticleCommand, SnapshotMode, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
|
|
1
|
+
import { ANIMATION_TARGET_ARG, ComponentStateReason, ElementQuerySchema, INSPECT_IN_VIEWPORT_FIELD, INSPECT_REVEAL_ARG, MatchArg, ReticleCommand, SnapshotMode, SESSION_REF, TRANSPORT_LIMITS, selectPath, capDepth, } from '@reticlehq/core';
|
|
2
2
|
import { buildSnapshot } from '../dom/snapshot.js';
|
|
3
3
|
import { paintContextOf } from '../dom/paint-context.js';
|
|
4
|
-
import { matchQuery, runQuery } from '../dom/query.js';
|
|
4
|
+
import { embeddedRootsUnder, matchQuery, runQuery } from '../dom/query.js';
|
|
5
5
|
import { executeAction, executeSequence } from '../actions/actions.js';
|
|
6
6
|
import { describe } from '../dom/a11y.js';
|
|
7
7
|
import { documentHasSourceStamps, sourceFor, formatSource } from '../dom/addressing/source.js';
|
|
@@ -173,23 +173,98 @@ function readState(ref, store, path, depth) {
|
|
|
173
173
|
}
|
|
174
174
|
return result;
|
|
175
175
|
}
|
|
176
|
-
function
|
|
176
|
+
function animationName(animation) {
|
|
177
|
+
const fields = animation;
|
|
178
|
+
const name = str(fields.animationName);
|
|
179
|
+
if (name !== undefined && name.length > 0) {
|
|
180
|
+
return name;
|
|
181
|
+
}
|
|
182
|
+
const transition = str(fields.transitionProperty);
|
|
183
|
+
return transition !== undefined && transition.length > 0 ? transition : undefined;
|
|
184
|
+
}
|
|
185
|
+
function resolveAnimationTarget(target) {
|
|
186
|
+
if (target === undefined) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const byRef = refs.resolve(target);
|
|
190
|
+
if (byRef !== null) {
|
|
191
|
+
const ref = refs.refFor(byRef);
|
|
192
|
+
return {
|
|
193
|
+
report: { target, matched: true, refs: [ref], count: 1 },
|
|
194
|
+
matches: (animationTarget) => animationTarget !== null && refs.refFor(animationTarget) === ref,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (SESSION_REF.test(target)) {
|
|
198
|
+
return {
|
|
199
|
+
report: { target, matched: false, refs: [], count: 0 },
|
|
200
|
+
matches: () => false,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
let elements;
|
|
204
|
+
try {
|
|
205
|
+
const roots = [document];
|
|
206
|
+
const container = document.body ?? document.documentElement;
|
|
207
|
+
if (container !== null) {
|
|
208
|
+
roots.push(...embeddedRootsUnder(container));
|
|
209
|
+
}
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
elements = [];
|
|
212
|
+
for (const root of roots) {
|
|
213
|
+
for (const el of root.querySelectorAll(target)) {
|
|
214
|
+
if (seen.has(el)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
seen.add(el);
|
|
218
|
+
elements.push(el);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return {
|
|
224
|
+
report: { target, matched: false, refs: [], count: 0, invalid: true },
|
|
225
|
+
matches: () => false,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const allRefs = new Set(elements.map((el) => refs.refFor(el)));
|
|
229
|
+
const refsInReport = Array.from(allRefs).slice(0, TRANSPORT_LIMITS.MAX_COLLECTION_ITEMS);
|
|
230
|
+
return {
|
|
231
|
+
report: {
|
|
232
|
+
target,
|
|
233
|
+
matched: elements.length > 0,
|
|
234
|
+
refs: refsInReport,
|
|
235
|
+
count: elements.length,
|
|
236
|
+
...(allRefs.size > refsInReport.length ? { truncated: true } : {}),
|
|
237
|
+
},
|
|
238
|
+
matches: (animationTarget) => animationTarget !== null && allRefs.has(refs.refFor(animationTarget)),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function listAnimations(args = {}) {
|
|
177
242
|
const doc = document;
|
|
243
|
+
const targetFilter = resolveAnimationTarget(str(args[ANIMATION_TARGET_ARG]));
|
|
178
244
|
if (typeof doc.getAnimations !== 'function') {
|
|
179
|
-
return {
|
|
245
|
+
return {
|
|
246
|
+
animations: [],
|
|
247
|
+
...(targetFilter === undefined ? {} : { target: targetFilter.report }),
|
|
248
|
+
};
|
|
180
249
|
}
|
|
181
250
|
const animations = doc.getAnimations().map((a) => {
|
|
182
251
|
const effect = a.effect;
|
|
183
252
|
const timing = effect?.getTiming();
|
|
184
253
|
const target = effect?.target ?? null;
|
|
254
|
+
const name = animationName(a);
|
|
185
255
|
return {
|
|
256
|
+
...(name === undefined ? {} : { name }),
|
|
186
257
|
playState: a.playState,
|
|
187
258
|
currentTime: a.currentTime,
|
|
188
259
|
duration: timing?.duration,
|
|
189
260
|
target: null === target ? null : describe(target),
|
|
261
|
+
...(targetFilter === undefined ? {} : { targetMatches: targetFilter.matches(target) }),
|
|
190
262
|
};
|
|
191
263
|
});
|
|
192
|
-
return {
|
|
264
|
+
return {
|
|
265
|
+
animations,
|
|
266
|
+
...(targetFilter === undefined ? {} : { target: targetFilter.report }),
|
|
267
|
+
};
|
|
193
268
|
}
|
|
194
269
|
export function resolveNavigationUrl(rawUrl, baseUrl) {
|
|
195
270
|
if (0 === rawUrl.length || rawUrl.length > TRANSPORT_LIMITS.MAX_URL_LENGTH) {
|
|
@@ -211,6 +286,7 @@ export function createCommandRegistry() {
|
|
|
211
286
|
includeRoot: true === args['includeRoot'],
|
|
212
287
|
ids: true === args['ids'],
|
|
213
288
|
audit: true === args['audit'],
|
|
289
|
+
charts: true === args['charts'],
|
|
214
290
|
}));
|
|
215
291
|
reg.set(ReticleCommand.QUERY, (args) => {
|
|
216
292
|
const limit = args['limit'];
|
|
@@ -229,7 +305,7 @@ export function createCommandRegistry() {
|
|
|
229
305
|
return result;
|
|
230
306
|
});
|
|
231
307
|
reg.set(ReticleCommand.INSPECT, (args) => inspect(str(args['ref']) ?? '', true === args[INSPECT_REVEAL_ARG]));
|
|
232
|
-
reg.set(ReticleCommand.ANIMATIONS, () => listAnimations());
|
|
308
|
+
reg.set(ReticleCommand.ANIMATIONS, (args) => listAnimations(args));
|
|
233
309
|
reg.set(ReticleCommand.CLOCK, (args) => {
|
|
234
310
|
if (true === args['reset']) {
|
|
235
311
|
resetClock();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HelloInstrumentation, type HelloEnv, type HelloScope, type RedactionConfig } from '@reticlehq/core';
|
|
1
|
+
import { HelloInstrumentation, HudVisibility, type HelloEnv, type HelloScope, type RedactionConfig } from '@reticlehq/core';
|
|
2
2
|
export interface ReticleConnectOptions {
|
|
3
3
|
url?: string;
|
|
4
4
|
session?: string;
|
|
@@ -37,3 +37,13 @@ export declare function helloMode(options: Pick<ReticleConnectOptions, 'env' | '
|
|
|
37
37
|
};
|
|
38
38
|
export declare function shouldBlockProduction(nodeEnv: string | undefined, allowInProduction: boolean): boolean;
|
|
39
39
|
export declare function leaseToken(options: ReticleConnectOptions, current: string | undefined): string | undefined;
|
|
40
|
+
export declare function oneOf<T extends Record<string, string>>(values: T, raw: unknown): T[keyof T] | undefined;
|
|
41
|
+
export declare function reticleParamsFromSearch(search: string): {
|
|
42
|
+
session?: string;
|
|
43
|
+
projectId?: string;
|
|
44
|
+
hud?: HudVisibility;
|
|
45
|
+
};
|
|
46
|
+
export declare function hudWithoutPanel(hud: HudVisibility | undefined): {
|
|
47
|
+
remember?: HudVisibility;
|
|
48
|
+
placed: HudVisibility[];
|
|
49
|
+
};
|
package/dist/connect-options.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HelloInstrumentation, } from '@reticlehq/core';
|
|
1
|
+
import { HelloInstrumentation, HudVisibility, RETICLE_URL_PARAM, } from '@reticlehq/core';
|
|
2
2
|
export function helloMode(options) {
|
|
3
3
|
return {
|
|
4
4
|
...(options.env === undefined ? {} : { env: options.env }),
|
|
@@ -16,3 +16,29 @@ export function leaseToken(options, current) {
|
|
|
16
16
|
}
|
|
17
17
|
return token;
|
|
18
18
|
}
|
|
19
|
+
export function oneOf(values, raw) {
|
|
20
|
+
return Object.values(values).includes(raw) ? raw : undefined;
|
|
21
|
+
}
|
|
22
|
+
export function reticleParamsFromSearch(search) {
|
|
23
|
+
const params = new URLSearchParams(search);
|
|
24
|
+
const out = {};
|
|
25
|
+
const session = params.get(RETICLE_URL_PARAM.SESSION);
|
|
26
|
+
const projectId = params.get(RETICLE_URL_PARAM.PROJECT);
|
|
27
|
+
const hud = oneOf(HudVisibility, params.get(RETICLE_URL_PARAM.HUD));
|
|
28
|
+
if (session !== null && session.length > 0) {
|
|
29
|
+
out.session = session;
|
|
30
|
+
}
|
|
31
|
+
if (projectId !== null && projectId.length > 0) {
|
|
32
|
+
out.projectId = projectId;
|
|
33
|
+
}
|
|
34
|
+
if (hud !== undefined) {
|
|
35
|
+
out.hud = hud;
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
export function hudWithoutPanel(hud) {
|
|
40
|
+
if (hud === undefined) {
|
|
41
|
+
return { placed: [] };
|
|
42
|
+
}
|
|
43
|
+
return { remember: hud, placed: HudVisibility.SHOWN === hud ? [] : [hud] };
|
|
44
|
+
}
|
package/dist/dom/a11y.js
CHANGED
|
@@ -5,7 +5,7 @@ import { refs } from './addressing/refs.js';
|
|
|
5
5
|
import { inspectChart } from './chart.js';
|
|
6
6
|
import { isSensitiveKey } from '../security/serialization.js';
|
|
7
7
|
import { formatSource, sourceFromDom } from './addressing/source.js';
|
|
8
|
-
import { HTML_DETAILS_OPEN_ATTRIBUTE, HTML_DETAILS_TAG, HTML_SUMMARY_TAG, isVisible, } from './visibility.js';
|
|
8
|
+
import { HTML_DETAILS_OPEN_ATTRIBUTE, HTML_DETAILS_TAG, HTML_SUMMARY_TAG, isNonRendered, isVisible, } from './visibility.js';
|
|
9
9
|
const NAME_FROM_CONTENT = new Set([
|
|
10
10
|
'button',
|
|
11
11
|
'link',
|
|
@@ -30,6 +30,7 @@ const NAME_FROM_CONTENT = new Set([
|
|
|
30
30
|
'alert',
|
|
31
31
|
]);
|
|
32
32
|
const INPUT_TEXT_TYPES = new Set(['text', 'email', 'tel', 'url', 'password', '']);
|
|
33
|
+
const INPUT_BUTTON_TYPES = new Set(['submit', 'button', 'reset', 'file', 'image']);
|
|
33
34
|
function inputRole(input) {
|
|
34
35
|
const type = input.type.toLowerCase();
|
|
35
36
|
if ('search' === type) {
|
|
@@ -50,7 +51,7 @@ function inputRole(input) {
|
|
|
50
51
|
if ('number' === type) {
|
|
51
52
|
return 'spinbutton';
|
|
52
53
|
}
|
|
53
|
-
if (
|
|
54
|
+
if (INPUT_BUTTON_TYPES.has(type)) {
|
|
54
55
|
return 'button';
|
|
55
56
|
}
|
|
56
57
|
return 'textbox';
|
|
@@ -182,7 +183,7 @@ function textWithoutHidden(node) {
|
|
|
182
183
|
return '';
|
|
183
184
|
}
|
|
184
185
|
const el = node;
|
|
185
|
-
if ('true' === el.getAttribute('aria-hidden')) {
|
|
186
|
+
if ('true' === el.getAttribute('aria-hidden') || isNonRendered(el)) {
|
|
186
187
|
return '';
|
|
187
188
|
}
|
|
188
189
|
if (isImage(el)) {
|