@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.
Files changed (101) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +112 -0
  4. package/dist/.build.json +1 -0
  5. package/dist/actions.d.ts +23 -0
  6. package/dist/actions.d.ts.map +1 -0
  7. package/dist/actions.js +174 -0
  8. package/dist/actions.js.map +1 -0
  9. package/dist/attempt-session.d.ts +76 -0
  10. package/dist/attempt-session.d.ts.map +1 -0
  11. package/dist/attempt-session.js +290 -0
  12. package/dist/attempt-session.js.map +1 -0
  13. package/dist/browser-connection.d.ts +34 -0
  14. package/dist/browser-connection.d.ts.map +1 -0
  15. package/dist/browser-connection.js +87 -0
  16. package/dist/browser-connection.js.map +1 -0
  17. package/dist/capture-scope.d.ts +21 -0
  18. package/dist/capture-scope.d.ts.map +1 -0
  19. package/dist/capture-scope.js +61 -0
  20. package/dist/capture-scope.js.map +1 -0
  21. package/dist/cdp-recovery.d.ts +29 -0
  22. package/dist/cdp-recovery.d.ts.map +1 -0
  23. package/dist/cdp-recovery.js +84 -0
  24. package/dist/cdp-recovery.js.map +1 -0
  25. package/dist/cdp-selectors.d.ts +14 -0
  26. package/dist/cdp-selectors.d.ts.map +1 -0
  27. package/dist/cdp-selectors.js +48 -0
  28. package/dist/cdp-selectors.js.map +1 -0
  29. package/dist/dialogs.d.ts +37 -0
  30. package/dist/dialogs.d.ts.map +1 -0
  31. package/dist/dialogs.js +78 -0
  32. package/dist/dialogs.js.map +1 -0
  33. package/dist/engine.d.ts +26 -0
  34. package/dist/engine.d.ts.map +1 -0
  35. package/dist/engine.js +166 -0
  36. package/dist/engine.js.map +1 -0
  37. package/dist/evaluation.d.ts +11 -0
  38. package/dist/evaluation.d.ts.map +1 -0
  39. package/dist/evaluation.js +27 -0
  40. package/dist/evaluation.js.map +1 -0
  41. package/dist/index.d.ts +21 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +14 -0
  44. package/dist/index.js.map +1 -0
  45. package/dist/install.d.ts +40 -0
  46. package/dist/install.d.ts.map +1 -0
  47. package/dist/install.js +117 -0
  48. package/dist/install.js.map +1 -0
  49. package/dist/locators.d.ts +74 -0
  50. package/dist/locators.d.ts.map +1 -0
  51. package/dist/locators.js +260 -0
  52. package/dist/locators.js.map +1 -0
  53. package/dist/observation-capture.d.ts +12 -0
  54. package/dist/observation-capture.d.ts.map +1 -0
  55. package/dist/observation-capture.js +93 -0
  56. package/dist/observation-capture.js.map +1 -0
  57. package/dist/observation.d.ts +63 -0
  58. package/dist/observation.d.ts.map +1 -0
  59. package/dist/observation.js +262 -0
  60. package/dist/observation.js.map +1 -0
  61. package/dist/observe.d.ts +51 -0
  62. package/dist/observe.d.ts.map +1 -0
  63. package/dist/observe.js +104 -0
  64. package/dist/observe.js.map +1 -0
  65. package/dist/operation-budget.d.ts +11 -0
  66. package/dist/operation-budget.d.ts.map +1 -0
  67. package/dist/operation-budget.js +40 -0
  68. package/dist/operation-budget.js.map +1 -0
  69. package/dist/protected-app.d.ts +32 -0
  70. package/dist/protected-app.d.ts.map +1 -0
  71. package/dist/protected-app.js +39 -0
  72. package/dist/protected-app.js.map +1 -0
  73. package/dist/read-node.d.ts +170 -0
  74. package/dist/read-node.d.ts.map +1 -0
  75. package/dist/read-node.js +823 -0
  76. package/dist/read-node.js.map +1 -0
  77. package/dist/refs.d.ts +43 -0
  78. package/dist/refs.d.ts.map +1 -0
  79. package/dist/refs.js +85 -0
  80. package/dist/refs.js.map +1 -0
  81. package/dist/route-pattern.d.ts +15 -0
  82. package/dist/route-pattern.d.ts.map +1 -0
  83. package/dist/route-pattern.js +68 -0
  84. package/dist/route-pattern.js.map +1 -0
  85. package/dist/support.d.ts +119 -0
  86. package/dist/support.d.ts.map +1 -0
  87. package/dist/support.js +299 -0
  88. package/dist/support.js.map +1 -0
  89. package/dist/surface.d.ts +257 -0
  90. package/dist/surface.d.ts.map +1 -0
  91. package/dist/surface.js +587 -0
  92. package/dist/surface.js.map +1 -0
  93. package/dist/video.d.ts +59 -0
  94. package/dist/video.d.ts.map +1 -0
  95. package/dist/video.js +115 -0
  96. package/dist/video.js.map +1 -0
  97. package/dist/web.d.ts +191 -0
  98. package/dist/web.d.ts.map +1 -0
  99. package/dist/web.js +354 -0
  100. package/dist/web.js.map +1 -0
  101. 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