@reticlehq/browser 2.2.1 → 2.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 +0 -1
- package/dist/actions/actions-dom.js +0 -1
- package/dist/actions/actions.d.ts +3 -1
- package/dist/actions/actions.js +55 -36
- package/dist/actions/scroll.d.ts +0 -1
- package/dist/actions/scroll.js +0 -1
- package/dist/actions/value-input.d.ts +33 -0
- package/dist/actions/value-input.js +63 -0
- package/dist/commands/commands.d.ts +0 -1
- package/dist/commands/commands.js +27 -7
- package/dist/connect-options.d.ts +97 -0
- package/dist/connect-options.js +1 -0
- package/dist/dom/a11y.d.ts +0 -2
- package/dist/dom/a11y.js +54 -24
- package/dist/dom/auto-anchor.d.ts +0 -1
- package/dist/dom/auto-anchor.js +0 -1
- package/dist/dom/blind-spots.d.ts +0 -1
- package/dist/dom/blind-spots.js +0 -1
- package/dist/dom/chart.d.ts +0 -1
- package/dist/dom/chart.js +0 -1
- package/dist/dom/desktop-capture.d.ts +36 -0
- package/dist/dom/desktop-capture.js +113 -0
- package/dist/dom/dom-ignore.d.ts +7 -1
- package/dist/dom/dom-ignore.js +1 -2
- package/dist/dom/occlusion.d.ts +0 -1
- package/dist/dom/occlusion.js +0 -1
- package/dist/dom/query.d.ts +0 -1
- package/dist/dom/query.js +61 -13
- package/dist/dom/realm.d.ts +30 -0
- package/dist/dom/realm.js +58 -0
- package/dist/dom/refs.d.ts +0 -1
- package/dist/dom/refs.js +0 -1
- package/dist/dom/shadow-registry.d.ts +40 -0
- package/dist/dom/shadow-registry.js +165 -0
- package/dist/dom/snapshot.d.ts +2 -2
- package/dist/dom/snapshot.js +21 -4
- package/dist/dom/source.d.ts +0 -1
- package/dist/dom/source.js +0 -1
- package/dist/dom/theme.d.ts +0 -1
- package/dist/dom/theme.js +0 -1
- package/dist/dom/virtualized.d.ts +19 -0
- package/dist/dom/virtualized.js +80 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +2 -3
- package/dist/observers/animation.d.ts +0 -1
- package/dist/observers/animation.js +0 -1
- package/dist/observers/blind-spots.d.ts +16 -4
- package/dist/observers/blind-spots.js +65 -11
- package/dist/observers/console.d.ts +0 -1
- package/dist/observers/console.js +0 -1
- package/dist/observers/dom.d.ts +0 -2
- package/dist/observers/dom.js +67 -9
- package/dist/observers/download.d.ts +6 -0
- package/dist/observers/download.js +103 -0
- package/dist/observers/focus.d.ts +0 -1
- package/dist/observers/focus.js +0 -1
- package/dist/observers/frames.d.ts +12 -0
- package/dist/observers/frames.js +67 -0
- package/dist/observers/health.d.ts +0 -1
- package/dist/observers/health.js +30 -2
- package/dist/observers/install-all.d.ts +5 -0
- package/dist/observers/install-all.js +63 -0
- package/dist/observers/ipc.d.ts +22 -0
- package/dist/observers/ipc.js +121 -0
- package/dist/observers/network-body.d.ts +17 -1
- package/dist/observers/network-body.js +26 -1
- package/dist/observers/network-redact.d.ts +0 -1
- package/dist/observers/network-redact.js +6 -3
- package/dist/observers/network-stream.d.ts +23 -0
- package/dist/observers/network-stream.js +102 -0
- package/dist/observers/network.d.ts +11 -1
- package/dist/observers/network.js +7 -1
- package/dist/observers/perf.d.ts +0 -1
- package/dist/observers/perf.js +0 -1
- package/dist/observers/route.d.ts +0 -1
- package/dist/observers/route.js +0 -1
- package/dist/observers/scroll.d.ts +0 -1
- package/dist/observers/scroll.js +0 -1
- package/dist/observers/sdk-failure.d.ts +28 -0
- package/dist/observers/sdk-failure.js +60 -0
- package/dist/observers/state.d.ts +0 -1
- package/dist/observers/state.js +0 -1
- package/dist/observers/storage.d.ts +0 -1
- package/dist/observers/storage.js +0 -1
- package/dist/observers/types.d.ts +0 -1
- package/dist/observers/types.js +0 -1
- package/dist/patching/capture-method.d.ts +0 -1
- package/dist/patching/capture-method.js +0 -1
- package/dist/presenter/overlay.d.ts +0 -1
- package/dist/presenter/overlay.js +0 -1
- package/dist/presenter/presenter-config.d.ts +0 -1
- package/dist/presenter/presenter-config.js +0 -1
- package/dist/presenter/presenter-controls.d.ts +0 -1
- package/dist/presenter/presenter-controls.js +0 -1
- package/dist/presenter/presenter-effects.d.ts +0 -1
- package/dist/presenter/presenter-effects.js +0 -1
- package/dist/presenter/presenter-glow.d.ts +0 -1
- package/dist/presenter/presenter-glow.js +0 -1
- package/dist/presenter/presenter-log.d.ts +0 -1
- package/dist/presenter/presenter-log.js +0 -1
- package/dist/presenter/presenter-run-state.d.ts +0 -1
- package/dist/presenter/presenter-run-state.js +0 -1
- package/dist/presenter/presenter-styles.d.ts +0 -1
- package/dist/presenter/presenter-styles.js +0 -1
- package/dist/presenter/presenter-tally.d.ts +0 -1
- package/dist/presenter/presenter-tally.js +0 -1
- package/dist/presenter/presenter-test-helpers.d.ts +0 -1
- package/dist/presenter/presenter-test-helpers.js +0 -1
- package/dist/presenter/presenter-verbs.d.ts +0 -1
- package/dist/presenter/presenter-verbs.js +0 -1
- package/dist/presenter/presenter.d.ts +0 -1
- package/dist/presenter/presenter.js +0 -1
- package/dist/recorder/recorder-styles.d.ts +0 -1
- package/dist/recorder/recorder-styles.js +0 -1
- package/dist/recorder/recorder.d.ts +0 -1
- package/dist/recorder/recorder.js +0 -1
- package/dist/registry/adapters.d.ts +0 -1
- package/dist/registry/adapters.js +0 -1
- package/dist/registry/capabilities.d.ts +2 -1
- package/dist/registry/capabilities.js +16 -1
- package/dist/registry/commit-and-signal.d.ts +0 -1
- package/dist/registry/commit-and-signal.js +0 -1
- package/dist/registry/domains.d.ts +0 -1
- package/dist/registry/domains.js +0 -1
- package/dist/registry/emitter.d.ts +0 -1
- package/dist/registry/emitter.js +0 -1
- package/dist/registry/store-adapters.d.ts +141 -7
- package/dist/registry/store-adapters.js +178 -1
- package/dist/registry/stores.d.ts +0 -1
- package/dist/registry/stores.js +8 -1
- package/dist/reticle-presenter-helpers.d.ts +0 -1
- package/dist/reticle-presenter-helpers.js +0 -1
- package/dist/reticle.d.ts +5 -59
- package/dist/reticle.js +37 -33
- package/dist/review/annotator.d.ts +0 -1
- package/dist/review/annotator.js +0 -1
- package/dist/review/mark-anchor.d.ts +0 -1
- package/dist/review/mark-anchor.js +0 -1
- package/dist/security/serialization.d.ts +0 -1
- package/dist/security/serialization.js +0 -1
- package/dist/timers/clock.d.ts +0 -1
- package/dist/timers/clock.js +0 -1
- package/dist/timers/native-console.d.ts +0 -1
- package/dist/timers/native-console.js +0 -1
- package/dist/timers/native-timers.d.ts +0 -1
- package/dist/timers/native-timers.js +0 -1
- package/dist/transport/transport.d.ts +12 -6
- package/dist/transport/transport.js +107 -17
- package/package.json +13 -3
- package/dist/actions/actions-dom.d.ts.map +0 -1
- package/dist/actions/actions-dom.js.map +0 -1
- package/dist/actions/actions.d.ts.map +0 -1
- package/dist/actions/actions.js.map +0 -1
- package/dist/actions/scroll.d.ts.map +0 -1
- package/dist/actions/scroll.js.map +0 -1
- package/dist/commands/commands.d.ts.map +0 -1
- package/dist/commands/commands.js.map +0 -1
- package/dist/dom/a11y.d.ts.map +0 -1
- package/dist/dom/a11y.js.map +0 -1
- package/dist/dom/auto-anchor.d.ts.map +0 -1
- package/dist/dom/auto-anchor.js.map +0 -1
- package/dist/dom/blind-spots.d.ts.map +0 -1
- package/dist/dom/blind-spots.js.map +0 -1
- package/dist/dom/chart.d.ts.map +0 -1
- package/dist/dom/chart.js.map +0 -1
- package/dist/dom/dom-ignore.d.ts.map +0 -1
- package/dist/dom/dom-ignore.js.map +0 -1
- package/dist/dom/occlusion.d.ts.map +0 -1
- package/dist/dom/occlusion.js.map +0 -1
- package/dist/dom/query.d.ts.map +0 -1
- package/dist/dom/query.js.map +0 -1
- package/dist/dom/refs.d.ts.map +0 -1
- package/dist/dom/refs.js.map +0 -1
- package/dist/dom/snapshot.d.ts.map +0 -1
- package/dist/dom/snapshot.js.map +0 -1
- package/dist/dom/source.d.ts.map +0 -1
- package/dist/dom/source.js.map +0 -1
- package/dist/dom/theme.d.ts.map +0 -1
- package/dist/dom/theme.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/observers/animation.d.ts.map +0 -1
- package/dist/observers/animation.js.map +0 -1
- package/dist/observers/blind-spots.d.ts.map +0 -1
- package/dist/observers/blind-spots.js.map +0 -1
- package/dist/observers/console.d.ts.map +0 -1
- package/dist/observers/console.js.map +0 -1
- package/dist/observers/dom.d.ts.map +0 -1
- package/dist/observers/dom.js.map +0 -1
- package/dist/observers/focus.d.ts.map +0 -1
- package/dist/observers/focus.js.map +0 -1
- package/dist/observers/health.d.ts.map +0 -1
- package/dist/observers/health.js.map +0 -1
- package/dist/observers/network-body.d.ts.map +0 -1
- package/dist/observers/network-body.js.map +0 -1
- package/dist/observers/network-redact.d.ts.map +0 -1
- package/dist/observers/network-redact.js.map +0 -1
- package/dist/observers/network.d.ts.map +0 -1
- package/dist/observers/network.js.map +0 -1
- package/dist/observers/perf.d.ts.map +0 -1
- package/dist/observers/perf.js.map +0 -1
- package/dist/observers/route.d.ts.map +0 -1
- package/dist/observers/route.js.map +0 -1
- package/dist/observers/scroll.d.ts.map +0 -1
- package/dist/observers/scroll.js.map +0 -1
- package/dist/observers/state.d.ts.map +0 -1
- package/dist/observers/state.js.map +0 -1
- package/dist/observers/storage.d.ts.map +0 -1
- package/dist/observers/storage.js.map +0 -1
- package/dist/observers/types.d.ts.map +0 -1
- package/dist/observers/types.js.map +0 -1
- package/dist/patching/capture-method.d.ts.map +0 -1
- package/dist/patching/capture-method.js.map +0 -1
- package/dist/presenter/overlay.d.ts.map +0 -1
- package/dist/presenter/overlay.js.map +0 -1
- package/dist/presenter/presenter-config.d.ts.map +0 -1
- package/dist/presenter/presenter-config.js.map +0 -1
- package/dist/presenter/presenter-controls.d.ts.map +0 -1
- package/dist/presenter/presenter-controls.js.map +0 -1
- package/dist/presenter/presenter-effects.d.ts.map +0 -1
- package/dist/presenter/presenter-effects.js.map +0 -1
- package/dist/presenter/presenter-glow.d.ts.map +0 -1
- package/dist/presenter/presenter-glow.js.map +0 -1
- package/dist/presenter/presenter-log.d.ts.map +0 -1
- package/dist/presenter/presenter-log.js.map +0 -1
- package/dist/presenter/presenter-run-state.d.ts.map +0 -1
- package/dist/presenter/presenter-run-state.js.map +0 -1
- package/dist/presenter/presenter-styles.d.ts.map +0 -1
- package/dist/presenter/presenter-styles.js.map +0 -1
- package/dist/presenter/presenter-tally.d.ts.map +0 -1
- package/dist/presenter/presenter-tally.js.map +0 -1
- package/dist/presenter/presenter-test-helpers.d.ts.map +0 -1
- package/dist/presenter/presenter-test-helpers.js.map +0 -1
- package/dist/presenter/presenter-verbs.d.ts.map +0 -1
- package/dist/presenter/presenter-verbs.js.map +0 -1
- package/dist/presenter/presenter.d.ts.map +0 -1
- package/dist/presenter/presenter.js.map +0 -1
- package/dist/recorder/recorder-styles.d.ts.map +0 -1
- package/dist/recorder/recorder-styles.js.map +0 -1
- package/dist/recorder/recorder.d.ts.map +0 -1
- package/dist/recorder/recorder.js.map +0 -1
- package/dist/registry/adapters.d.ts.map +0 -1
- package/dist/registry/adapters.js.map +0 -1
- package/dist/registry/capabilities.d.ts.map +0 -1
- package/dist/registry/capabilities.js.map +0 -1
- package/dist/registry/commit-and-signal.d.ts.map +0 -1
- package/dist/registry/commit-and-signal.js.map +0 -1
- package/dist/registry/domains.d.ts.map +0 -1
- package/dist/registry/domains.js.map +0 -1
- package/dist/registry/emitter.d.ts.map +0 -1
- package/dist/registry/emitter.js.map +0 -1
- package/dist/registry/store-adapters.d.ts.map +0 -1
- package/dist/registry/store-adapters.js.map +0 -1
- package/dist/registry/stores.d.ts.map +0 -1
- package/dist/registry/stores.js.map +0 -1
- package/dist/reticle-presenter-helpers.d.ts.map +0 -1
- package/dist/reticle-presenter-helpers.js.map +0 -1
- package/dist/reticle.d.ts.map +0 -1
- package/dist/reticle.js.map +0 -1
- package/dist/review/annotator.d.ts.map +0 -1
- package/dist/review/annotator.js.map +0 -1
- package/dist/review/mark-anchor.d.ts.map +0 -1
- package/dist/review/mark-anchor.js.map +0 -1
- package/dist/security/serialization.d.ts.map +0 -1
- package/dist/security/serialization.js.map +0 -1
- package/dist/timers/clock.d.ts.map +0 -1
- package/dist/timers/clock.js.map +0 -1
- package/dist/timers/native-console.d.ts.map +0 -1
- package/dist/timers/native-console.js.map +0 -1
- package/dist/timers/native-timers.d.ts.map +0 -1
- package/dist/timers/native-timers.js.map +0 -1
- package/dist/transport/transport.d.ts.map +0 -1
- package/dist/transport/transport.js.map +0 -1
package/dist/dom/a11y.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { ElementState, REDACTED_VALUE } from '@reticlehq/core';
|
|
2
|
+
import { isButton, isHtmlElement, isImage, isInput, isSelect, isTextArea } from './realm.js';
|
|
2
3
|
import { refs } from './refs.js';
|
|
3
4
|
import { inspectChart } from './chart.js';
|
|
4
5
|
import { isSensitiveKey } from '../security/serialization.js';
|
|
5
6
|
import { formatSource, sourceFromDom } from './source.js';
|
|
6
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Roles whose accessible name comes from their text content (ARIA's `nameFrom: author content`).
|
|
9
|
+
*
|
|
10
|
+
* `radio`, `checkbox`, `row` and `tooltip` were missing, and the gap is not cosmetic: a segmented
|
|
11
|
+
* filter written as `<button role="radio">held</button>` — an extremely ordinary design-system
|
|
12
|
+
* control — reported as a nameless `radio`, so `by: role` + name could not address it at all and an
|
|
13
|
+
* agent had to fall back to a testid the app has no reason to carry. Measured on a shipments console:
|
|
14
|
+
* six filters, six nameless radios, none reachable by name.
|
|
15
|
+
*
|
|
16
|
+
* `listitem`, `status` and `alert` are NOT in the spec's list. They are kept as a deliberate
|
|
17
|
+
* over-approximation — a name computed from content is more useful than no name — and removing them
|
|
18
|
+
* has no measured symptom to justify the risk.
|
|
19
|
+
*/
|
|
7
20
|
const NAME_FROM_CONTENT = new Set([
|
|
8
21
|
'button',
|
|
9
22
|
'link',
|
|
@@ -11,9 +24,13 @@ const NAME_FROM_CONTENT = new Set([
|
|
|
11
24
|
'option',
|
|
12
25
|
'listitem',
|
|
13
26
|
'cell',
|
|
27
|
+
'checkbox',
|
|
14
28
|
'columnheader',
|
|
29
|
+
'radio',
|
|
30
|
+
'row',
|
|
15
31
|
'rowheader',
|
|
16
32
|
'tab',
|
|
33
|
+
'tooltip',
|
|
17
34
|
'menuitem',
|
|
18
35
|
'menuitemcheckbox',
|
|
19
36
|
'menuitemradio',
|
|
@@ -104,12 +121,36 @@ function labelledByText(el) {
|
|
|
104
121
|
for (const id of ids.split(/\s+/)) {
|
|
105
122
|
const ref = el.ownerDocument.getElementById(id);
|
|
106
123
|
if (ref !== null)
|
|
107
|
-
parts.push(collapse(ref
|
|
124
|
+
parts.push(collapse(textWithoutHidden(ref)));
|
|
108
125
|
}
|
|
109
126
|
const joined = parts.join(' ').trim();
|
|
110
127
|
return joined.length > 0 ? joined : null;
|
|
111
128
|
}
|
|
112
129
|
/** Accessible name via a practical subset of the accname algorithm. */
|
|
130
|
+
/** Accessible name via a practical subset of the accname algorithm. */
|
|
131
|
+
/**
|
|
132
|
+
* Text content with `aria-hidden` subtrees removed.
|
|
133
|
+
*
|
|
134
|
+
* The accessible-name spec excludes them, and — decisively — so does dom-accessibility-api, which is
|
|
135
|
+
* what `by: role` + name MATCHES against via testing-library. Reading raw `textContent` here made the
|
|
136
|
+
* name we REPORT differ from the name that can be SELECTED: Material UI renders its required-field
|
|
137
|
+
* marker as `<span aria-hidden="true"> *</span>`, so a login field was reported as `"Username *"` and
|
|
138
|
+
* addressable only as `"Username"`. Reporting a name the agent cannot use defeats the purpose of
|
|
139
|
+
* reporting it at all, so both must come from the same rule.
|
|
140
|
+
*/
|
|
141
|
+
function textWithoutHidden(node) {
|
|
142
|
+
if (node.nodeType === Node.TEXT_NODE)
|
|
143
|
+
return node.textContent ?? '';
|
|
144
|
+
if (node.nodeType !== Node.ELEMENT_NODE)
|
|
145
|
+
return '';
|
|
146
|
+
const el = node;
|
|
147
|
+
if (el.getAttribute('aria-hidden') === 'true')
|
|
148
|
+
return '';
|
|
149
|
+
let out = '';
|
|
150
|
+
for (const child of el.childNodes)
|
|
151
|
+
out += textWithoutHidden(child);
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
113
154
|
export function getAccessibleName(el) {
|
|
114
155
|
const labelled = labelledByText(el);
|
|
115
156
|
if (labelled !== null)
|
|
@@ -117,31 +158,29 @@ export function getAccessibleName(el) {
|
|
|
117
158
|
const ariaLabel = el.getAttribute('aria-label');
|
|
118
159
|
if (ariaLabel !== null && ariaLabel.trim().length > 0)
|
|
119
160
|
return ariaLabel.trim();
|
|
120
|
-
if (el
|
|
161
|
+
if (isImage(el)) {
|
|
121
162
|
const alt = el.getAttribute('alt');
|
|
122
163
|
if (alt !== null)
|
|
123
164
|
return alt.trim();
|
|
124
165
|
}
|
|
125
|
-
if (el
|
|
126
|
-
el instanceof HTMLTextAreaElement ||
|
|
127
|
-
el instanceof HTMLSelectElement) {
|
|
166
|
+
if (isInput(el) || isTextArea(el) || isSelect(el)) {
|
|
128
167
|
const labels = el.labels;
|
|
129
168
|
if (labels !== null && labels.length > 0) {
|
|
130
169
|
const text = [...labels]
|
|
131
|
-
.map((l) => collapse(l
|
|
170
|
+
.map((l) => collapse(textWithoutHidden(l)))
|
|
132
171
|
.join(' ')
|
|
133
172
|
.trim();
|
|
134
173
|
if (text.length > 0)
|
|
135
174
|
return text;
|
|
136
175
|
}
|
|
137
|
-
if (el
|
|
176
|
+
if (isInput(el) || isTextArea(el)) {
|
|
138
177
|
const placeholder = el.getAttribute('placeholder');
|
|
139
178
|
if (placeholder !== null && placeholder.trim().length > 0)
|
|
140
179
|
return placeholder.trim();
|
|
141
180
|
}
|
|
142
181
|
}
|
|
143
182
|
if (NAME_FROM_CONTENT.has(getRole(el))) {
|
|
144
|
-
const text = collapse(el
|
|
183
|
+
const text = collapse(textWithoutHidden(el));
|
|
145
184
|
if (text.length > 0)
|
|
146
185
|
return text;
|
|
147
186
|
}
|
|
@@ -163,14 +202,10 @@ function ariaBool(el, attr) {
|
|
|
163
202
|
export function getStates(el, visible = isVisible(el)) {
|
|
164
203
|
const states = [ElementState.PRESENT];
|
|
165
204
|
states.push(visible ? ElementState.VISIBLE : ElementState.HIDDEN);
|
|
166
|
-
const disabledProp = (el
|
|
167
|
-
el instanceof HTMLInputElement ||
|
|
168
|
-
el instanceof HTMLSelectElement ||
|
|
169
|
-
el instanceof HTMLTextAreaElement) &&
|
|
170
|
-
el.disabled;
|
|
205
|
+
const disabledProp = (isButton(el) || isInput(el) || isSelect(el) || isTextArea(el)) && el.disabled;
|
|
171
206
|
const disabled = disabledProp || ariaBool(el, 'aria-disabled') === true;
|
|
172
207
|
states.push(disabled ? ElementState.DISABLED : ElementState.ENABLED);
|
|
173
|
-
const checkedProp = el
|
|
208
|
+
const checkedProp = isInput(el) && (el.type === 'checkbox' || el.type === 'radio') && el.checked;
|
|
174
209
|
if (checkedProp || ariaBool(el, 'aria-checked') === true)
|
|
175
210
|
states.push(ElementState.CHECKED);
|
|
176
211
|
if (ariaBool(el, 'aria-expanded') === true)
|
|
@@ -186,9 +221,7 @@ export function getStates(el, visible = isVisible(el)) {
|
|
|
186
221
|
* flow recorder's fill-value redaction, so recorded flows never persist typed passwords/OTPs/keys.
|
|
187
222
|
*/
|
|
188
223
|
export function isSensitiveField(el) {
|
|
189
|
-
if (!(el
|
|
190
|
-
!(el instanceof HTMLTextAreaElement) &&
|
|
191
|
-
!(el instanceof HTMLSelectElement)) {
|
|
224
|
+
if (!isInput(el) && !isTextArea(el) && !isSelect(el)) {
|
|
192
225
|
return false;
|
|
193
226
|
}
|
|
194
227
|
const autocomplete = el.getAttribute('autocomplete') ?? '';
|
|
@@ -199,14 +232,12 @@ export function isSensitiveField(el) {
|
|
|
199
232
|
el.getAttribute('aria-label') ?? '',
|
|
200
233
|
];
|
|
201
234
|
const sensitiveAutocomplete = /current-password|new-password|cc-number|cc-csc|one-time-code/i.test(autocomplete);
|
|
202
|
-
return ((el
|
|
235
|
+
return ((isInput(el) && el.type.toLowerCase() === 'password') ||
|
|
203
236
|
sensitiveAutocomplete ||
|
|
204
237
|
identifiers.some(isSensitiveKey));
|
|
205
238
|
}
|
|
206
239
|
export function getValue(el) {
|
|
207
|
-
if (el
|
|
208
|
-
el instanceof HTMLTextAreaElement ||
|
|
209
|
-
el instanceof HTMLSelectElement) {
|
|
240
|
+
if (isInput(el) || isTextArea(el) || isSelect(el)) {
|
|
210
241
|
if (isSensitiveField(el))
|
|
211
242
|
return REDACTED_VALUE;
|
|
212
243
|
return el.value;
|
|
@@ -218,7 +249,7 @@ export function getValue(el) {
|
|
|
218
249
|
function selfHidden(el) {
|
|
219
250
|
if (el.getAttribute('aria-hidden') === 'true')
|
|
220
251
|
return true;
|
|
221
|
-
if (el
|
|
252
|
+
if (isHtmlElement(el) && el.hidden)
|
|
222
253
|
return true;
|
|
223
254
|
const view = el.ownerDocument.defaultView;
|
|
224
255
|
if (view !== null) {
|
|
@@ -296,4 +327,3 @@ export function describe(el, memo) {
|
|
|
296
327
|
}
|
|
297
328
|
return base;
|
|
298
329
|
}
|
|
299
|
-
//# sourceMappingURL=a11y.js.map
|
package/dist/dom/auto-anchor.js
CHANGED
|
@@ -11,4 +11,3 @@ export declare function classifyCrossOriginFrames(frames: ReadonlyArray<{
|
|
|
11
11
|
}>): number;
|
|
12
12
|
/** Probe the live document: count iframes whose document is unreachable (cross-origin). */
|
|
13
13
|
export declare function countCrossOriginFrames(): number;
|
|
14
|
-
//# sourceMappingURL=blind-spots.d.ts.map
|
package/dist/dom/blind-spots.js
CHANGED
package/dist/dom/chart.d.ts
CHANGED
package/dist/dom/chart.js
CHANGED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop screenshots, asked of the runtime that owns the window.
|
|
3
|
+
*
|
|
4
|
+
* A browser tab is captured through CDP. A desktop webview has no CDP endpoint, so the pixels have
|
|
5
|
+
* to come from the shell — and the shell is the only party that can produce them CORRECTLY.
|
|
6
|
+
*
|
|
7
|
+
* Capturing a screen *region* instead was tried and rejected: it photographs the glass, so an app
|
|
8
|
+
* window sitting behind the editor yields a picture of the editor, saved as a visual baseline that a
|
|
9
|
+
* later diff would trust. A screenshot tool that can silently return another window's pixels is
|
|
10
|
+
* worse than one that returns nothing — it manufactures exactly the false green Reticle exists to
|
|
11
|
+
* eliminate. Electron's `webContents.capturePage()` reads the window's own backing store instead:
|
|
12
|
+
* correct while occluded, correct while backgrounded, and needing no screen-recording permission.
|
|
13
|
+
*
|
|
14
|
+
* Both desktop runtimes can do this, each through its own shell API — `webContents.capturePage()` on
|
|
15
|
+
* Electron, and on Tauri the platform's own webview snapshot (`WKWebView.takeSnapshot`, WebView2
|
|
16
|
+
* `CapturePreview`, or WebKitGTK's snapshot). None of them reads the screen, so all stay correct with
|
|
17
|
+
* the window occluded, hidden, or running headless with nothing on screen at all.
|
|
18
|
+
*
|
|
19
|
+
* The app opts in with one line in its main process (`@reticlehq/electron/main`) or one Rust command
|
|
20
|
+
* (`reticle-tauri`). Absent that, this returns `{ ok: false }` and the tool reports no-provider
|
|
21
|
+
* rather than guessing.
|
|
22
|
+
*/
|
|
23
|
+
export interface CaptureResult {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Filesystem path of the captured PNG, written by the desktop shell.
|
|
27
|
+
*
|
|
28
|
+
* A path, not the image bytes: the SDK's transport sanitizer caps every string at 64KB, so a
|
|
29
|
+
* base64 PNG came back SILENTLY TRUNCATED and was saved as a "successful" screenshot that no
|
|
30
|
+
* decoder could read. The daemon and the app always share a machine here (the bridge is loopback),
|
|
31
|
+
* so handing over a path keeps the image off the event wire entirely.
|
|
32
|
+
*/
|
|
33
|
+
path?: string;
|
|
34
|
+
reason?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function captureDesktopWindow(fullPage?: boolean): Promise<CaptureResult>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Desktop screenshots, asked of the runtime that owns the window.
|
|
3
|
+
*
|
|
4
|
+
* A browser tab is captured through CDP. A desktop webview has no CDP endpoint, so the pixels have
|
|
5
|
+
* to come from the shell — and the shell is the only party that can produce them CORRECTLY.
|
|
6
|
+
*
|
|
7
|
+
* Capturing a screen *region* instead was tried and rejected: it photographs the glass, so an app
|
|
8
|
+
* window sitting behind the editor yields a picture of the editor, saved as a visual baseline that a
|
|
9
|
+
* later diff would trust. A screenshot tool that can silently return another window's pixels is
|
|
10
|
+
* worse than one that returns nothing — it manufactures exactly the false green Reticle exists to
|
|
11
|
+
* eliminate. Electron's `webContents.capturePage()` reads the window's own backing store instead:
|
|
12
|
+
* correct while occluded, correct while backgrounded, and needing no screen-recording permission.
|
|
13
|
+
*
|
|
14
|
+
* Both desktop runtimes can do this, each through its own shell API — `webContents.capturePage()` on
|
|
15
|
+
* Electron, and on Tauri the platform's own webview snapshot (`WKWebView.takeSnapshot`, WebView2
|
|
16
|
+
* `CapturePreview`, or WebKitGTK's snapshot). None of them reads the screen, so all stay correct with
|
|
17
|
+
* the window occluded, hidden, or running headless with nothing on screen at all.
|
|
18
|
+
*
|
|
19
|
+
* The app opts in with one line in its main process (`@reticlehq/electron/main`) or one Rust command
|
|
20
|
+
* (`reticle-tauri`). Absent that, this returns `{ ok: false }` and the tool reports no-provider
|
|
21
|
+
* rather than guessing.
|
|
22
|
+
*/
|
|
23
|
+
import { RETICLE_IPC_GLOBAL, RETICLE_TAURI_CAPTURE_COMMAND, VisualReason } from '@reticlehq/core';
|
|
24
|
+
import { nativeFrame } from '../timers/native-timers.js';
|
|
25
|
+
import { RETICLE_OVERLAY } from './dom-ignore.js';
|
|
26
|
+
/**
|
|
27
|
+
* What a shell returns when it can photograph the viewport but not the whole scroll height.
|
|
28
|
+
*
|
|
29
|
+
* Only WebKitGTK can render a full document offscreen; `capturePage()` and `takeSnapshot` both give
|
|
30
|
+
* what is composited. Downgrading silently would be the worst option available: the caller asked for
|
|
31
|
+
* the content below the fold, would get an image without it, and would bank it as a baseline that a
|
|
32
|
+
* later diff reports as matching. So the shells say so, and the tool reports the reason.
|
|
33
|
+
*/
|
|
34
|
+
const FULL_PAGE_UNSUPPORTED_MARKER = VisualReason.FULL_PAGE_UNSUPPORTED;
|
|
35
|
+
const TAURI_INTERNALS_GLOBAL = '__TAURI_INTERNALS__';
|
|
36
|
+
/**
|
|
37
|
+
* Tauri's capture path, used when no preload-installed channel exists.
|
|
38
|
+
*
|
|
39
|
+
* Electron can install `RETICLE_IPC_GLOBAL` from its preload, which runs before app code. Tauri has
|
|
40
|
+
* no preload stage at all, so requiring the same global would mean every Tauri app hand-writing a
|
|
41
|
+
* frontend shim — a setup step that silently yields "no screenshots" when forgotten. Invoking the
|
|
42
|
+
* command directly removes the JavaScript side entirely: the app registers one Rust command and the
|
|
43
|
+
* SDK finds it. Absent that command the invoke rejects, and this reports no-provider like any other.
|
|
44
|
+
*/
|
|
45
|
+
function tauriCapture() {
|
|
46
|
+
const internals = window[TAURI_INTERNALS_GLOBAL];
|
|
47
|
+
if (typeof internals?.invoke !== 'function')
|
|
48
|
+
return undefined;
|
|
49
|
+
const invoke = internals.invoke.bind(internals);
|
|
50
|
+
return async (fullPage) => {
|
|
51
|
+
const path = await invoke(RETICLE_TAURI_CAPTURE_COMMAND, { fullPage: fullPage === true });
|
|
52
|
+
return typeof path === 'string' ? path : null;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Take Reticle's own UI out of the shot, then put it back.
|
|
57
|
+
*
|
|
58
|
+
* The shell photographs the composited window, so anything Reticle mounted is IN the picture — and
|
|
59
|
+
* the presenter panel is the worst possible thing to bank into a visual baseline, because it renders
|
|
60
|
+
* live session state: an event tally, a log tail, a run badge. A baseline captured with it is a
|
|
61
|
+
* baseline of Reticle's own UI as much as the app's, and the next `reticle_visual_diff` reports a
|
|
62
|
+
* change that belongs entirely to the observer. That is measuring the instrument.
|
|
63
|
+
*
|
|
64
|
+
* Two frames, not one: the first lets the style change apply, the second lets the compositor present
|
|
65
|
+
* it — capture before that and the shell photographs the pre-hide window anyway. Both are BOUNDED
|
|
66
|
+
* frames, which is load-bearing rather than defensive: a headless Tauri window is hidden, and a
|
|
67
|
+
* hidden webview never fires `requestAnimationFrame` at all, so a raw double-rAF here deadlocked
|
|
68
|
+
* every capture until the command timed out at 8s. Restoration runs in a `finally`, so a capture
|
|
69
|
+
* that throws still leaves the panel on screen for the human watching.
|
|
70
|
+
*/
|
|
71
|
+
async function withReticleUiHidden(capture) {
|
|
72
|
+
// The SAME selector the snapshot layer already uses to keep Reticle's own DOM out of what the
|
|
73
|
+
// agent reads — the presenter panel, the cursor, the glow, and the annotator's "flag a bug"
|
|
74
|
+
// button. One definition: a root that leaked into snapshots once would otherwise leak into
|
|
75
|
+
// baselines the second time someone added one.
|
|
76
|
+
const roots = [...document.querySelectorAll(RETICLE_OVERLAY)];
|
|
77
|
+
const previous = roots.map((el) => el.style.visibility);
|
|
78
|
+
// `visibility`, not `display`: it keeps the element's box, so hiding cannot reflow the app
|
|
79
|
+
// underneath and change the very layout the screenshot is meant to record.
|
|
80
|
+
for (const el of roots)
|
|
81
|
+
el.style.visibility = 'hidden';
|
|
82
|
+
try {
|
|
83
|
+
if (roots.length > 0) {
|
|
84
|
+
await nativeFrame();
|
|
85
|
+
await nativeFrame();
|
|
86
|
+
}
|
|
87
|
+
return await capture();
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
roots.forEach((el, i) => {
|
|
91
|
+
el.style.visibility = previous[i] ?? '';
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function captureDesktopWindow(fullPage = false) {
|
|
96
|
+
const channel = window[RETICLE_IPC_GLOBAL];
|
|
97
|
+
const capture = typeof channel?.capture === 'function' ? channel.capture : tauriCapture();
|
|
98
|
+
if (capture === undefined) {
|
|
99
|
+
return { ok: false, reason: 'no desktop capture helper installed' };
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const path = await withReticleUiHidden(() => capture(fullPage));
|
|
103
|
+
return typeof path === 'string' && path.length > 0
|
|
104
|
+
? { ok: true, path }
|
|
105
|
+
: { ok: false, reason: 'capture returned no image' };
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
109
|
+
return reason.includes(FULL_PAGE_UNSUPPORTED_MARKER)
|
|
110
|
+
? { ok: false, reason: FULL_PAGE_UNSUPPORTED_MARKER }
|
|
111
|
+
: { ok: false, reason };
|
|
112
|
+
}
|
|
113
|
+
}
|
package/dist/dom/dom-ignore.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selectors for Reticle's own presenter overlay (cursor, HUD, glow) + the annotator's "flag a bug"
|
|
3
|
+
* UI (`data-reticle-mark`) — never observed/snapshotted. The annotator mounts by DEFAULT with the
|
|
4
|
+
* presenter, so omitting its selector here leaked a phantom `<button>Flag a bug</button>` into every
|
|
5
|
+
* snapshot and streamed its highlight-box mutations/transitions as if the APP had rendered them.
|
|
6
|
+
*/
|
|
7
|
+
export declare const RETICLE_OVERLAY = "[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark]";
|
|
1
8
|
/** Let the host app add selectors to exclude from snapshots (e.g. its own dev widgets). */
|
|
2
9
|
export declare function setIgnoreSelectors(selectors: string[]): void;
|
|
3
10
|
/** True if the element is part of Reticle's own presenter overlay. */
|
|
@@ -6,4 +13,3 @@ export declare function isReticleOverlay(el: Element): boolean;
|
|
|
6
13
|
export declare function isReticleUi(node: Element | null): boolean;
|
|
7
14
|
/** True if the element should be excluded from snapshots/queries (Reticle overlay or dev overlay). */
|
|
8
15
|
export declare function isIgnored(el: Element): boolean;
|
|
9
|
-
//# sourceMappingURL=dom-ignore.d.ts.map
|
package/dist/dom/dom-ignore.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* presenter, so omitting its selector here leaked a phantom `<button>Flag a bug</button>` into every
|
|
5
5
|
* snapshot and streamed its highlight-box mutations/transitions as if the APP had rendered them.
|
|
6
6
|
*/
|
|
7
|
-
const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark]';
|
|
7
|
+
export const RETICLE_OVERLAY = '[data-reticle-overlay],[data-reticle-cursor],[data-reticle-hud],[data-reticle-glow],[data-reticle-mark]';
|
|
8
8
|
/** Known third-party dev overlays to keep out of snapshots (Agentation, Next dev UI). */
|
|
9
9
|
const DEV_OVERLAYS = '[data-agentation],#__next-build-watcher,nextjs-portal,[data-nextjs-dialog],[data-nextjs-toast]';
|
|
10
10
|
let extraIgnore = '';
|
|
@@ -33,4 +33,3 @@ export function isIgnored(el) {
|
|
|
33
33
|
: `${RETICLE_OVERLAY},${DEV_OVERLAYS}`;
|
|
34
34
|
return el.closest(sel) !== null;
|
|
35
35
|
}
|
|
36
|
-
//# sourceMappingURL=dom-ignore.js.map
|
package/dist/dom/occlusion.d.ts
CHANGED
package/dist/dom/occlusion.js
CHANGED
package/dist/dom/query.d.ts
CHANGED
|
@@ -16,4 +16,3 @@ export declare function matchQuery(query: ElementQuery, state?: ElementState, li
|
|
|
16
16
|
* do — silently put the server back to counting survivors and calling that the answer.
|
|
17
17
|
*/
|
|
18
18
|
export declare function runQuery(query: ElementQuery, limit?: number): QueryResult;
|
|
19
|
-
//# sourceMappingURL=query.d.ts.map
|
package/dist/dom/query.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { queryAllByRole, queryAllByText, queryAllByLabelText, queryAllByPlaceholderText, queryAllByTestId, queryAllByAltText, } from '@testing-library/dom';
|
|
2
2
|
import { DATA_RETICLE_SOURCE_ATTR, ElementState, QueryBy, REDACTED_VALUE, TRANSPORT_LIMITS, } from '@reticlehq/core';
|
|
3
|
+
import { isFrame, isHtmlElement } from './realm.js';
|
|
4
|
+
import { capturedRootOf } from './shadow-registry.js';
|
|
3
5
|
import { describe, getStates, isVisible } from './a11y.js';
|
|
6
|
+
import { isIgnored } from './dom-ignore.js';
|
|
4
7
|
import { isSensitiveKey } from '../security/serialization.js';
|
|
5
8
|
import { getCapabilities } from '../registry/capabilities.js';
|
|
6
9
|
import { identifyComponent } from '../registry/adapters.js';
|
|
@@ -22,11 +25,11 @@ function resolveContainer(scope) {
|
|
|
22
25
|
if (scope === undefined)
|
|
23
26
|
return { container: document.body, scopeMissing: false };
|
|
24
27
|
const byRef = refs.resolve(scope);
|
|
25
|
-
if (byRef
|
|
28
|
+
if (isHtmlElement(byRef))
|
|
26
29
|
return { container: byRef, scopeMissing: false };
|
|
27
30
|
try {
|
|
28
31
|
const found = document.querySelector(scope);
|
|
29
|
-
if (found
|
|
32
|
+
if (isHtmlElement(found))
|
|
30
33
|
return { container: found, scopeMissing: false };
|
|
31
34
|
}
|
|
32
35
|
catch {
|
|
@@ -138,18 +141,56 @@ function findIn(container, query) {
|
|
|
138
141
|
* A closed root is deliberately unreachable — `element.shadowRoot` is null by design, and the SDK
|
|
139
142
|
* reports that as a blind spot rather than pretending to see through it.
|
|
140
143
|
*/
|
|
141
|
-
|
|
144
|
+
/**
|
|
145
|
+
* A same-origin `<iframe>`'s body, or null when the document is unreachable.
|
|
146
|
+
*
|
|
147
|
+
* Cross-origin access THROWS in some engines and yields null in others, so both are handled — and
|
|
148
|
+
* both mean the same thing here: not ours to read. That case is not a silent skip, it is reported
|
|
149
|
+
* separately as a declared blind spot.
|
|
150
|
+
*/
|
|
151
|
+
function readableFrameBody(frame) {
|
|
152
|
+
try {
|
|
153
|
+
return frame.contentDocument?.body ?? null;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** How deep to follow frames-inside-frames before giving up. Real consoles nest one, maybe two. */
|
|
160
|
+
const FRAME_DEPTH_MAX = 3;
|
|
161
|
+
/**
|
|
162
|
+
* Every root beneath the scope that the page can read but `querySelectorAll` will not reach:
|
|
163
|
+
* open shadow roots, and the documents of SAME-ORIGIN iframes.
|
|
164
|
+
*
|
|
165
|
+
* The iframe half was measured missing: a same-origin frame whose text the page could read straight
|
|
166
|
+
* off `contentDocument` returned zero matches from `query`, and coverage said nothing about it — the
|
|
167
|
+
* worst combination, an absent-element answer for content that is right there. Cross-origin frames
|
|
168
|
+
* are a genuinely different case and stay unread; they are declared instead.
|
|
169
|
+
*/
|
|
170
|
+
function embeddedRootsUnder(root) {
|
|
142
171
|
const found = [];
|
|
143
|
-
const walk = (node) => {
|
|
172
|
+
const walk = (node, depth) => {
|
|
144
173
|
for (const el of node.querySelectorAll('*')) {
|
|
145
|
-
const shadow = el.shadowRoot;
|
|
174
|
+
const shadow = el.shadowRoot ?? capturedRootOf(el);
|
|
146
175
|
if (shadow !== null) {
|
|
176
|
+
// A ShadowRoot is a DocumentFragment; the Testing-Library queries accept any HTMLElement-like
|
|
177
|
+
// container, and every call site only reads from it.
|
|
147
178
|
found.push(shadow);
|
|
148
|
-
walk(shadow); // nested web components
|
|
179
|
+
walk(shadow, depth); // nested web components
|
|
180
|
+
continue;
|
|
149
181
|
}
|
|
182
|
+
if (depth >= FRAME_DEPTH_MAX)
|
|
183
|
+
continue;
|
|
184
|
+
if (!isFrame(el))
|
|
185
|
+
continue;
|
|
186
|
+
const body = readableFrameBody(el);
|
|
187
|
+
if (body === null)
|
|
188
|
+
continue; // cross-origin — declared, not searched
|
|
189
|
+
found.push(body);
|
|
190
|
+
walk(body, depth + 1);
|
|
150
191
|
}
|
|
151
192
|
};
|
|
152
|
-
walk(root);
|
|
193
|
+
walk(root, 0);
|
|
153
194
|
return found;
|
|
154
195
|
}
|
|
155
196
|
/**
|
|
@@ -185,16 +226,22 @@ function findCandidates(query) {
|
|
|
185
226
|
for (const el of els) {
|
|
186
227
|
if (seen.has(el))
|
|
187
228
|
continue; // reachable from both host and root — count once
|
|
229
|
+
// Reticle's OWN UI is not part of the app under test. `snapshot` has always excluded it; query
|
|
230
|
+
// did not, so the presenter panel and the annotator answered `by: role` like app controls —
|
|
231
|
+
// measured on a real merchant dashboard, 7 of the 40 buttons an agent could see were ours
|
|
232
|
+
// ("Pause", "End", "Minimise the panel", "Export", "Flag a bug for the agent"). Two of those
|
|
233
|
+
// share a NAME with a real control on that page, so an agent resolving "Export" could drive the
|
|
234
|
+
// observer instead of the app and then reason about the result. Filtered here, at the one
|
|
235
|
+
// collection point, so every entry path (by role/text/testid, shadow roots, scoped) is covered.
|
|
236
|
+
if (isIgnored(el))
|
|
237
|
+
continue;
|
|
188
238
|
seen.add(el);
|
|
189
239
|
out.push(el);
|
|
190
240
|
}
|
|
191
241
|
};
|
|
192
242
|
collect(findIn(container, query));
|
|
193
|
-
for (const
|
|
194
|
-
|
|
195
|
-
// container, and every call site below only reads from it.
|
|
196
|
-
collect(findIn(shadow, query));
|
|
197
|
-
}
|
|
243
|
+
for (const embedded of embeddedRootsUnder(container))
|
|
244
|
+
collect(findIn(embedded, query));
|
|
198
245
|
return { candidates: out, scopeMissing };
|
|
199
246
|
}
|
|
200
247
|
/** Longest attribute value returned; one enormous href must not blow the response budget. */
|
|
@@ -351,6 +398,8 @@ function buildEmptyHint(query) {
|
|
|
351
398
|
const all = container.querySelectorAll(`[${TESTID_ATTR}]`);
|
|
352
399
|
const present = [];
|
|
353
400
|
for (const el of Array.from(all)) {
|
|
401
|
+
if (isIgnored(el))
|
|
402
|
+
continue; // the "what IS here" hint must not advertise Reticle's own UI either
|
|
354
403
|
const id = el.getAttribute(TESTID_ATTR);
|
|
355
404
|
if (id !== null && id.length > 0 && !present.includes(id)) {
|
|
356
405
|
present.push(id);
|
|
@@ -388,4 +437,3 @@ export function runQuery(query, limit) {
|
|
|
388
437
|
}
|
|
389
438
|
return { elements: result.elements, count: result.count, ...scopeFields };
|
|
390
439
|
}
|
|
391
|
-
//# sourceMappingURL=query.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-realm type tests.
|
|
3
|
+
*
|
|
4
|
+
* `instanceof` compares against ONE realm's constructor. An element inside a same-origin `<iframe>`
|
|
5
|
+
* lives in that document's realm, so `frameButton instanceof HTMLElement` — evaluated with the top
|
|
6
|
+
* window's `HTMLElement` — is FALSE for a perfectly ordinary button.
|
|
7
|
+
*
|
|
8
|
+
* That is not theoretical. Once `query` learned to descend into same-origin frames, it returned the
|
|
9
|
+
* frame's button correctly and `act` then rejected the ref with "is not an HTMLElement": found, and
|
|
10
|
+
* unusable. A find you cannot act on is worse than a miss, because it reads as a product bug in the
|
|
11
|
+
* app rather than in the tool.
|
|
12
|
+
*
|
|
13
|
+
* These resolve the constructor from the NODE's own document instead, so an element is measured
|
|
14
|
+
* against its own realm. Same-realm nodes behave exactly as `instanceof` did.
|
|
15
|
+
*/
|
|
16
|
+
export declare const isElement: (n: unknown) => n is Element;
|
|
17
|
+
export declare const isImage: (n: unknown) => n is HTMLImageElement;
|
|
18
|
+
export declare const isHtmlElement: (n: unknown) => n is HTMLElement;
|
|
19
|
+
export declare const isInput: (n: unknown) => n is HTMLInputElement;
|
|
20
|
+
export declare const isTextArea: (n: unknown) => n is HTMLTextAreaElement;
|
|
21
|
+
export declare const isSelect: (n: unknown) => n is HTMLSelectElement;
|
|
22
|
+
export declare const isButton: (n: unknown) => n is HTMLButtonElement;
|
|
23
|
+
export declare const isForm: (n: unknown) => n is HTMLFormElement;
|
|
24
|
+
export declare const isFrame: (n: unknown) => n is HTMLIFrameElement;
|
|
25
|
+
/**
|
|
26
|
+
* The prototype whose native value setter drives `el` — React and friends install their own accessor
|
|
27
|
+
* on the instance, so a value write has to go through the prototype's. It must come from the
|
|
28
|
+
* ELEMENT's realm for the same reason as above.
|
|
29
|
+
*/
|
|
30
|
+
export declare function valuePrototypeOf(el: Element): object | undefined;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-realm type tests.
|
|
3
|
+
*
|
|
4
|
+
* `instanceof` compares against ONE realm's constructor. An element inside a same-origin `<iframe>`
|
|
5
|
+
* lives in that document's realm, so `frameButton instanceof HTMLElement` — evaluated with the top
|
|
6
|
+
* window's `HTMLElement` — is FALSE for a perfectly ordinary button.
|
|
7
|
+
*
|
|
8
|
+
* That is not theoretical. Once `query` learned to descend into same-origin frames, it returned the
|
|
9
|
+
* frame's button correctly and `act` then rejected the ref with "is not an HTMLElement": found, and
|
|
10
|
+
* unusable. A find you cannot act on is worse than a miss, because it reads as a product bug in the
|
|
11
|
+
* app rather than in the tool.
|
|
12
|
+
*
|
|
13
|
+
* These resolve the constructor from the NODE's own document instead, so an element is measured
|
|
14
|
+
* against its own realm. Same-realm nodes behave exactly as `instanceof` did.
|
|
15
|
+
*/
|
|
16
|
+
/** The constructor named `name` in the realm that owns `node`, or undefined if there isn't one. */
|
|
17
|
+
function ctorIn(node, name) {
|
|
18
|
+
if (node === null || typeof node !== 'object')
|
|
19
|
+
return undefined;
|
|
20
|
+
// A Document owns itself; every other node reports an ownerDocument.
|
|
21
|
+
const doc = node.ownerDocument ?? node;
|
|
22
|
+
const view = doc.defaultView;
|
|
23
|
+
if (view === null || view === undefined)
|
|
24
|
+
return undefined;
|
|
25
|
+
return view[name];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* `node instanceof <name>`, evaluated in the node's own realm.
|
|
29
|
+
*
|
|
30
|
+
* Falls back to the ambient constructor when the node has no view — a detached document, or a jsdom
|
|
31
|
+
* fragment — so behaviour outside a browser is unchanged.
|
|
32
|
+
*/
|
|
33
|
+
function isIn(node, name, ambient) {
|
|
34
|
+
const ctor = ctorIn(node, name) ?? ambient;
|
|
35
|
+
return typeof ctor === 'function' && node instanceof ctor;
|
|
36
|
+
}
|
|
37
|
+
export const isElement = (n) => isIn(n, 'Element', typeof Element === 'undefined' ? undefined : Element);
|
|
38
|
+
export const isImage = (n) => isIn(n, 'HTMLImageElement', typeof HTMLImageElement === 'undefined' ? undefined : HTMLImageElement);
|
|
39
|
+
export const isHtmlElement = (n) => isIn(n, 'HTMLElement', typeof HTMLElement === 'undefined' ? undefined : HTMLElement);
|
|
40
|
+
export const isInput = (n) => isIn(n, 'HTMLInputElement', typeof HTMLInputElement === 'undefined' ? undefined : HTMLInputElement);
|
|
41
|
+
export const isTextArea = (n) => isIn(n, 'HTMLTextAreaElement', typeof HTMLTextAreaElement === 'undefined' ? undefined : HTMLTextAreaElement);
|
|
42
|
+
export const isSelect = (n) => isIn(n, 'HTMLSelectElement', typeof HTMLSelectElement === 'undefined' ? undefined : HTMLSelectElement);
|
|
43
|
+
export const isButton = (n) => isIn(n, 'HTMLButtonElement', typeof HTMLButtonElement === 'undefined' ? undefined : HTMLButtonElement);
|
|
44
|
+
export const isForm = (n) => isIn(n, 'HTMLFormElement', typeof HTMLFormElement === 'undefined' ? undefined : HTMLFormElement);
|
|
45
|
+
export const isFrame = (n) => isIn(n, 'HTMLIFrameElement', typeof HTMLIFrameElement === 'undefined' ? undefined : HTMLIFrameElement);
|
|
46
|
+
/**
|
|
47
|
+
* The prototype whose native value setter drives `el` — React and friends install their own accessor
|
|
48
|
+
* on the instance, so a value write has to go through the prototype's. It must come from the
|
|
49
|
+
* ELEMENT's realm for the same reason as above.
|
|
50
|
+
*/
|
|
51
|
+
export function valuePrototypeOf(el) {
|
|
52
|
+
const name = isTextArea(el) ? 'HTMLTextAreaElement' : 'HTMLInputElement';
|
|
53
|
+
const ctor = ctorIn(el, name);
|
|
54
|
+
if (typeof ctor !== 'function')
|
|
55
|
+
return undefined;
|
|
56
|
+
const proto = ctor.prototype;
|
|
57
|
+
return typeof proto === 'object' && proto !== null ? proto : undefined;
|
|
58
|
+
}
|
package/dist/dom/refs.d.ts
CHANGED
package/dist/dom/refs.js
CHANGED