@e2edev/web 0.11.0-canary-20260921180210
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/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +112 -0
- package/dist/.build.json +1 -0
- package/dist/actions.d.ts +23 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +174 -0
- package/dist/actions.js.map +1 -0
- package/dist/attempt-session.d.ts +76 -0
- package/dist/attempt-session.d.ts.map +1 -0
- package/dist/attempt-session.js +290 -0
- package/dist/attempt-session.js.map +1 -0
- package/dist/browser-connection.d.ts +34 -0
- package/dist/browser-connection.d.ts.map +1 -0
- package/dist/browser-connection.js +87 -0
- package/dist/browser-connection.js.map +1 -0
- package/dist/capture-scope.d.ts +21 -0
- package/dist/capture-scope.d.ts.map +1 -0
- package/dist/capture-scope.js +61 -0
- package/dist/capture-scope.js.map +1 -0
- package/dist/cdp-recovery.d.ts +29 -0
- package/dist/cdp-recovery.d.ts.map +1 -0
- package/dist/cdp-recovery.js +84 -0
- package/dist/cdp-recovery.js.map +1 -0
- package/dist/cdp-selectors.d.ts +14 -0
- package/dist/cdp-selectors.d.ts.map +1 -0
- package/dist/cdp-selectors.js +48 -0
- package/dist/cdp-selectors.js.map +1 -0
- package/dist/dialogs.d.ts +37 -0
- package/dist/dialogs.d.ts.map +1 -0
- package/dist/dialogs.js +78 -0
- package/dist/dialogs.js.map +1 -0
- package/dist/engine.d.ts +26 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +166 -0
- package/dist/engine.js.map +1 -0
- package/dist/evaluation.d.ts +11 -0
- package/dist/evaluation.d.ts.map +1 -0
- package/dist/evaluation.js +27 -0
- package/dist/evaluation.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +40 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +117 -0
- package/dist/install.js.map +1 -0
- package/dist/locators.d.ts +74 -0
- package/dist/locators.d.ts.map +1 -0
- package/dist/locators.js +260 -0
- package/dist/locators.js.map +1 -0
- package/dist/observation-capture.d.ts +12 -0
- package/dist/observation-capture.d.ts.map +1 -0
- package/dist/observation-capture.js +93 -0
- package/dist/observation-capture.js.map +1 -0
- package/dist/observation.d.ts +63 -0
- package/dist/observation.d.ts.map +1 -0
- package/dist/observation.js +262 -0
- package/dist/observation.js.map +1 -0
- package/dist/observe.d.ts +51 -0
- package/dist/observe.d.ts.map +1 -0
- package/dist/observe.js +104 -0
- package/dist/observe.js.map +1 -0
- package/dist/operation-budget.d.ts +11 -0
- package/dist/operation-budget.d.ts.map +1 -0
- package/dist/operation-budget.js +40 -0
- package/dist/operation-budget.js.map +1 -0
- package/dist/protected-app.d.ts +32 -0
- package/dist/protected-app.d.ts.map +1 -0
- package/dist/protected-app.js +39 -0
- package/dist/protected-app.js.map +1 -0
- package/dist/read-node.d.ts +170 -0
- package/dist/read-node.d.ts.map +1 -0
- package/dist/read-node.js +823 -0
- package/dist/read-node.js.map +1 -0
- package/dist/refs.d.ts +43 -0
- package/dist/refs.d.ts.map +1 -0
- package/dist/refs.js +85 -0
- package/dist/refs.js.map +1 -0
- package/dist/route-pattern.d.ts +15 -0
- package/dist/route-pattern.d.ts.map +1 -0
- package/dist/route-pattern.js +68 -0
- package/dist/route-pattern.js.map +1 -0
- package/dist/support.d.ts +119 -0
- package/dist/support.d.ts.map +1 -0
- package/dist/support.js +299 -0
- package/dist/support.js.map +1 -0
- package/dist/surface.d.ts +257 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +587 -0
- package/dist/surface.js.map +1 -0
- package/dist/video.d.ts +59 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +115 -0
- package/dist/video.js.map +1 -0
- package/dist/web.d.ts +191 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +354 -0
- package/dist/web.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-page semantic reader executed via locator.evaluate, locator.evaluateAll,
|
|
3
|
+
* and locator.evaluateHandle. Everything below the exported functions is
|
|
4
|
+
* serialized into the page, so it must stay self-contained: no imports, no
|
|
5
|
+
* module-scope references, every constant inside the function body.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* The one definition of a secure field.
|
|
9
|
+
*
|
|
10
|
+
* It is a selector rather than a predicate because it has to be applied from
|
|
11
|
+
* two sides that cannot share code: the in-page reader matches elements
|
|
12
|
+
* against it to mark nodes `secure`, and the engine hands the same string to
|
|
13
|
+
* the screenshot masker. One string means the tree's redaction and the image's
|
|
14
|
+
* redaction cannot describe different sets of elements, which is what the
|
|
15
|
+
* runner's pixel-clearance check relies on.
|
|
16
|
+
*/
|
|
17
|
+
export const SECURE_FIELD_SELECTOR = 'input[type="password" i]';
|
|
18
|
+
/**
|
|
19
|
+
* Where the init script below records closed shadow roots, keyed by host, and
|
|
20
|
+
* where the in-page reader looks for them. The reader is serialized into the
|
|
21
|
+
* page and cannot import this constant, so the literal is repeated inside it;
|
|
22
|
+
* the two must agree, like `SECURE_FIELD_SELECTOR` on both sides of masking.
|
|
23
|
+
*/
|
|
24
|
+
export const CLOSED_SHADOW_ROOTS_KEY = 'e2e.closedShadowRoots';
|
|
25
|
+
/**
|
|
26
|
+
* Context init script that keeps every closed shadow root reachable for the
|
|
27
|
+
* reader. A closed root hides its tree from `element.shadowRoot`, so a checkout
|
|
28
|
+
* button a third-party widget renders that way is on screen for a person yet
|
|
29
|
+
* absent from the walk. `attachShadow` is the one way a script creates such a
|
|
30
|
+
* root; wrapping it before any page script runs records host and root in a
|
|
31
|
+
* WeakMap under a well-known symbol, which the reader consults where it reads
|
|
32
|
+
* `shadowRoot`. The map is keyed by the host element and never enumerated, so
|
|
33
|
+
* it holds nothing alive and changes nothing the page can observe about the
|
|
34
|
+
* root itself. Declarative `<template shadowrootmode="closed">` roots are
|
|
35
|
+
* parsed rather than attached and stay out of reach.
|
|
36
|
+
*/
|
|
37
|
+
export const CLOSED_SHADOW_ROOTS_INIT_SCRIPT = `(() => {
|
|
38
|
+
const key = Symbol.for(${JSON.stringify(CLOSED_SHADOW_ROOTS_KEY)});
|
|
39
|
+
if (Object.prototype.hasOwnProperty.call(globalThis, key)) return;
|
|
40
|
+
const roots = new WeakMap();
|
|
41
|
+
Object.defineProperty(globalThis, key, { value: roots, enumerable: false, configurable: false, writable: false });
|
|
42
|
+
const attachShadow = Element.prototype.attachShadow;
|
|
43
|
+
Element.prototype.attachShadow = function (init) {
|
|
44
|
+
const root = attachShadow.call(this, init);
|
|
45
|
+
if (root.mode === 'closed') roots.set(this, root);
|
|
46
|
+
return root;
|
|
47
|
+
};
|
|
48
|
+
})();`;
|
|
49
|
+
/** Playwright selector engine name; `e2e-closed=<css>` matches inside recorded closed shadow roots. */
|
|
50
|
+
export const CLOSED_SHADOW_SELECTOR_ENGINE = 'e2e-closed';
|
|
51
|
+
/**
|
|
52
|
+
* The engine behind `e2e-closed=<css>`: every element matching the CSS
|
|
53
|
+
* selector inside a closed shadow root the init script recorded, reached from
|
|
54
|
+
* the query root through light DOM and open roots, and through roots of either
|
|
55
|
+
* kind nested below a closed one. Playwright's own selectors stop at a closed
|
|
56
|
+
* root, so this is what lets a screenshot mask cover a secure field the reader
|
|
57
|
+
* now reports there. It runs in the page's main world (not as a content
|
|
58
|
+
* script) because that is where the record lives.
|
|
59
|
+
*/
|
|
60
|
+
export const CLOSED_SHADOW_SELECTOR_ENGINE_SOURCE = `() => {
|
|
61
|
+
const roots = globalThis[Symbol.for(${JSON.stringify(CLOSED_SHADOW_ROOTS_KEY)})];
|
|
62
|
+
const closedRootsUnder = (root, out) => {
|
|
63
|
+
for (const el of root.querySelectorAll('*')) {
|
|
64
|
+
const closed = roots.get(el);
|
|
65
|
+
if (closed !== undefined) out.push(closed);
|
|
66
|
+
if (el.shadowRoot !== null) closedRootsUnder(el.shadowRoot, out);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
};
|
|
70
|
+
const matchesIn = (root, selector, out) => {
|
|
71
|
+
for (const el of root.querySelectorAll(selector)) out.push(el);
|
|
72
|
+
for (const el of root.querySelectorAll('*')) {
|
|
73
|
+
const nested = el.shadowRoot !== null ? el.shadowRoot : roots.get(el);
|
|
74
|
+
if (nested !== undefined && nested !== null) matchesIn(nested, selector, out);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
};
|
|
78
|
+
const queryAll = (root, selector) => {
|
|
79
|
+
if (!(roots instanceof WeakMap)) throw new Error('closed-shadow root tracking is unavailable for this document');
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const closed of closedRootsUnder(root, [])) matchesIn(closed, selector, out);
|
|
82
|
+
return out;
|
|
83
|
+
};
|
|
84
|
+
return { queryAll, query: (root, selector) => queryAll(root, selector)[0] ?? null };
|
|
85
|
+
}`;
|
|
86
|
+
/**
|
|
87
|
+
* Serialized into the page by Playwright. Must stay self-contained: no outer
|
|
88
|
+
* captures beyond its two arguments.
|
|
89
|
+
*
|
|
90
|
+
* `mode.kind === 'node'` reads exactly one element for locator reads.
|
|
91
|
+
* `mode.kind === 'tree'` walks the subtree for one agent observation and
|
|
92
|
+
* returns live element handles aligned with the flattened node list. The two
|
|
93
|
+
* modes also project nodes differently; those differences are data (see
|
|
94
|
+
* `projection` below), not scattered branches.
|
|
95
|
+
*/
|
|
96
|
+
const readSemanticsFunction = (element, options) => {
|
|
97
|
+
const SKIP_TAGS = [
|
|
98
|
+
'script',
|
|
99
|
+
'style',
|
|
100
|
+
'noscript',
|
|
101
|
+
'template',
|
|
102
|
+
'head',
|
|
103
|
+
'meta',
|
|
104
|
+
'link',
|
|
105
|
+
'title',
|
|
106
|
+
'base',
|
|
107
|
+
'param',
|
|
108
|
+
'source',
|
|
109
|
+
'track',
|
|
110
|
+
'col',
|
|
111
|
+
'colgroup',
|
|
112
|
+
'frame',
|
|
113
|
+
'frameset',
|
|
114
|
+
'object',
|
|
115
|
+
'embed',
|
|
116
|
+
];
|
|
117
|
+
const OPAQUE_TAGS = ['svg', 'math', 'canvas', 'video', 'audio'];
|
|
118
|
+
/** Options listed under one select; a country picker's tail is not worth the tokens. */
|
|
119
|
+
const MAX_SELECT_OPTIONS = 60;
|
|
120
|
+
/** Input types whose accessible name falls back to the placeholder (HTML-AAM 4.1.1). */
|
|
121
|
+
const PLACEHOLDER_NAMED_INPUT_TYPES = ['text', 'password', 'number', 'search', 'tel', 'email', 'url'];
|
|
122
|
+
/**
|
|
123
|
+
* How the active mode projects one node, expressed as data so `describe`
|
|
124
|
+
* stays branch-free. Tree mode is the model-bound projection: bounded text,
|
|
125
|
+
* a bounded attribute projection, hrefs reduced to origin+path, and the root
|
|
126
|
+
* document named by its title. Node mode is the full locator-read surface.
|
|
127
|
+
*/
|
|
128
|
+
const projection = options.mode.kind === 'tree'
|
|
129
|
+
? {
|
|
130
|
+
attributes: ['type', 'autocomplete', 'href', 'role', 'placeholder'],
|
|
131
|
+
textLimit: options.mode.textLimit,
|
|
132
|
+
nameLimit: options.mode.nameLimit,
|
|
133
|
+
redactHref: true,
|
|
134
|
+
directTextOnly: true,
|
|
135
|
+
documentRoot: true,
|
|
136
|
+
}
|
|
137
|
+
: {
|
|
138
|
+
attributes: null,
|
|
139
|
+
textLimit: null,
|
|
140
|
+
nameLimit: null,
|
|
141
|
+
redactHref: false,
|
|
142
|
+
directTextOnly: false,
|
|
143
|
+
documentRoot: false,
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Per-element memo for the facts several passes need. A tree walk asks for
|
|
147
|
+
* an element's role, name, and direct text from `isInteresting`, `describe`,
|
|
148
|
+
* and the empty-box test; each is a pure function of the element for the
|
|
149
|
+
* duration of one read, and `innerText` in particular forces layout.
|
|
150
|
+
*/
|
|
151
|
+
const memoized = (compute) => {
|
|
152
|
+
const cache = new Map();
|
|
153
|
+
return (el) => {
|
|
154
|
+
if (cache.has(el))
|
|
155
|
+
return cache.get(el);
|
|
156
|
+
const value = compute(el);
|
|
157
|
+
cache.set(el, value);
|
|
158
|
+
return value;
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
const implicitRole = memoized((el) => {
|
|
162
|
+
const explicit = el.getAttribute('role');
|
|
163
|
+
if (explicit !== null && explicit !== '')
|
|
164
|
+
return explicit.split(/\s+/)[0] ?? null;
|
|
165
|
+
const tag = el.tagName.toLowerCase();
|
|
166
|
+
const type = (el.getAttribute('type') ?? '').toLowerCase();
|
|
167
|
+
switch (tag) {
|
|
168
|
+
case 'a':
|
|
169
|
+
return el.hasAttribute('href') ? 'link' : null;
|
|
170
|
+
case 'button':
|
|
171
|
+
return 'button';
|
|
172
|
+
case 'select':
|
|
173
|
+
return el.hasAttribute('multiple') ? 'listbox' : 'combobox';
|
|
174
|
+
case 'textarea':
|
|
175
|
+
return 'textbox';
|
|
176
|
+
case 'img':
|
|
177
|
+
return el.getAttribute('alt') === '' ? 'presentation' : 'image';
|
|
178
|
+
case 'nav':
|
|
179
|
+
return 'navigation';
|
|
180
|
+
case 'main':
|
|
181
|
+
return 'main';
|
|
182
|
+
case 'option':
|
|
183
|
+
return 'option';
|
|
184
|
+
case 'h1':
|
|
185
|
+
case 'h2':
|
|
186
|
+
case 'h3':
|
|
187
|
+
case 'h4':
|
|
188
|
+
case 'h5':
|
|
189
|
+
case 'h6':
|
|
190
|
+
return 'heading';
|
|
191
|
+
case 'li':
|
|
192
|
+
return 'listitem';
|
|
193
|
+
case 'ul':
|
|
194
|
+
case 'ol':
|
|
195
|
+
return 'list';
|
|
196
|
+
case 'table':
|
|
197
|
+
return 'table';
|
|
198
|
+
// Rows and cells carry the structure a table's controls belong to: a
|
|
199
|
+
// "Delete" button means one thing per row, and only the row says which.
|
|
200
|
+
case 'tr':
|
|
201
|
+
return 'row';
|
|
202
|
+
case 'td':
|
|
203
|
+
return 'cell';
|
|
204
|
+
case 'th':
|
|
205
|
+
return 'columnheader';
|
|
206
|
+
case 'dialog':
|
|
207
|
+
return 'dialog';
|
|
208
|
+
case 'output':
|
|
209
|
+
return 'status';
|
|
210
|
+
case 'input':
|
|
211
|
+
switch (type) {
|
|
212
|
+
case 'button':
|
|
213
|
+
case 'submit':
|
|
214
|
+
case 'reset':
|
|
215
|
+
case 'image':
|
|
216
|
+
return 'button';
|
|
217
|
+
case 'checkbox':
|
|
218
|
+
return 'checkbox';
|
|
219
|
+
case 'radio':
|
|
220
|
+
return 'radio';
|
|
221
|
+
case 'range':
|
|
222
|
+
return 'slider';
|
|
223
|
+
case 'search':
|
|
224
|
+
return 'searchbox';
|
|
225
|
+
case 'hidden':
|
|
226
|
+
return null;
|
|
227
|
+
default:
|
|
228
|
+
return 'textbox';
|
|
229
|
+
}
|
|
230
|
+
default:
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
/** Rendered text as a person sees it: innerText, aria-hidden glyphs included. */
|
|
235
|
+
const textOf = (el) => {
|
|
236
|
+
if (el instanceof HTMLElement)
|
|
237
|
+
return el.innerText;
|
|
238
|
+
return el.textContent ?? '';
|
|
239
|
+
};
|
|
240
|
+
/**
|
|
241
|
+
* Text for an accessible name: what innerText shows minus every aria-hidden
|
|
242
|
+
* subtree, which the name computation drops and a screen reader never speaks
|
|
243
|
+
* (a required-field marker, a decorative glyph). Walks the tree itself so a
|
|
244
|
+
* display:none or visibility:hidden descendant stays out, as innerText keeps
|
|
245
|
+
* it out, while aria-hidden text between visible fragments is skipped too.
|
|
246
|
+
*/
|
|
247
|
+
const nameTextOf = (el) => {
|
|
248
|
+
const parts = [];
|
|
249
|
+
const walk = (node) => {
|
|
250
|
+
if (node.nodeType === 3) {
|
|
251
|
+
parts.push(node.nodeValue ?? '');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (!(node instanceof Element))
|
|
255
|
+
return;
|
|
256
|
+
if (node.getAttribute('aria-hidden') === 'true')
|
|
257
|
+
return;
|
|
258
|
+
// A control's own content is its value, not label text: innerText renders none of it.
|
|
259
|
+
if (NAME_OPAQUE_TAGS.has(node.tagName))
|
|
260
|
+
return;
|
|
261
|
+
if (node instanceof HTMLElement) {
|
|
262
|
+
const style = styleOf(node);
|
|
263
|
+
if (style !== undefined && (style.display === 'none' || style.visibility === 'hidden'))
|
|
264
|
+
return;
|
|
265
|
+
if (node.tagName === 'BR')
|
|
266
|
+
parts.push(' ');
|
|
267
|
+
}
|
|
268
|
+
for (const child of Array.from(node.childNodes))
|
|
269
|
+
walk(child);
|
|
270
|
+
};
|
|
271
|
+
walk(el);
|
|
272
|
+
return parts.join('').replace(/\s+/g, ' ').trim();
|
|
273
|
+
};
|
|
274
|
+
const NAME_OPAQUE_TAGS = new Set(['TEXTAREA', 'SELECT', 'INPUT', 'SCRIPT', 'STYLE']);
|
|
275
|
+
/** The `<label>` elements associated with a labelable element (button, input, meter, output, progress, select, textarea). */
|
|
276
|
+
const associatedLabels = (el) => {
|
|
277
|
+
const labels = el.labels;
|
|
278
|
+
return labels === undefined || labels === null ? [] : Array.from(labels);
|
|
279
|
+
};
|
|
280
|
+
/** The label sources an exact label query may match; see `RawNodeData.labels`. */
|
|
281
|
+
const labelsOf = (el) => {
|
|
282
|
+
const labels = [];
|
|
283
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
284
|
+
if (ariaLabel !== null && ariaLabel.trim() !== '')
|
|
285
|
+
labels.push(ariaLabel.trim());
|
|
286
|
+
const labelledBy = el.getAttribute('aria-labelledby');
|
|
287
|
+
if (labelledBy !== null) {
|
|
288
|
+
for (const id of labelledBy.split(/\s+/)) {
|
|
289
|
+
const target = el.ownerDocument.getElementById(id);
|
|
290
|
+
const text = target === null ? '' : nameTextOf(target);
|
|
291
|
+
if (text !== '')
|
|
292
|
+
labels.push(text);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
for (const label of associatedLabels(el)) {
|
|
296
|
+
const text = nameTextOf(label);
|
|
297
|
+
if (text !== '')
|
|
298
|
+
labels.push(text);
|
|
299
|
+
}
|
|
300
|
+
return labels.length === 0 ? null : labels;
|
|
301
|
+
};
|
|
302
|
+
/** Text owned directly by an element, excluding descendant elements. */
|
|
303
|
+
const directTextOf = memoized((el) => {
|
|
304
|
+
let out = '';
|
|
305
|
+
for (const child of Array.from(el.childNodes)) {
|
|
306
|
+
if (child.nodeType === 3)
|
|
307
|
+
out += child.nodeValue ?? '';
|
|
308
|
+
}
|
|
309
|
+
return out.replace(/\s+/g, ' ').trim();
|
|
310
|
+
});
|
|
311
|
+
const accessibleName = memoized((el) => {
|
|
312
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
313
|
+
if (ariaLabel !== null && ariaLabel.trim() !== '')
|
|
314
|
+
return ariaLabel.trim();
|
|
315
|
+
const labelledBy = el.getAttribute('aria-labelledby');
|
|
316
|
+
if (labelledBy !== null && labelledBy.trim() !== '') {
|
|
317
|
+
const parts = labelledBy
|
|
318
|
+
.split(/\s+/)
|
|
319
|
+
.map((id) => {
|
|
320
|
+
const target = el.ownerDocument.getElementById(id);
|
|
321
|
+
return target === null ? '' : nameTextOf(target);
|
|
322
|
+
})
|
|
323
|
+
.filter((part) => part.trim() !== '');
|
|
324
|
+
if (parts.length > 0)
|
|
325
|
+
return parts.join(' ').trim();
|
|
326
|
+
}
|
|
327
|
+
const labels = associatedLabels(el);
|
|
328
|
+
if (labels.length > 0) {
|
|
329
|
+
const joined = labels
|
|
330
|
+
.map((label) => nameTextOf(label))
|
|
331
|
+
.join(' ')
|
|
332
|
+
.trim();
|
|
333
|
+
if (joined !== '')
|
|
334
|
+
return joined;
|
|
335
|
+
}
|
|
336
|
+
if (el instanceof HTMLImageElement) {
|
|
337
|
+
const alt = el.getAttribute('alt');
|
|
338
|
+
if (alt !== null && alt.trim() !== '')
|
|
339
|
+
return alt.trim();
|
|
340
|
+
}
|
|
341
|
+
const role = implicitRole(el);
|
|
342
|
+
if (role === 'button' ||
|
|
343
|
+
role === 'link' ||
|
|
344
|
+
role === 'heading' ||
|
|
345
|
+
role === 'tab' ||
|
|
346
|
+
role === 'menuitem' ||
|
|
347
|
+
role === 'option' ||
|
|
348
|
+
role === 'listitem' ||
|
|
349
|
+
role === 'status' ||
|
|
350
|
+
role === 'alert') {
|
|
351
|
+
const text = nameTextOf(el);
|
|
352
|
+
if (text !== '')
|
|
353
|
+
return text;
|
|
354
|
+
}
|
|
355
|
+
if (el instanceof HTMLInputElement && (el.type === 'button' || el.type === 'submit')) {
|
|
356
|
+
if (el.value.trim() !== '')
|
|
357
|
+
return el.value.trim();
|
|
358
|
+
}
|
|
359
|
+
const title = el.getAttribute('title');
|
|
360
|
+
if (title !== null && title.trim() !== '')
|
|
361
|
+
return title.trim();
|
|
362
|
+
// HTML-AAM names an unlabeled text control by its placeholder, after the
|
|
363
|
+
// title: a bare search box is `textbox "Search…"`, not an anonymous field.
|
|
364
|
+
if (isPlaceholderNamed(el)) {
|
|
365
|
+
const placeholder = el.getAttribute('placeholder');
|
|
366
|
+
if (placeholder !== null && placeholder.trim() !== '')
|
|
367
|
+
return placeholder.trim();
|
|
368
|
+
const ariaPlaceholder = el.getAttribute('aria-placeholder');
|
|
369
|
+
if (ariaPlaceholder !== null && ariaPlaceholder.trim() !== '')
|
|
370
|
+
return ariaPlaceholder.trim();
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
});
|
|
374
|
+
/** The controls HTML-AAM lets a placeholder name: text-like inputs and textareas. */
|
|
375
|
+
const isPlaceholderNamed = (el) => {
|
|
376
|
+
if (el instanceof HTMLTextAreaElement)
|
|
377
|
+
return true;
|
|
378
|
+
if (!(el instanceof HTMLInputElement))
|
|
379
|
+
return false;
|
|
380
|
+
return PLACEHOLDER_NAMED_INPUT_TYPES.indexOf(el.type) !== -1;
|
|
381
|
+
};
|
|
382
|
+
/** Computed style, or undefined for a node the view cannot style. */
|
|
383
|
+
const styleOf = (el) => el instanceof HTMLElement ? el.ownerDocument.defaultView?.getComputedStyle(el) : undefined;
|
|
384
|
+
/**
|
|
385
|
+
* Closed shadow roots the context's init script recorded, when it ran in this
|
|
386
|
+
* document. Same literal as `CLOSED_SHADOW_ROOTS_KEY`; the reader cannot import it.
|
|
387
|
+
*/
|
|
388
|
+
const closedShadowRoots = globalThis[Symbol.for('e2e.closedShadowRoots')];
|
|
389
|
+
const shadowRootOf = (el) => el.shadowRoot ?? closedShadowRoots?.get(el) ?? null;
|
|
390
|
+
const isHidden = (el, style = styleOf(el)) => {
|
|
391
|
+
if (el.getAttribute('aria-hidden') === 'true')
|
|
392
|
+
return true;
|
|
393
|
+
if (!(el instanceof HTMLElement))
|
|
394
|
+
return el.getClientRects().length === 0;
|
|
395
|
+
if (style !== undefined && (style.visibility === 'hidden' || style.display === 'none')) {
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
// `display: contents` generates no box of its own while every child still
|
|
399
|
+
// paints (Shopify's one-page checkout form is one), so an empty rect list
|
|
400
|
+
// says nothing about what a person sees; the children decide for themselves.
|
|
401
|
+
if (style !== undefined && style.display === 'contents')
|
|
402
|
+
return false;
|
|
403
|
+
return el.getClientRects().length === 0;
|
|
404
|
+
};
|
|
405
|
+
/** Smallest side, in CSS pixels, an empty box must have to be worth reporting. */
|
|
406
|
+
const MIN_BOX_SIDE = 12;
|
|
407
|
+
/** True when a computed style paints something a person can see. */
|
|
408
|
+
const hasPaint = (style) => {
|
|
409
|
+
if (style.backgroundImage !== 'none' && style.backgroundImage !== '')
|
|
410
|
+
return true;
|
|
411
|
+
// A fully transparent color serializes with a zero alpha component.
|
|
412
|
+
const background = style.backgroundColor;
|
|
413
|
+
if (background !== '' && background !== 'transparent' && !/,\s*0\)$/.test(background)) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
if (parseFloat(style.outlineWidth) > 0 && style.outlineStyle !== 'none')
|
|
417
|
+
return true;
|
|
418
|
+
const sides = ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'];
|
|
419
|
+
const styles = ['borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle'];
|
|
420
|
+
for (let index = 0; index < sides.length; index += 1) {
|
|
421
|
+
const width = parseFloat(style[sides[index]]);
|
|
422
|
+
const kind = style[styles[index]];
|
|
423
|
+
if (width > 0 && kind !== 'none' && kind !== 'hidden')
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
return false;
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* True when an element is an empty painted rectangle: no children, no text,
|
|
430
|
+
* no role, no name — and yet plainly visible, because it has a border, an
|
|
431
|
+
* outline, or a background of its own.
|
|
432
|
+
*
|
|
433
|
+
* This is the one element class defined by being empty. A drop zone, a canvas,
|
|
434
|
+
* a chart placeholder, a colour swatch: a person sees a rectangle and can aim
|
|
435
|
+
* at it, so a tree that omits it cannot describe the page it is describing.
|
|
436
|
+
* `dragTo` in particular has no usable destination without it.
|
|
437
|
+
*
|
|
438
|
+
* The size floor and the paint requirement are what keep this from admitting
|
|
439
|
+
* every layout div: a spacer, a clearfix, or a zero-alpha wrapper paints
|
|
440
|
+
* nothing and is not something anyone can point at.
|
|
441
|
+
*/
|
|
442
|
+
const isVisibleEmptyBox = memoized((el) => {
|
|
443
|
+
const style = styleOf(el);
|
|
444
|
+
if (style === undefined)
|
|
445
|
+
return false;
|
|
446
|
+
if (el.children.length > 0)
|
|
447
|
+
return false;
|
|
448
|
+
const explicit = (el.getAttribute('role') ?? '').trim();
|
|
449
|
+
if (explicit !== '')
|
|
450
|
+
return false;
|
|
451
|
+
if (directTextOf(el) !== '')
|
|
452
|
+
return false;
|
|
453
|
+
const rect = el.getBoundingClientRect();
|
|
454
|
+
if (rect.width < MIN_BOX_SIDE || rect.height < MIN_BOX_SIDE)
|
|
455
|
+
return false;
|
|
456
|
+
return hasPaint(style);
|
|
457
|
+
});
|
|
458
|
+
/**
|
|
459
|
+
* Reduces a URL to origin and path, dropping userinfo, query, and fragment.
|
|
460
|
+
* Bounded: hrefs are shown to the model as link hints, not resolved, so a
|
|
461
|
+
* long path only buys tokens.
|
|
462
|
+
*/
|
|
463
|
+
const HREF_LIMIT = 80;
|
|
464
|
+
const originAndPath = (value, base) => {
|
|
465
|
+
try {
|
|
466
|
+
const url = new URL(value, base);
|
|
467
|
+
return `${url.origin}${url.pathname}`.slice(0, HREF_LIMIT);
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return (value.split('?')[0]?.split('#')[0] ?? '').slice(0, HREF_LIMIT);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
/** Attributes that name an element rather than describe where it sits. */
|
|
474
|
+
const NAMING_ATTRIBUTES = [options.testIdAttribute, 'name'];
|
|
475
|
+
/**
|
|
476
|
+
* A document-unique attribute selector for one element, when it has one.
|
|
477
|
+
*
|
|
478
|
+
* A test ID names an element by definition; a form control's `name` names it
|
|
479
|
+
* because the server reads it, which is also why it outlives redesigns. Ids
|
|
480
|
+
* are deliberately absent: a framework that mints them per render
|
|
481
|
+
* (`#firstName-aepj7PyFWSmXAkB8bb91h`) would make every selector single-use.
|
|
482
|
+
*/
|
|
483
|
+
const namedSelectorOf = (el) => {
|
|
484
|
+
for (const attribute of NAMING_ATTRIBUTES) {
|
|
485
|
+
const value = el.getAttribute(attribute);
|
|
486
|
+
if (value === null || value === '')
|
|
487
|
+
continue;
|
|
488
|
+
const selector = `[${attribute}="${value.replace(/["\\]/g, '\\$&')}"]`;
|
|
489
|
+
let unique;
|
|
490
|
+
try {
|
|
491
|
+
unique = el.ownerDocument.querySelectorAll(selector).length === 1;
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (unique)
|
|
497
|
+
return selector;
|
|
498
|
+
}
|
|
499
|
+
return null;
|
|
500
|
+
};
|
|
501
|
+
/**
|
|
502
|
+
* Path to one element in its own document, and whether anything along it names
|
|
503
|
+
* the element rather than counting positions to it.
|
|
504
|
+
*
|
|
505
|
+
* The walk stops at the first ancestor something names, so an anchored path is
|
|
506
|
+
* only positional *below* that ancestor. An unanchored path is positional all
|
|
507
|
+
* the way from `body`, which makes it fragile for a reason that has nothing to
|
|
508
|
+
* do with the element: a chat widget, a consent frame, or a portal appended
|
|
509
|
+
* anywhere above shifts every `nth-child` index beneath it.
|
|
510
|
+
*
|
|
511
|
+
* Each `namedSelectorOf` probe is a document-wide `querySelectorAll`, so this
|
|
512
|
+
* is deliberately not called for every node of a tree walk; see `projection`.
|
|
513
|
+
*/
|
|
514
|
+
const pathTo = (el) => {
|
|
515
|
+
const named = namedSelectorOf(el);
|
|
516
|
+
if (named !== null)
|
|
517
|
+
return { selector: named, anchored: true };
|
|
518
|
+
const parts = [];
|
|
519
|
+
let current = el;
|
|
520
|
+
while (current !== null && current.tagName.toLowerCase() !== 'html') {
|
|
521
|
+
const parent = current.parentElement;
|
|
522
|
+
if (parent === null)
|
|
523
|
+
break;
|
|
524
|
+
const index = Array.prototype.indexOf.call(parent.children, current) + 1;
|
|
525
|
+
parts.unshift(`${current.tagName.toLowerCase()}:nth-child(${index})`);
|
|
526
|
+
const anchor = namedSelectorOf(parent);
|
|
527
|
+
if (anchor !== null)
|
|
528
|
+
return { selector: `${anchor} > ${parts.join(' > ')}`, anchored: true };
|
|
529
|
+
current = parent;
|
|
530
|
+
}
|
|
531
|
+
return { selector: parts.join(' > '), anchored: false };
|
|
532
|
+
};
|
|
533
|
+
/**
|
|
534
|
+
* The selector used to re-enter one iframe. Unanchored is fine here: it is
|
|
535
|
+
* resolved against the document it was just read from, within this
|
|
536
|
+
* observation, and never stored. This is the one place a selector is
|
|
537
|
+
* derived: each `namedSelectorOf` probe is a document-wide query, and no
|
|
538
|
+
* runner consumes a per-node selector, so neither read mode pays for one.
|
|
539
|
+
*/
|
|
540
|
+
const frameSelectorOf = (el) => pathTo(el).selector;
|
|
541
|
+
/**
|
|
542
|
+
* The role both read modes report for one element.
|
|
543
|
+
*
|
|
544
|
+
* A tree walk and a single-node re-read have to agree: the runner acts on a
|
|
545
|
+
* node it selected from an observation by re-reading that node and requiring
|
|
546
|
+
* the same signature, so a role the walk invents and the re-read does not
|
|
547
|
+
* makes the node unactionable. Anything the walk used to attach after the fact
|
|
548
|
+
* belongs here instead.
|
|
549
|
+
*/
|
|
550
|
+
const roleOf = (el, tag) => {
|
|
551
|
+
const implicit = implicitRole(el);
|
|
552
|
+
if (implicit !== null)
|
|
553
|
+
return implicit;
|
|
554
|
+
if (tag === 'iframe')
|
|
555
|
+
return 'iframe';
|
|
556
|
+
return isVisibleEmptyBox(el) ? 'box' : null;
|
|
557
|
+
};
|
|
558
|
+
const describe = (el, style = styleOf(el)) => {
|
|
559
|
+
const tag = el.tagName.toLowerCase();
|
|
560
|
+
const autocomplete = (el.getAttribute('autocomplete') ?? '').toLowerCase();
|
|
561
|
+
let value = null;
|
|
562
|
+
let checked = null;
|
|
563
|
+
let selectedState = null;
|
|
564
|
+
if (el instanceof HTMLInputElement) {
|
|
565
|
+
if (el.type === 'checkbox' || el.type === 'radio')
|
|
566
|
+
checked = el.checked;
|
|
567
|
+
else
|
|
568
|
+
value = el.value;
|
|
569
|
+
}
|
|
570
|
+
else if (el instanceof HTMLTextAreaElement) {
|
|
571
|
+
value = el.value;
|
|
572
|
+
}
|
|
573
|
+
else if (el instanceof HTMLSelectElement) {
|
|
574
|
+
value = el.value;
|
|
575
|
+
}
|
|
576
|
+
else if (el instanceof HTMLOptionElement) {
|
|
577
|
+
selectedState = el.selected;
|
|
578
|
+
value = el.value;
|
|
579
|
+
}
|
|
580
|
+
const ariaChecked = el.getAttribute('aria-checked');
|
|
581
|
+
if (ariaChecked !== null)
|
|
582
|
+
checked = ariaChecked === 'true';
|
|
583
|
+
const ariaSelected = el.getAttribute('aria-selected');
|
|
584
|
+
if (ariaSelected !== null)
|
|
585
|
+
selectedState = ariaSelected === 'true';
|
|
586
|
+
const disabled = ((el instanceof HTMLInputElement ||
|
|
587
|
+
el instanceof HTMLTextAreaElement ||
|
|
588
|
+
el instanceof HTMLSelectElement ||
|
|
589
|
+
el instanceof HTMLButtonElement) &&
|
|
590
|
+
el.disabled) ||
|
|
591
|
+
el.getAttribute('aria-disabled') === 'true';
|
|
592
|
+
const ariaExpanded = el.getAttribute('aria-expanded');
|
|
593
|
+
const ariaPressed = el.getAttribute('aria-pressed');
|
|
594
|
+
// Matched against the shared selector rather than re-derived from tag and
|
|
595
|
+
// type, so this node's `secure` flag and the screenshot mask agree by
|
|
596
|
+
// construction.
|
|
597
|
+
const secure = el.matches(options.secureFieldSelector);
|
|
598
|
+
let inputPurpose = 'none';
|
|
599
|
+
if (secure)
|
|
600
|
+
inputPurpose = 'password';
|
|
601
|
+
else if (autocomplete === 'username')
|
|
602
|
+
inputPurpose = 'username';
|
|
603
|
+
else if (autocomplete === 'current-password' || autocomplete === 'new-password') {
|
|
604
|
+
inputPurpose = 'password';
|
|
605
|
+
}
|
|
606
|
+
else if (autocomplete === 'one-time-code')
|
|
607
|
+
inputPurpose = 'one-time-code';
|
|
608
|
+
const attributes = {};
|
|
609
|
+
for (const attribute of Array.from(el.attributes)) {
|
|
610
|
+
if (projection.attributes === null ||
|
|
611
|
+
projection.attributes.indexOf(attribute.name) !== -1 ||
|
|
612
|
+
attribute.name.startsWith('aria-')) {
|
|
613
|
+
if (secure && attribute.name === 'value')
|
|
614
|
+
continue;
|
|
615
|
+
// Observations expose href origin and path only: query strings and
|
|
616
|
+
// fragments routinely carry tokens.
|
|
617
|
+
if (projection.redactHref && attribute.name === 'href') {
|
|
618
|
+
attributes[attribute.name] = originAndPath(attribute.value, el.ownerDocument.baseURI);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
attributes[attribute.name] = attribute.value;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
let text;
|
|
625
|
+
if (secure)
|
|
626
|
+
text = '';
|
|
627
|
+
else if (projection.directTextOnly)
|
|
628
|
+
text = directTextOf(el);
|
|
629
|
+
else
|
|
630
|
+
text = textOf(el);
|
|
631
|
+
if (projection.textLimit !== null)
|
|
632
|
+
text = text.slice(0, projection.textLimit);
|
|
633
|
+
let name = accessibleName(el);
|
|
634
|
+
if (projection.nameLimit !== null && name !== null)
|
|
635
|
+
name = name.slice(0, projection.nameLimit);
|
|
636
|
+
// A secure field withholds its value, never its labels: a password field is still found by its label.
|
|
637
|
+
const labels = labelsOf(el);
|
|
638
|
+
const isDocumentRoot = projection.documentRoot && tag === 'html';
|
|
639
|
+
if (isDocumentRoot)
|
|
640
|
+
name = el.ownerDocument.title;
|
|
641
|
+
const rect = el.getBoundingClientRect();
|
|
642
|
+
const role = isDocumentRoot ? 'document' : roleOf(el, tag);
|
|
643
|
+
let level = null;
|
|
644
|
+
if (role === 'heading') {
|
|
645
|
+
const ariaLevel = Number.parseInt(el.getAttribute('aria-level') ?? '', 10);
|
|
646
|
+
if (Number.isInteger(ariaLevel) && ariaLevel > 0)
|
|
647
|
+
level = ariaLevel;
|
|
648
|
+
else if (/^h[1-6]$/.test(tag))
|
|
649
|
+
level = Number(tag.slice(1));
|
|
650
|
+
// HTML-AAM: a role="heading" element with no aria-level is level 2.
|
|
651
|
+
else
|
|
652
|
+
level = 2;
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
role,
|
|
656
|
+
name,
|
|
657
|
+
labels,
|
|
658
|
+
text,
|
|
659
|
+
value: secure ? null : value,
|
|
660
|
+
inputPurpose,
|
|
661
|
+
states: {
|
|
662
|
+
checked,
|
|
663
|
+
disabled,
|
|
664
|
+
selected: selectedState,
|
|
665
|
+
expanded: ariaExpanded === null ? null : ariaExpanded === 'true',
|
|
666
|
+
pressed: ariaPressed === null ? null : ariaPressed === 'true',
|
|
667
|
+
focused: el.ownerDocument.activeElement === el,
|
|
668
|
+
hidden: isHidden(el, style),
|
|
669
|
+
secure,
|
|
670
|
+
},
|
|
671
|
+
level,
|
|
672
|
+
attributes,
|
|
673
|
+
testId: el.getAttribute(options.testIdAttribute),
|
|
674
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
675
|
+
};
|
|
676
|
+
};
|
|
677
|
+
// The conditional return type resolves per call site; inside the body the
|
|
678
|
+
// discriminant narrows the value but not the generic, hence the two casts.
|
|
679
|
+
if (options.mode.kind === 'node')
|
|
680
|
+
return describe(element);
|
|
681
|
+
const maxNodes = options.mode.maxNodes;
|
|
682
|
+
const nodes = [];
|
|
683
|
+
const elements = [];
|
|
684
|
+
const ids = [];
|
|
685
|
+
let nextId = options.mode.idSeed;
|
|
686
|
+
let truncated = false;
|
|
687
|
+
// The id lives on the element itself under a registry symbol: invisible to
|
|
688
|
+
// application code (no attribute, no enumerable property), gone with the
|
|
689
|
+
// element, and readable by every later observation of the same document.
|
|
690
|
+
const REF_KEY = Symbol.for('e2e.observation.ref');
|
|
691
|
+
const stamp = (el) => {
|
|
692
|
+
const carrier = el;
|
|
693
|
+
let id = carrier[REF_KEY];
|
|
694
|
+
if (id === undefined) {
|
|
695
|
+
id = `n${String(nextId)}`;
|
|
696
|
+
nextId += 1;
|
|
697
|
+
carrier[REF_KEY] = id;
|
|
698
|
+
}
|
|
699
|
+
return id;
|
|
700
|
+
};
|
|
701
|
+
const include = (el, parent, style = styleOf(el)) => {
|
|
702
|
+
const data = describe(el, style);
|
|
703
|
+
nodes.push({ ...data, parent });
|
|
704
|
+
elements.push(el);
|
|
705
|
+
ids.push(stamp(el));
|
|
706
|
+
return nodes.length - 1;
|
|
707
|
+
};
|
|
708
|
+
/** True when a node carries semantics worth sending to a model. */
|
|
709
|
+
const isInteresting = (el) => {
|
|
710
|
+
if (el.hasAttribute(options.testIdAttribute))
|
|
711
|
+
return true;
|
|
712
|
+
const role = implicitRole(el);
|
|
713
|
+
if (role !== null && role !== 'presentation' && role !== 'none')
|
|
714
|
+
return true;
|
|
715
|
+
if (accessibleName(el) !== null)
|
|
716
|
+
return true;
|
|
717
|
+
return directTextOf(el) !== '';
|
|
718
|
+
};
|
|
719
|
+
const walk = (el, parent) => {
|
|
720
|
+
if (truncated)
|
|
721
|
+
return;
|
|
722
|
+
const tag = el.tagName.toLowerCase();
|
|
723
|
+
if (SKIP_TAGS.indexOf(tag) !== -1)
|
|
724
|
+
return;
|
|
725
|
+
// Read once and share: `isHidden` and the empty-box test both need it, and
|
|
726
|
+
// this walk already pays one `getComputedStyle` per node.
|
|
727
|
+
const style = styleOf(el);
|
|
728
|
+
if (isHidden(el, style))
|
|
729
|
+
return;
|
|
730
|
+
// Iframes are emitted as boundary nodes and never entered: their content
|
|
731
|
+
// lives in another document, which the engine captures per frame and
|
|
732
|
+
// stitches under this node.
|
|
733
|
+
if (tag === 'iframe') {
|
|
734
|
+
if (nodes.length >= maxNodes) {
|
|
735
|
+
truncated = true;
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
const index = include(el, parent, style);
|
|
739
|
+
const node = nodes[index];
|
|
740
|
+
node.frameSelector = frameSelectorOf(el);
|
|
741
|
+
if (node.name === null) {
|
|
742
|
+
const title = el.getAttribute('title');
|
|
743
|
+
if (title !== null && title.trim() !== '')
|
|
744
|
+
node.name = title.trim();
|
|
745
|
+
}
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
let nextParent = parent;
|
|
749
|
+
// An empty painted rectangle carries no semantics to be "interesting" by and
|
|
750
|
+
// is still something a person sees and aims at; `roleOf` names it `box`.
|
|
751
|
+
if (isInteresting(el) || isVisibleEmptyBox(el)) {
|
|
752
|
+
if (nodes.length >= maxNodes) {
|
|
753
|
+
truncated = true;
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
nextParent = include(el, parent, style);
|
|
757
|
+
}
|
|
758
|
+
// A closed select paints none of its options, so the hidden test would
|
|
759
|
+
// drop every one of them — and a model shown `combobox "Category"` alone
|
|
760
|
+
// has to guess the labels it may pick. The options are what the control
|
|
761
|
+
// offers, so they are listed under it, bounded like any long list.
|
|
762
|
+
if (tag === 'select') {
|
|
763
|
+
const choices = Array.from(el.options).slice(0, MAX_SELECT_OPTIONS);
|
|
764
|
+
for (const option of choices) {
|
|
765
|
+
if (nodes.length >= maxNodes) {
|
|
766
|
+
truncated = true;
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const index = include(option, nextParent, styleOf(option));
|
|
770
|
+
const node = nodes[index];
|
|
771
|
+
// Listed on purpose, so not "hidden"; the label is the name and the
|
|
772
|
+
// value attribute is the app's internal token, not something to show.
|
|
773
|
+
node.states.hidden = false;
|
|
774
|
+
node.value = null;
|
|
775
|
+
}
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
if (OPAQUE_TAGS.indexOf(tag) !== -1)
|
|
779
|
+
return;
|
|
780
|
+
for (const child of Array.from(el.children))
|
|
781
|
+
walk(child, nextParent);
|
|
782
|
+
// A shadow root is part of what the user sees, so it is part of what the
|
|
783
|
+
// model is shown. Walking the host's light children and its shadow tree
|
|
784
|
+
// double-counts nothing: slotted elements are light children, and the shadow
|
|
785
|
+
// tree holds the `<slot>` placeholders rather than copies of them. A closed
|
|
786
|
+
// root is unreachable from `shadowRoot`, so it comes from the record the
|
|
787
|
+
// context's init script kept when the page attached it.
|
|
788
|
+
const shadow = shadowRootOf(el);
|
|
789
|
+
if (shadow !== null) {
|
|
790
|
+
for (const child of Array.from(shadow.children))
|
|
791
|
+
walk(child, nextParent);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
include(element, -1);
|
|
795
|
+
for (const child of Array.from(element.children))
|
|
796
|
+
walk(child, 0);
|
|
797
|
+
return { nodes, elements, ids, truncated };
|
|
798
|
+
};
|
|
799
|
+
/**
|
|
800
|
+
* Walks a whole document from its root element in one in-page call. Built
|
|
801
|
+
* from the reader's source like the batch reader below, so the caller
|
|
802
|
+
* evaluates it directly on a page or frame instead of first resolving a
|
|
803
|
+
* `:root` locator - one fewer protocol round trip per document per capture.
|
|
804
|
+
*/
|
|
805
|
+
export const readDocumentSemanticsFunction = new Function('options', `return (${readSemanticsFunction.toString()})(document.documentElement, options);`);
|
|
806
|
+
/**
|
|
807
|
+
* Reads every matched element in one in-page round trip. A page function
|
|
808
|
+
* cannot close over module scope, so the batch function is assembled from the
|
|
809
|
+
* reader's own source (the same source `evaluate` sends) and is a
|
|
810
|
+
* self-contained function Playwright serializes and calls with the elements.
|
|
811
|
+
*/
|
|
812
|
+
export const readManySemanticsFunction = new Function('elements', 'options', `return elements.map((element) => (${readSemanticsFunction.toString()})(element, options));`);
|
|
813
|
+
/**
|
|
814
|
+
* The batch reader for `page.evaluate`, which takes one argument: the element
|
|
815
|
+
* handles the caller already holds, so what is read and what is later acted
|
|
816
|
+
* on are the same elements by construction rather than by a second lookup.
|
|
817
|
+
*/
|
|
818
|
+
/**
|
|
819
|
+
* Reads every handle in one round trip. Evaluated on the first handle so the read runs in the
|
|
820
|
+
* frame the handles belong to; `page.evaluate` would reject handles taken inside an iframe.
|
|
821
|
+
*/
|
|
822
|
+
export const readHandlesSemanticsFunction = new Function('_first', 'arg', `return arg.elements.map((element) => (${readSemanticsFunction.toString()})(element, arg.options));`);
|
|
823
|
+
//# sourceMappingURL=read-node.js.map
|