@e2e-dev/web 0.11.0-canary-20260928184528

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 (121) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +138 -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 +77 -0
  10. package/dist/attempt-session.d.ts.map +1 -0
  11. package/dist/attempt-session.js +293 -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 +86 -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 +15 -0
  26. package/dist/cdp-selectors.d.ts.map +1 -0
  27. package/dist/cdp-selectors.js +52 -0
  28. package/dist/cdp-selectors.js.map +1 -0
  29. package/dist/closed-shadow.d.ts +64 -0
  30. package/dist/closed-shadow.d.ts.map +1 -0
  31. package/dist/closed-shadow.js +142 -0
  32. package/dist/closed-shadow.js.map +1 -0
  33. package/dist/dialogs.d.ts +38 -0
  34. package/dist/dialogs.d.ts.map +1 -0
  35. package/dist/dialogs.js +87 -0
  36. package/dist/dialogs.js.map +1 -0
  37. package/dist/engine.d.ts +26 -0
  38. package/dist/engine.d.ts.map +1 -0
  39. package/dist/engine.js +176 -0
  40. package/dist/engine.js.map +1 -0
  41. package/dist/evaluation.d.ts +18 -0
  42. package/dist/evaluation.d.ts.map +1 -0
  43. package/dist/evaluation.js +35 -0
  44. package/dist/evaluation.js.map +1 -0
  45. package/dist/in-page/read-semantics.d.ts +28 -0
  46. package/dist/in-page/read-semantics.d.ts.map +1 -0
  47. package/dist/in-page/read-semantics.js +1178 -0
  48. package/dist/in-page/read-semantics.js.map +1 -0
  49. package/dist/index.d.ts +22 -0
  50. package/dist/index.d.ts.map +1 -0
  51. package/dist/index.js +14 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/install.d.ts +40 -0
  54. package/dist/install.d.ts.map +1 -0
  55. package/dist/install.js +117 -0
  56. package/dist/install.js.map +1 -0
  57. package/dist/label-selector.d.ts +37 -0
  58. package/dist/label-selector.d.ts.map +1 -0
  59. package/dist/label-selector.js +53 -0
  60. package/dist/label-selector.js.map +1 -0
  61. package/dist/locators.d.ts +76 -0
  62. package/dist/locators.d.ts.map +1 -0
  63. package/dist/locators.js +289 -0
  64. package/dist/locators.js.map +1 -0
  65. package/dist/observation-capture.d.ts +12 -0
  66. package/dist/observation-capture.d.ts.map +1 -0
  67. package/dist/observation-capture.js +93 -0
  68. package/dist/observation-capture.js.map +1 -0
  69. package/dist/observation.d.ts +63 -0
  70. package/dist/observation.d.ts.map +1 -0
  71. package/dist/observation.js +280 -0
  72. package/dist/observation.js.map +1 -0
  73. package/dist/observe.d.ts +51 -0
  74. package/dist/observe.d.ts.map +1 -0
  75. package/dist/observe.js +105 -0
  76. package/dist/observe.js.map +1 -0
  77. package/dist/operation-budget.d.ts +11 -0
  78. package/dist/operation-budget.d.ts.map +1 -0
  79. package/dist/operation-budget.js +40 -0
  80. package/dist/operation-budget.js.map +1 -0
  81. package/dist/protected-app.d.ts +32 -0
  82. package/dist/protected-app.d.ts.map +1 -0
  83. package/dist/protected-app.js +39 -0
  84. package/dist/protected-app.js.map +1 -0
  85. package/dist/provider.d.ts +158 -0
  86. package/dist/provider.d.ts.map +1 -0
  87. package/dist/provider.js +311 -0
  88. package/dist/provider.js.map +1 -0
  89. package/dist/read-node.d.ts +152 -0
  90. package/dist/read-node.d.ts.map +1 -0
  91. package/dist/read-node.js +42 -0
  92. package/dist/read-node.js.map +1 -0
  93. package/dist/refs.d.ts +43 -0
  94. package/dist/refs.d.ts.map +1 -0
  95. package/dist/refs.js +85 -0
  96. package/dist/refs.js.map +1 -0
  97. package/dist/route-pattern.d.ts +13 -0
  98. package/dist/route-pattern.d.ts.map +1 -0
  99. package/dist/route-pattern.js +55 -0
  100. package/dist/route-pattern.js.map +1 -0
  101. package/dist/selector-engines.d.ts +6 -0
  102. package/dist/selector-engines.d.ts.map +1 -0
  103. package/dist/selector-engines.js +8 -0
  104. package/dist/selector-engines.js.map +1 -0
  105. package/dist/support.d.ts +147 -0
  106. package/dist/support.d.ts.map +1 -0
  107. package/dist/support.js +366 -0
  108. package/dist/support.js.map +1 -0
  109. package/dist/surface.d.ts +313 -0
  110. package/dist/surface.d.ts.map +1 -0
  111. package/dist/surface.js +691 -0
  112. package/dist/surface.js.map +1 -0
  113. package/dist/video.d.ts +59 -0
  114. package/dist/video.d.ts.map +1 -0
  115. package/dist/video.js +116 -0
  116. package/dist/video.js.map +1 -0
  117. package/dist/web.d.ts +191 -0
  118. package/dist/web.d.ts.map +1 -0
  119. package/dist/web.js +382 -0
  120. package/dist/web.js.map +1 -0
  121. package/package.json +67 -0
@@ -0,0 +1,1178 @@
1
+ /**
2
+ * The in-page semantic reader. Playwright serializes it with `toString()` and
3
+ * runs it inside the page (`read-node.ts` assembles the three page functions
4
+ * from its source), so it must stay one self-contained function: no imports
5
+ * beyond types, no module-scope references, every constant inside the body.
6
+ * That is why it has a file of its own.
7
+ */
8
+ /**
9
+ * Serialized into the page by Playwright. Must stay self-contained: no outer
10
+ * captures beyond its two arguments.
11
+ *
12
+ * `mode.kind === 'node'` reads exactly one element for locator reads. An
13
+ * element no longer in its document reads as detached with its value alone:
14
+ * a handle taken a moment earlier can point at a node the page has since
15
+ * replaced, and what such a node reports (no boxes, so hidden) describes
16
+ * nothing on screen.
17
+ * `mode.kind === 'tree'` walks the subtree for one agent observation and
18
+ * returns live element handles aligned with the flattened node list. The two
19
+ * modes also project nodes differently; those differences are data (see
20
+ * `projection` below), not scattered branches.
21
+ */
22
+ export const readSemanticsFunction = (element, options) => {
23
+ const SKIP_TAGS = [
24
+ 'script',
25
+ 'style',
26
+ 'noscript',
27
+ 'template',
28
+ 'head',
29
+ 'meta',
30
+ 'link',
31
+ 'title',
32
+ 'base',
33
+ 'param',
34
+ 'source',
35
+ 'track',
36
+ 'col',
37
+ 'colgroup',
38
+ 'frame',
39
+ 'frameset',
40
+ 'object',
41
+ 'embed',
42
+ ];
43
+ const OPAQUE_TAGS = ['svg', 'math', 'canvas', 'video', 'audio'];
44
+ /**
45
+ * Tags a `contenteditable` never makes an editing host. A drawn or embedded
46
+ * surface and a void element hold no DOM text to edit; a native control (a
47
+ * link, a button, a form field) has a role and a value of its own, so a fill
48
+ * reaches it as what it is. Chromium reports `isContentEditable` on all of
49
+ * them regardless.
50
+ */
51
+ const NON_HOST_TAGS = [...OPAQUE_TAGS, 'iframe', 'img', 'hr', 'br', 'wbr', 'area', 'a', 'button', 'input', 'textarea', 'select'];
52
+ /** Options listed under one select; a country picker's tail is not worth the tokens. */
53
+ const MAX_SELECT_OPTIONS = 60;
54
+ /** Input types whose accessible name falls back to the placeholder (HTML-AAM 4.1.1). */
55
+ const PLACEHOLDER_NAMED_INPUT_TYPES = ['text', 'password', 'number', 'search', 'tel', 'email', 'url'];
56
+ /**
57
+ * How the active mode projects one node, expressed as data so `describe`
58
+ * stays branch-free. Tree mode is the model-bound projection: bounded text,
59
+ * a bounded attribute projection, hrefs reduced to origin+path, and the root
60
+ * document named by its title. Node mode is the full locator-read surface.
61
+ */
62
+ const projection = options.mode.kind === 'tree'
63
+ ? {
64
+ attributes: ['type', 'autocomplete', 'href', 'role', 'placeholder'],
65
+ textLimit: options.mode.textLimit,
66
+ nameLimit: options.mode.nameLimit,
67
+ redactHref: true,
68
+ directTextOnly: true,
69
+ documentRoot: true,
70
+ }
71
+ : {
72
+ attributes: null,
73
+ textLimit: null,
74
+ nameLimit: null,
75
+ redactHref: false,
76
+ directTextOnly: false,
77
+ documentRoot: false,
78
+ };
79
+ /**
80
+ * Per-element memo for the facts several passes need. A tree walk asks for
81
+ * an element's role, name, and direct text from `isInteresting`, `describe`,
82
+ * and the empty-box test; each is a pure function of the element for the
83
+ * duration of one read, and `innerText` in particular forces layout.
84
+ */
85
+ const memoized = (compute) => {
86
+ const cache = new Map();
87
+ return (el) => {
88
+ if (cache.has(el))
89
+ return cache.get(el);
90
+ const value = compute(el);
91
+ cache.set(el, value);
92
+ return value;
93
+ };
94
+ };
95
+ /**
96
+ * True when an element carries a name of its own (HTML-AAM: `aria-label`,
97
+ * `aria-labelledby`, or `title`). A `<form>` or `<section>` is a landmark
98
+ * only when named; unnamed, it is a plain container.
99
+ */
100
+ const hasOwnName = (el) => ['aria-label', 'aria-labelledby', 'title'].some((attribute) => (el.getAttribute(attribute) ?? '').trim() !== '');
101
+ const SECTIONING_SCOPE = [
102
+ 'article', 'aside', 'main', 'nav', 'section',
103
+ ...['article', 'complementary', 'main', 'navigation', 'region'].map((role) => `[role~="${role}"]`),
104
+ ].join(', ');
105
+ /**
106
+ * True when a `<header>` or `<footer>` is the page's, not an article's or a
107
+ * section's: only then is it the `banner` or `contentinfo` landmark.
108
+ * HTML-AAM scopes it to sectioning content and to the ARIA roles that stand
109
+ * for it, so a `<div role="article">` scopes it like an `<article>`.
110
+ */
111
+ const isPageLevel = (el) => el.closest(SECTIONING_SCOPE) === null;
112
+ /**
113
+ * The root of a contenteditable region: editable itself, under a parent that
114
+ * is not. A rich-text editor (ProseMirror, TipTap, Lexical, Slate) renders
115
+ * its document as such a host with block children; the host is the control
116
+ * a person types into and the blocks are its content, so only the host is a
117
+ * textbox, whatever its tag: an `<article contenteditable>` is the editor,
118
+ * not an article. A canvas made editable to collect keystrokes is a drawn
119
+ * field the keyboard reaches, not a textbox; see `NON_HOST_TAGS`.
120
+ */
121
+ const isEditingHost = (el) => el instanceof HTMLElement &&
122
+ el.isContentEditable &&
123
+ NON_HOST_TAGS.indexOf(el.tagName.toLowerCase()) === -1 &&
124
+ !(el.parentElement instanceof HTMLElement && el.parentElement.isContentEditable);
125
+ const implicitRole = memoized((el) => {
126
+ const explicit = el.getAttribute('role');
127
+ if (explicit !== null && explicit.trim() !== '') {
128
+ const first = explicit.trim().split(/\s+/)[0] ?? null;
129
+ // The vocabulary spells ARIA's `img` as `image`.
130
+ return first === 'img' ? 'image' : first;
131
+ }
132
+ // Ahead of the tag: an editor's host is the control, whatever landmark or
133
+ // structure its tag would otherwise be.
134
+ if (isEditingHost(el))
135
+ return 'textbox';
136
+ const tag = el.tagName.toLowerCase();
137
+ const type = (el.getAttribute('type') ?? '').toLowerCase();
138
+ switch (tag) {
139
+ case 'a':
140
+ return el.hasAttribute('href') ? 'link' : null;
141
+ case 'button':
142
+ return 'button';
143
+ // HTML-AAM: a select that shows several rows is a listbox; only a
144
+ // one-row drop-down is a combobox.
145
+ case 'select':
146
+ return el.hasAttribute('multiple') || el.size > 1 ? 'listbox' : 'combobox';
147
+ case 'textarea':
148
+ return 'textbox';
149
+ case 'img':
150
+ return el.getAttribute('alt') === '' ? 'presentation' : 'image';
151
+ case 'nav':
152
+ return 'navigation';
153
+ case 'main':
154
+ return 'main';
155
+ case 'aside':
156
+ return 'complementary';
157
+ case 'header':
158
+ return isPageLevel(el) ? 'banner' : null;
159
+ case 'footer':
160
+ return isPageLevel(el) ? 'contentinfo' : null;
161
+ case 'section':
162
+ return hasOwnName(el) ? 'region' : null;
163
+ case 'form':
164
+ return hasOwnName(el) ? 'form' : null;
165
+ case 'article':
166
+ return 'article';
167
+ case 'figure':
168
+ return 'figure';
169
+ case 'hr':
170
+ return 'separator';
171
+ case 'progress':
172
+ return 'progressbar';
173
+ case 'meter':
174
+ return 'meter';
175
+ // HTML-AAM: `<menu>` is a list; only `role="menu"` is a menu.
176
+ case 'menu':
177
+ return 'list';
178
+ case 'fieldset':
179
+ case 'details':
180
+ case 'optgroup':
181
+ case 'address':
182
+ return 'group';
183
+ case 'option':
184
+ return 'option';
185
+ case 'h1':
186
+ case 'h2':
187
+ case 'h3':
188
+ case 'h4':
189
+ case 'h5':
190
+ case 'h6':
191
+ return 'heading';
192
+ case 'li':
193
+ return 'listitem';
194
+ case 'ul':
195
+ case 'ol':
196
+ return 'list';
197
+ case 'table':
198
+ return 'table';
199
+ // Rows and cells carry the structure a table's controls belong to: a
200
+ // "Delete" button means one thing per row, and only the row says which.
201
+ case 'thead':
202
+ case 'tbody':
203
+ case 'tfoot':
204
+ return 'rowgroup';
205
+ case 'tr':
206
+ return 'row';
207
+ // A grid's cells are the interactive kind, as Playwright reads them.
208
+ case 'td': {
209
+ const table = el.closest('table');
210
+ const tableRole = table?.getAttribute('role')?.trim().split(/\s+/)[0];
211
+ return tableRole === 'grid' || tableRole === 'treegrid' ? 'gridcell' : 'cell';
212
+ }
213
+ case 'th': {
214
+ const scope = (el.getAttribute('scope') ?? '').toLowerCase();
215
+ return scope === 'row' || scope === 'rowgroup' ? 'rowheader' : 'columnheader';
216
+ }
217
+ case 'dialog':
218
+ return 'dialog';
219
+ case 'output':
220
+ return 'status';
221
+ case 'input':
222
+ // HTML-AAM: a text field with suggestions from a datalist is a combobox.
223
+ // `list` resolves the attribute to a datalist element, null for anything else.
224
+ // The `type` property, not the attribute: a type the browser does not know is `text`.
225
+ if (el.list !== null && ['text', 'search', 'tel', 'url', 'email'].indexOf(el.type) !== -1)
226
+ return 'combobox';
227
+ switch (type) {
228
+ case 'button':
229
+ case 'submit':
230
+ case 'reset':
231
+ case 'image':
232
+ // A file input is its picker button, as Chrome and Playwright read it.
233
+ case 'file':
234
+ return 'button';
235
+ case 'checkbox':
236
+ return 'checkbox';
237
+ case 'radio':
238
+ return 'radio';
239
+ case 'range':
240
+ return 'slider';
241
+ case 'number':
242
+ return 'spinbutton';
243
+ case 'search':
244
+ return 'searchbox';
245
+ case 'hidden':
246
+ return null;
247
+ default:
248
+ return 'textbox';
249
+ }
250
+ default:
251
+ return null;
252
+ }
253
+ });
254
+ /** Rendered text as a person sees it: innerText, aria-hidden glyphs included. */
255
+ const textOf = (el) => {
256
+ if (el instanceof HTMLElement)
257
+ return el.innerText;
258
+ return el.textContent ?? '';
259
+ };
260
+ /** Computed style of any element, SVG included, or undefined in a document with no view. */
261
+ const styleOf = (el) => el.ownerDocument.defaultView?.getComputedStyle(el);
262
+ /** A walk from the top, with the elements that may not contribute again already visited. */
263
+ const nameWalk = (visited) => ({
264
+ inReference: false,
265
+ hiddenAllowed: false,
266
+ visited: new Set(visited),
267
+ });
268
+ /** True for a subtree the name computation drops: aria-hidden, or hidden by style as innerText leaves it out. */
269
+ const isNameHidden = (el, style) => el.getAttribute('aria-hidden') === 'true' ||
270
+ (style !== undefined && (style.display === 'none' || style.visibility === 'hidden'));
271
+ /** `alt` of an element HTML-AAM names by it: an `<img>` or an `<input type="image">`. */
272
+ const altOf = (el) => {
273
+ const named = el instanceof HTMLImageElement || (el instanceof HTMLInputElement && el.type === 'image');
274
+ const alt = named ? el.getAttribute('alt') : null;
275
+ return alt !== null && alt.trim() !== '' ? alt.trim() : null;
276
+ };
277
+ /**
278
+ * True for a referenced target accname 2A reads whole: hidden itself, or
279
+ * under an `aria-hidden` ancestor, which excludes it from the tree as
280
+ * surely as its own attribute would.
281
+ */
282
+ const isReferenceHidden = (el) => isHidden(el) || el.closest('[aria-hidden="true"]') !== null;
283
+ /**
284
+ * The element an id names for `el`, looked up in `el`'s own tree: an IDREF
285
+ * resolves in the tree scope it is written in, so a shadow tree's ids are
286
+ * its own and `getElementById` on the document never sees them. A root of
287
+ * either kind answers for its tree once the reader holds the element.
288
+ */
289
+ const referencedElementOf = (el, id) => {
290
+ const root = el.getRootNode();
291
+ return root instanceof Document || root instanceof DocumentFragment ? root.getElementById(id) : null;
292
+ };
293
+ /**
294
+ * Each aria-labelledby target's contribution (accname 2B), in attribute
295
+ * order, unnamed targets dropped; null when the element references nothing
296
+ * that names it. A hidden target is read whole, as a screen reader reads it,
297
+ * and a reference inside a target is not followed, so this is null inside a
298
+ * reference traversal too.
299
+ */
300
+ const referencedNamesOf = (el, walk) => {
301
+ if (walk.inReference)
302
+ return null;
303
+ const ids = (el.getAttribute('aria-labelledby') ?? '').trim();
304
+ if (ids === '')
305
+ return null;
306
+ const names = [];
307
+ for (const id of ids.split(/\s+/)) {
308
+ const target = referencedElementOf(el, id);
309
+ if (target === null)
310
+ continue;
311
+ const name = contentNameOf(target, styleOf(target), {
312
+ inReference: true,
313
+ hiddenAllowed: isReferenceHidden(target),
314
+ visited: walk.visited,
315
+ })
316
+ .replace(/\s+/g, ' ')
317
+ .trim();
318
+ if (name !== '')
319
+ names.push(name);
320
+ }
321
+ return names.length === 0 ? null : names;
322
+ };
323
+ /**
324
+ * What one element contributes to a name computed through another: as an
325
+ * aria-labelledby target (accname 2B) or as a descendant read for name from
326
+ * content (2F). Its own references, else its `aria-label`, its `alt`, its
327
+ * children, then its `title`. So `<button><img alt="Search"></button>` is
328
+ * the button "Search", an icon whose only child is
329
+ * `<svg aria-labelledby="t"><title id="t">Close</title></svg>` is "Close",
330
+ * and a hidden `<span>` a button references still names it. A form
331
+ * control's content is its value, not label text, so it contributes nothing.
332
+ */
333
+ const contentNameOf = (el, style, walk) => {
334
+ if (walk.visited.has(el))
335
+ return '';
336
+ walk.visited.add(el);
337
+ if (!walk.hiddenAllowed && isNameHidden(el, style))
338
+ return '';
339
+ const referenced = referencedNamesOf(el, walk);
340
+ if (referenced !== null)
341
+ return referenced.join(' ');
342
+ const ariaLabel = el.getAttribute('aria-label');
343
+ if (ariaLabel !== null && ariaLabel.trim() !== '')
344
+ return ariaLabel.trim();
345
+ const alt = altOf(el);
346
+ if (alt !== null)
347
+ return alt;
348
+ if (NAME_OPAQUE_TAGS.has(el.tagName))
349
+ return '';
350
+ const content = childrenNameOf(el, walk);
351
+ if (content.trim() !== '')
352
+ return content;
353
+ const title = el.getAttribute('title');
354
+ return title === null ? '' : title.trim();
355
+ };
356
+ /**
357
+ * The children's contributions joined as Playwright's role selector joins
358
+ * them: a space on each side of a block-level child and of a `<br>`, none
359
+ * around an inline one. `<img alt="Search">Go` reads "SearchGo" and
360
+ * `<div>A</div><div>B</div>` reads "A B", so the name a role query matched
361
+ * is the name the node reports.
362
+ */
363
+ const childrenNameOf = (el, walk) => {
364
+ let out = '';
365
+ for (const child of contentChildrenOf(el)) {
366
+ if (child.nodeType === 3) {
367
+ out += child.nodeValue ?? '';
368
+ continue;
369
+ }
370
+ if (!(child instanceof Element))
371
+ continue;
372
+ const style = styleOf(child);
373
+ const token = contentNameOf(child, style, walk);
374
+ const block = child.tagName === 'BR' || (style?.display ?? 'inline') !== 'inline';
375
+ out += block ? ` ${token} ` : token;
376
+ }
377
+ return out;
378
+ };
379
+ /**
380
+ * The nodes a name from content reads under an element, in the order the
381
+ * page composes them: a `<slot>` reads what is assigned to it, and a host
382
+ * reads its light children (a slotted one skipped, its slot reads it) and
383
+ * then its shadow tree, so `<button>` whose text lives behind a shadow root
384
+ * is named by that text as Playwright's role selector names it. Which light
385
+ * children are slotted is read from the root's slots, not from each child's
386
+ * `assignedSlot`: a closed root hides that property from the outside while
387
+ * the tracked root's slots still list their assigned nodes.
388
+ */
389
+ const contentChildrenOf = (el) => {
390
+ if (el instanceof HTMLSlotElement) {
391
+ const assigned = el.assignedNodes();
392
+ if (assigned.length > 0)
393
+ return assigned;
394
+ }
395
+ const shadow = shadowRootOf(el);
396
+ if (shadow === null)
397
+ return Array.from(el.childNodes);
398
+ const slotted = new Set();
399
+ for (const slot of Array.from(shadow.querySelectorAll('slot'))) {
400
+ for (const node of slot.assignedNodes())
401
+ slotted.add(node);
402
+ }
403
+ const own = Array.from(el.childNodes).filter((child) => !slotted.has(child));
404
+ return own.concat(Array.from(shadow.childNodes));
405
+ };
406
+ /**
407
+ * Text for an accessible name from an element's own content (accname 2F):
408
+ * its descendants' contributions, hidden and aria-hidden subtrees dropped (a
409
+ * required-field marker, a decorative glyph), whitespace collapsed. The
410
+ * element's own attributes are `accessibleName`'s business; this reads what
411
+ * is inside it.
412
+ */
413
+ const nameTextOf = (el) => {
414
+ if (isNameHidden(el, styleOf(el)))
415
+ return '';
416
+ if (NAME_OPAQUE_TAGS.has(el.tagName))
417
+ return '';
418
+ return childrenNameOf(el, nameWalk([el])).replace(/\s+/g, ' ').trim();
419
+ };
420
+ const NAME_OPAQUE_TAGS = new Set(['TEXTAREA', 'SELECT', 'INPUT', 'SCRIPT', 'STYLE']);
421
+ /**
422
+ * Roles named from their content (accname 2F, the list Playwright's role
423
+ * selector uses), plus `listitem`, `status`, and `alert`, which the tree
424
+ * has always named so a list row or a message reads as one line.
425
+ */
426
+ const NAME_FROM_CONTENT_ROLES = new Set([
427
+ 'button',
428
+ 'cell',
429
+ 'checkbox',
430
+ 'columnheader',
431
+ 'gridcell',
432
+ 'heading',
433
+ 'link',
434
+ 'menuitem',
435
+ 'menuitemcheckbox',
436
+ 'menuitemradio',
437
+ 'option',
438
+ 'radio',
439
+ 'row',
440
+ 'rowheader',
441
+ 'switch',
442
+ 'tab',
443
+ 'tooltip',
444
+ 'treeitem',
445
+ 'listitem',
446
+ 'status',
447
+ 'alert',
448
+ ]);
449
+ /** HTML-AAM: the child element that names its parent when nothing ARIA does. */
450
+ const NAMING_CHILD_TAGS = { fieldset: 'legend', figure: 'figcaption', table: 'caption' };
451
+ /** The `<label>` elements associated with a labelable element (button, input, meter, output, progress, select, textarea). */
452
+ const associatedLabels = (el) => {
453
+ const labels = el.labels;
454
+ return labels === undefined || labels === null ? [] : Array.from(labels);
455
+ };
456
+ /** The label sources an exact label query may match; see `RawNodeData.labels`. */
457
+ const labelsOf = (el) => {
458
+ const labels = [];
459
+ const ariaLabel = el.getAttribute('aria-label');
460
+ if (ariaLabel !== null && ariaLabel.trim() !== '')
461
+ labels.push(ariaLabel.trim());
462
+ for (const name of referencedNamesOf(el, nameWalk([])) ?? [])
463
+ labels.push(name);
464
+ for (const label of associatedLabels(el)) {
465
+ const text = nameTextOf(label);
466
+ if (text !== '')
467
+ labels.push(text);
468
+ }
469
+ return labels.length === 0 ? null : labels;
470
+ };
471
+ /** Text owned directly by an element, excluding descendant elements. */
472
+ const directTextOf = memoized((el) => {
473
+ let out = '';
474
+ for (const child of Array.from(el.childNodes)) {
475
+ if (child.nodeType === 3)
476
+ out += child.nodeValue ?? '';
477
+ }
478
+ return out.replace(/\s+/g, ' ').trim();
479
+ });
480
+ const accessibleName = memoized((el) => {
481
+ // accname reads a labelledby reference (2B) before the element's own aria-label (2C).
482
+ const referenced = referencedNamesOf(el, nameWalk([]));
483
+ if (referenced !== null)
484
+ return referenced.join(' ');
485
+ const ariaLabel = el.getAttribute('aria-label');
486
+ if (ariaLabel !== null && ariaLabel.trim() !== '')
487
+ return ariaLabel.trim();
488
+ const labels = associatedLabels(el);
489
+ if (labels.length > 0) {
490
+ const joined = labels
491
+ .map((label) => nameTextOf(label))
492
+ .join(' ')
493
+ .trim();
494
+ if (joined !== '')
495
+ return joined;
496
+ }
497
+ const alt = altOf(el);
498
+ if (alt !== null)
499
+ return alt;
500
+ const captionTag = NAMING_CHILD_TAGS[el.tagName.toLowerCase()];
501
+ if (captionTag !== undefined) {
502
+ const caption = Array.from(el.children).find((child) => child.tagName.toLowerCase() === captionTag);
503
+ const text = caption === undefined ? '' : nameTextOf(caption);
504
+ if (text !== '')
505
+ return text;
506
+ }
507
+ if (el instanceof HTMLInputElement && (el.type === 'button' || el.type === 'submit' || el.type === 'reset')) {
508
+ if (el.value.trim() !== '')
509
+ return el.value.trim();
510
+ // HTML-AAM: a submit or reset button with no value reads its default label.
511
+ if (el.type === 'submit')
512
+ return 'Submit';
513
+ if (el.type === 'reset')
514
+ return 'Reset';
515
+ }
516
+ const role = implicitRole(el);
517
+ if (role !== null && NAME_FROM_CONTENT_ROLES.has(role)) {
518
+ const text = nameTextOf(el);
519
+ if (text !== '')
520
+ return text;
521
+ }
522
+ const title = el.getAttribute('title');
523
+ if (title !== null && title.trim() !== '')
524
+ return title.trim();
525
+ // HTML-AAM names an unlabeled text control by its placeholder, after the
526
+ // title: a bare search box is `textbox "Search…"`, not an anonymous field.
527
+ if (isPlaceholderNamed(el)) {
528
+ const placeholder = el.getAttribute('placeholder');
529
+ if (placeholder !== null && placeholder.trim() !== '')
530
+ return placeholder.trim();
531
+ const ariaPlaceholder = el.getAttribute('aria-placeholder');
532
+ if (ariaPlaceholder !== null && ariaPlaceholder.trim() !== '')
533
+ return ariaPlaceholder.trim();
534
+ if (isEditingHost(el))
535
+ return editorPlaceholderOf(el);
536
+ }
537
+ return null;
538
+ });
539
+ /**
540
+ * The controls a placeholder may name: text-like inputs and textareas
541
+ * (HTML-AAM 4.1.1), and the textbox and searchbox roles `aria-placeholder`
542
+ * applies to, an editing host among them.
543
+ */
544
+ const isPlaceholderNamed = (el) => {
545
+ if (el instanceof HTMLTextAreaElement)
546
+ return true;
547
+ if (el instanceof HTMLInputElement)
548
+ return PLACEHOLDER_NAMED_INPUT_TYPES.indexOf(el.type) !== -1;
549
+ const role = implicitRole(el);
550
+ return role === 'textbox' || role === 'searchbox';
551
+ };
552
+ /**
553
+ * The placeholder a rich-text editor paints from `data-placeholder`, the one
554
+ * convention its frameworks share: Quill sets it on the host; ProseMirror
555
+ * and TipTap set it on the first empty block and drop it once the document
556
+ * has content, as the painted hint goes.
557
+ */
558
+ const editorPlaceholderOf = (host) => {
559
+ const own = host.getAttribute('data-placeholder');
560
+ if (own !== null && own.trim() !== '')
561
+ return own.trim();
562
+ for (const block of Array.from(host.querySelectorAll('[data-placeholder]'))) {
563
+ if ((block.textContent ?? '').trim() !== '')
564
+ continue;
565
+ const placeholder = block.getAttribute('data-placeholder');
566
+ if (placeholder !== null && placeholder.trim() !== '')
567
+ return placeholder.trim();
568
+ }
569
+ return null;
570
+ };
571
+ /**
572
+ * Closed shadow roots the context's init script recorded, when it ran in this
573
+ * document. Same literal as `CLOSED_SHADOW_ROOTS_KEY`; the reader cannot import it.
574
+ */
575
+ const closedShadowRoots = globalThis[Symbol.for('e2e.closedShadowRoots')];
576
+ const shadowRootOf = (el) => el.shadowRoot ?? closedShadowRoots?.get(el) ?? null;
577
+ /**
578
+ * The element that holds focus, through every shadow root on the way.
579
+ * `document.activeElement` is retargeted to the outermost host and each
580
+ * root's `activeElement` names the next hop, so the chain is followed until
581
+ * a root holds no focus of its own; a closed root is reached through the
582
+ * record, as everywhere else in the reader.
583
+ */
584
+ const focusedElementOf = (doc) => {
585
+ let active = doc.activeElement;
586
+ while (active !== null) {
587
+ const next = shadowRootOf(active)?.activeElement ?? null;
588
+ if (next === null)
589
+ return active;
590
+ active = next;
591
+ }
592
+ return null;
593
+ };
594
+ const focusedElement = focusedElementOf(element.ownerDocument);
595
+ /**
596
+ * The text selected inside a focused field: the slice between an input's or
597
+ * textarea's selection ends, or the document selection when it lies within
598
+ * an editing host. Null for a collapsed caret, so a node with no selection
599
+ * reads exactly as before.
600
+ */
601
+ const selectedTextOf = (el) => {
602
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
603
+ const start = el.selectionStart;
604
+ const end = el.selectionEnd;
605
+ if (start === null || end === null || start === end)
606
+ return null;
607
+ return el.value.slice(Math.min(start, end), Math.max(start, end));
608
+ }
609
+ if (!isEditingHost(el))
610
+ return null;
611
+ const selection = el.ownerDocument.getSelection();
612
+ if (selection === null || selection.rangeCount === 0 || selection.isCollapsed)
613
+ return null;
614
+ if (!el.contains(selection.getRangeAt(0).commonAncestorContainer))
615
+ return null;
616
+ const text = selection.toString();
617
+ return text === '' ? null : text;
618
+ };
619
+ /** True for a text node that lays out to a box a person can see. */
620
+ const isVisibleText = (node) => {
621
+ const range = node.ownerDocument.createRange();
622
+ range.selectNode(node);
623
+ const rect = range.getBoundingClientRect();
624
+ return rect.width > 0 && rect.height > 0;
625
+ };
626
+ /**
627
+ * True for content a closed `<details>` folds away: anything under it that
628
+ * is not in its `<summary>`. Every closed ancestor is asked, so a summary's
629
+ * own controls stay visible while the closed body's do not, nested
630
+ * `<details>` and their summaries included.
631
+ */
632
+ const isInClosedDetails = (el) => {
633
+ let details = el.parentElement?.closest('details') ?? null;
634
+ while (details !== null) {
635
+ if (!details.open) {
636
+ const summary = details.querySelector(':scope > summary');
637
+ if (summary === null || !summary.contains(el))
638
+ return true;
639
+ }
640
+ details = details.parentElement?.closest('details') ?? null;
641
+ }
642
+ return false;
643
+ };
644
+ /**
645
+ * The visibility Playwright's `toBeHidden` reads, so a semantic `hidden`
646
+ * state and the platform's own filter agree: `aria-hidden` on the element,
647
+ * `display: none` or a `visibility` other than `visible` on any element (an
648
+ * SVG included), content a closed `<details>` folds away, and a box with no
649
+ * width or no height. `display: contents` generates no box of its own while
650
+ * every child still paints (Shopify's one-page checkout form is one), so
651
+ * such an element is shown when some child element or text is, and hidden
652
+ * when nothing under it is.
653
+ */
654
+ const isHidden = (el, style = styleOf(el)) => {
655
+ if (el.getAttribute('aria-hidden') === 'true')
656
+ return true;
657
+ if (style === undefined)
658
+ return true;
659
+ if (style.display === 'contents') {
660
+ for (let child = el.firstChild; child !== null; child = child.nextSibling) {
661
+ if (child instanceof Element && !isHidden(child))
662
+ return false;
663
+ if (child.nodeType === Node.TEXT_NODE && isVisibleText(child))
664
+ return false;
665
+ }
666
+ return true;
667
+ }
668
+ if (style.display === 'none' || style.visibility !== 'visible')
669
+ return true;
670
+ if (isInClosedDetails(el))
671
+ return true;
672
+ const rect = el.getBoundingClientRect();
673
+ return !(rect.width > 0 && rect.height > 0);
674
+ };
675
+ /**
676
+ * The part of `isHidden` that holds for everything under the element too,
677
+ * which is what lets the tree walk stop there. A box with no size is not in
678
+ * it: a zero-height `<html>` or wrapper still shows the fixed, absolute, and
679
+ * overflowing descendants laid out past its edges, so the walk goes on
680
+ * through it and only the element itself stays unlisted. An inert element
681
+ * is in it too (`isInert`).
682
+ */
683
+ const hidesSubtree = (el, style) => el.getAttribute('aria-hidden') === 'true' ||
684
+ isInert(el, style) ||
685
+ style === undefined ||
686
+ style.display === 'none' ||
687
+ (style.display !== 'contents' && style.visibility !== 'visible') ||
688
+ isInClosedDetails(el);
689
+ /**
690
+ * Inert content takes no input and is hidden from assistive technology, as
691
+ * Chrome's tree drops it, though it still paints: listing it would hand the
692
+ * agent controls that ignore every action. Chromium computes the inherited
693
+ * `interactivity: inert`, which follows the flat tree, so a light child
694
+ * slotted into an inert slot of a closed root counts; the attribute covers a
695
+ * browser that does not compute it. A modal dialog's inertness of the rest
696
+ * of the page is not in the style and stays listed.
697
+ */
698
+ const isInert = (el, style) => el.hasAttribute('inert') || style?.getPropertyValue('interactivity') === 'inert';
699
+ /** Smallest side, in CSS pixels, an empty box must have to be worth reporting. */
700
+ const MIN_BOX_SIDE = 12;
701
+ /** True when a computed style paints something a person can see. */
702
+ const hasPaint = (style) => {
703
+ if (style.backgroundImage !== 'none' && style.backgroundImage !== '')
704
+ return true;
705
+ // A fully transparent color serializes with a zero alpha component.
706
+ const background = style.backgroundColor;
707
+ if (background !== '' && background !== 'transparent' && !/,\s*0\)$/.test(background)) {
708
+ return true;
709
+ }
710
+ if (parseFloat(style.outlineWidth) > 0 && style.outlineStyle !== 'none')
711
+ return true;
712
+ const sides = ['borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth'];
713
+ const styles = ['borderTopStyle', 'borderRightStyle', 'borderBottomStyle', 'borderLeftStyle'];
714
+ for (let index = 0; index < sides.length; index += 1) {
715
+ const width = parseFloat(style[sides[index]]);
716
+ const kind = style[styles[index]];
717
+ if (width > 0 && kind !== 'none' && kind !== 'hidden')
718
+ return true;
719
+ }
720
+ return false;
721
+ };
722
+ /**
723
+ * True when an element is an empty painted rectangle: no children, no text,
724
+ * no role, no name — and yet plainly visible, because it has a border, an
725
+ * outline, or a background of its own.
726
+ *
727
+ * This is the one element class defined by being empty. A drop zone, a canvas,
728
+ * a chart placeholder, a colour swatch: a person sees a rectangle and can aim
729
+ * at it, so a tree that omits it cannot describe the page it is describing.
730
+ * `dragTo` in particular has no usable destination without it.
731
+ *
732
+ * The size floor and the paint requirement are what keep this from admitting
733
+ * every layout div: a spacer, a clearfix, or a zero-alpha wrapper paints
734
+ * nothing and is not something anyone can point at.
735
+ */
736
+ const isVisibleEmptyBox = memoized((el) => {
737
+ const style = styleOf(el);
738
+ if (style === undefined)
739
+ return false;
740
+ if (el.children.length > 0)
741
+ return false;
742
+ const explicit = (el.getAttribute('role') ?? '').trim();
743
+ if (explicit !== '')
744
+ return false;
745
+ if (directTextOf(el) !== '')
746
+ return false;
747
+ const rect = el.getBoundingClientRect();
748
+ if (rect.width < MIN_BOX_SIDE || rect.height < MIN_BOX_SIDE)
749
+ return false;
750
+ return hasPaint(style);
751
+ });
752
+ /**
753
+ * Reduces a URL to origin and path, dropping userinfo, query, and fragment.
754
+ * Bounded: hrefs are shown to the model as link hints, not resolved, so a
755
+ * long path only buys tokens.
756
+ */
757
+ const HREF_LIMIT = 80;
758
+ const originAndPath = (value, base) => {
759
+ try {
760
+ const url = new URL(value, base);
761
+ return `${url.origin}${url.pathname}`.slice(0, HREF_LIMIT);
762
+ }
763
+ catch {
764
+ return (value.split('?')[0]?.split('#')[0] ?? '').slice(0, HREF_LIMIT);
765
+ }
766
+ };
767
+ /** Attributes that name an element rather than describe where it sits. */
768
+ const NAMING_ATTRIBUTES = [options.testIdAttribute, 'name'];
769
+ /**
770
+ * A document-unique attribute selector for one element, when it has one.
771
+ *
772
+ * A test ID names an element by definition; a form control's `name` names it
773
+ * because the server reads it, which is also why it outlives redesigns. Ids
774
+ * are deliberately absent: a framework that mints them per render
775
+ * (`#firstName-aepj7PyFWSmXAkB8bb91h`) would make every selector single-use.
776
+ */
777
+ const namedSelectorOf = (el) => {
778
+ for (const attribute of NAMING_ATTRIBUTES) {
779
+ const value = el.getAttribute(attribute);
780
+ if (value === null || value === '')
781
+ continue;
782
+ const selector = `[${attribute}="${value.replace(/["\\]/g, '\\$&')}"]`;
783
+ let unique;
784
+ try {
785
+ unique = el.ownerDocument.querySelectorAll(selector).length === 1;
786
+ }
787
+ catch {
788
+ continue;
789
+ }
790
+ if (unique)
791
+ return selector;
792
+ }
793
+ return null;
794
+ };
795
+ /**
796
+ * Path to one element in its own document, and whether anything along it names
797
+ * the element rather than counting positions to it.
798
+ *
799
+ * The walk stops at the first ancestor something names, so an anchored path is
800
+ * only positional *below* that ancestor. An unanchored path is positional all
801
+ * the way from `body`, which makes it fragile for a reason that has nothing to
802
+ * do with the element: a chat widget, a consent frame, or a portal appended
803
+ * anywhere above shifts every `nth-child` index beneath it.
804
+ *
805
+ * Each `namedSelectorOf` probe is a document-wide `querySelectorAll`, so this
806
+ * is deliberately not called for every node of a tree walk; see `projection`.
807
+ */
808
+ const pathTo = (el) => {
809
+ const named = namedSelectorOf(el);
810
+ if (named !== null)
811
+ return { selector: named, anchored: true };
812
+ const parts = [];
813
+ let current = el;
814
+ while (current !== null && current.tagName.toLowerCase() !== 'html') {
815
+ const parent = current.parentElement;
816
+ if (parent === null)
817
+ break;
818
+ const index = Array.prototype.indexOf.call(parent.children, current) + 1;
819
+ parts.unshift(`${current.tagName.toLowerCase()}:nth-child(${index})`);
820
+ const anchor = namedSelectorOf(parent);
821
+ if (anchor !== null)
822
+ return { selector: `${anchor} > ${parts.join(' > ')}`, anchored: true };
823
+ current = parent;
824
+ }
825
+ return { selector: parts.join(' > '), anchored: false };
826
+ };
827
+ /**
828
+ * The selector used to re-enter one iframe. Unanchored is fine here: it is
829
+ * resolved against the document it was just read from, within this
830
+ * observation, and never stored. This is the one place a selector is
831
+ * derived: each `namedSelectorOf` probe is a document-wide query, and no
832
+ * runner consumes a per-node selector, so neither read mode pays for one.
833
+ */
834
+ const frameSelectorOf = (el) => pathTo(el).selector;
835
+ /**
836
+ * The role both read modes report for one element.
837
+ *
838
+ * A tree walk and a single-node re-read have to agree: the runner acts on a
839
+ * node it selected from an observation by re-reading that node and requiring
840
+ * the same signature, so a role the walk invents and the re-read does not
841
+ * makes the node unactionable. Anything the walk used to attach after the fact
842
+ * belongs here instead.
843
+ */
844
+ const roleOf = (el, tag) => {
845
+ const implicit = implicitRole(el);
846
+ if (implicit !== null)
847
+ return implicit;
848
+ if (tag === 'iframe')
849
+ return 'iframe';
850
+ return isVisibleEmptyBox(el) ? 'box' : null;
851
+ };
852
+ /**
853
+ * The roles WAI-ARIA 1.2 lets `aria-disabled` apply to, the same list
854
+ * Playwright's `toBeDisabled` consults. Widgets and the composites that hold
855
+ * them; a plain container or text takes no disabled state.
856
+ */
857
+ const ARIA_DISABLED_ROLES = [
858
+ 'application', 'button', 'composite', 'gridcell', 'group', 'input', 'link', 'menuitem', 'scrollbar',
859
+ 'separator', 'tab', 'checkbox', 'columnheader', 'combobox', 'grid', 'listbox', 'menu', 'menubar',
860
+ 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'radiogroup', 'row', 'rowheader', 'searchbox',
861
+ 'select', 'slider', 'spinbutton', 'switch', 'tablist', 'textbox', 'toolbar', 'tree', 'treegrid', 'treeitem',
862
+ ];
863
+ /** The parent `aria-disabled` inherits across: the tree parent, or the host at a shadow root. */
864
+ const parentOrHostOf = (el) => {
865
+ if (el.parentElement !== null)
866
+ return el.parentElement;
867
+ const root = el.getRootNode();
868
+ return root instanceof ShadowRoot ? root.host : null;
869
+ };
870
+ /**
871
+ * What the nearest `aria-disabled` on the element or above it says
872
+ * (WAI-ARIA: the state applies to every descendant), across shadow
873
+ * boundaries, `false` cutting the chain. Memoized so a subtree under one
874
+ * disabled ancestor costs one walk, not one per node.
875
+ */
876
+ const ariaDisabledInChain = memoized((el) => {
877
+ const attribute = (el.getAttribute('aria-disabled') ?? '').toLowerCase();
878
+ if (attribute === 'true')
879
+ return true;
880
+ if (attribute === 'false')
881
+ return false;
882
+ const parent = parentOrHostOf(el);
883
+ return parent === null ? false : ariaDisabledInChain(parent);
884
+ });
885
+ /**
886
+ * The disabled state a person meets, not the attribute the element carries.
887
+ * `:disabled` is the browser's answer for a form control: its own attribute,
888
+ * a disabled fieldset above it (except inside that fieldset's first legend),
889
+ * a disabled optgroup. An `aria-disabled="true"` on the element counts for
890
+ * any role; inherited from an ancestor, it reaches the roles the state
891
+ * applies to, as Playwright reads it.
892
+ */
893
+ const isDisabled = (el) => {
894
+ if (el.matches(':disabled'))
895
+ return true;
896
+ if (el.getAttribute('aria-disabled') === 'true')
897
+ return true;
898
+ const role = implicitRole(el);
899
+ return role !== null && ARIA_DISABLED_ROLES.indexOf(role) !== -1 && ariaDisabledInChain(el);
900
+ };
901
+ const describe = (el, style = styleOf(el)) => {
902
+ const tag = el.tagName.toLowerCase();
903
+ const autocomplete = (el.getAttribute('autocomplete') ?? '').toLowerCase();
904
+ let value = null;
905
+ let checked = null;
906
+ let selectedState = null;
907
+ if (el instanceof HTMLInputElement) {
908
+ if (el.type === 'checkbox' || el.type === 'radio')
909
+ checked = el.checked;
910
+ else
911
+ value = el.value;
912
+ }
913
+ else if (el instanceof HTMLTextAreaElement) {
914
+ value = el.value;
915
+ }
916
+ else if (el instanceof HTMLSelectElement) {
917
+ value = el.value;
918
+ }
919
+ else if (el instanceof HTMLOptionElement) {
920
+ selectedState = el.selected;
921
+ value = el.value;
922
+ }
923
+ else if (isEditingHost(el)) {
924
+ // An editor's document is its value, as a textarea's text is: kept as
925
+ // rendered, spaces and newlines included. An empty editor renders
926
+ // `<p><br></p>`, which innerText reads as a newline, so a document with
927
+ // no text nodes, or with only markup whitespace that renders as that one
928
+ // newline, is an empty value; typed spaces are text nodes and stay. The
929
+ // zero-width no-break space Slate and Quill pad an empty line with
930
+ // renders nothing, so it is no text either. Cut like text in the
931
+ // model-bound projection.
932
+ const rendered = textOf(el).replace(//g, '');
933
+ const text = (el.textContent ?? '').replace(//g, '');
934
+ value = text === '' || (text.trim() === '' && rendered === '\n') ? '' : rendered;
935
+ if (projection.textLimit !== null)
936
+ value = value.slice(0, projection.textLimit);
937
+ }
938
+ // A native control's state is the control's, as Playwright reads it: a
939
+ // stale aria-checked on a checkbox or aria-selected on an option says
940
+ // nothing about what the browser will submit. ARIA fills in only where the
941
+ // element has no native state.
942
+ if (checked === null) {
943
+ const ariaChecked = el.getAttribute('aria-checked');
944
+ if (ariaChecked !== null)
945
+ checked = ariaChecked === 'true';
946
+ }
947
+ if (selectedState === null) {
948
+ const ariaSelected = el.getAttribute('aria-selected');
949
+ if (ariaSelected !== null)
950
+ selectedState = ariaSelected === 'true';
951
+ }
952
+ const disabled = isDisabled(el);
953
+ const ariaExpanded = el.getAttribute('aria-expanded');
954
+ const ariaPressed = el.getAttribute('aria-pressed');
955
+ // Matched against the shared selector rather than re-derived from tag and
956
+ // type, so this node's `secure` flag and the screenshot mask agree by
957
+ // construction.
958
+ const secure = el.matches(options.secureFieldSelector);
959
+ let selection = null;
960
+ if (focusedElement === el && !secure) {
961
+ selection = selectedTextOf(el);
962
+ if (selection !== null && projection.textLimit !== null)
963
+ selection = selection.slice(0, projection.textLimit);
964
+ }
965
+ let inputPurpose = 'none';
966
+ if (secure)
967
+ inputPurpose = 'password';
968
+ else if (autocomplete === 'username')
969
+ inputPurpose = 'username';
970
+ else if (autocomplete === 'current-password' || autocomplete === 'new-password') {
971
+ inputPurpose = 'password';
972
+ }
973
+ else if (autocomplete === 'one-time-code')
974
+ inputPurpose = 'one-time-code';
975
+ const attributes = Object.create(null);
976
+ for (const attribute of Array.from(el.attributes)) {
977
+ if (projection.attributes === null ||
978
+ projection.attributes.indexOf(attribute.name) !== -1 ||
979
+ attribute.name.startsWith('aria-')) {
980
+ if (secure && attribute.name === 'value')
981
+ continue;
982
+ // Observations expose href origin and path only: query strings and
983
+ // fragments routinely carry tokens.
984
+ if (projection.redactHref && attribute.name === 'href') {
985
+ attributes[attribute.name] = originAndPath(attribute.value, el.ownerDocument.baseURI);
986
+ continue;
987
+ }
988
+ attributes[attribute.name] = attribute.value;
989
+ }
990
+ }
991
+ let text;
992
+ if (secure)
993
+ text = '';
994
+ else if (projection.directTextOnly)
995
+ text = directTextOf(el);
996
+ else
997
+ text = textOf(el);
998
+ if (projection.textLimit !== null)
999
+ text = text.slice(0, projection.textLimit);
1000
+ let name = accessibleName(el);
1001
+ if (projection.nameLimit !== null && name !== null)
1002
+ name = name.slice(0, projection.nameLimit);
1003
+ // A secure field withholds its value, never its labels: a password field is still found by its label.
1004
+ const labels = labelsOf(el);
1005
+ const isDocumentRoot = projection.documentRoot && tag === 'html';
1006
+ if (isDocumentRoot)
1007
+ name = el.ownerDocument.title;
1008
+ const rect = el.getBoundingClientRect();
1009
+ const role = isDocumentRoot ? 'document' : roleOf(el, tag);
1010
+ let level = null;
1011
+ if (role === 'heading') {
1012
+ const ariaLevel = Number.parseInt(el.getAttribute('aria-level') ?? '', 10);
1013
+ if (Number.isInteger(ariaLevel) && ariaLevel > 0)
1014
+ level = ariaLevel;
1015
+ else if (/^h[1-6]$/.test(tag))
1016
+ level = Number(tag.slice(1));
1017
+ // HTML-AAM: a role="heading" element with no aria-level is level 2.
1018
+ else
1019
+ level = 2;
1020
+ }
1021
+ return {
1022
+ role,
1023
+ name,
1024
+ labels,
1025
+ text,
1026
+ value: secure ? null : value,
1027
+ selection,
1028
+ inputPurpose,
1029
+ states: {
1030
+ checked,
1031
+ disabled,
1032
+ selected: selectedState,
1033
+ expanded: ariaExpanded === null ? null : ariaExpanded === 'true',
1034
+ pressed: ariaPressed === null ? null : ariaPressed === 'true',
1035
+ focused: focusedElement === el,
1036
+ hidden: isHidden(el, style),
1037
+ secure,
1038
+ },
1039
+ level,
1040
+ attributes,
1041
+ testId: el.getAttribute(options.testIdAttribute),
1042
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
1043
+ };
1044
+ };
1045
+ // The conditional return type resolves per call site; inside the body the
1046
+ // discriminant narrows the value but not the generic, hence the two casts.
1047
+ if (options.mode.kind === 'node') {
1048
+ return (element.isConnected ? describe(element) : { detached: true, value: describe(element).value });
1049
+ }
1050
+ const maxNodes = options.mode.maxNodes;
1051
+ const nodes = [];
1052
+ const elements = [];
1053
+ const ids = [];
1054
+ let nextId = options.mode.idSeed;
1055
+ let truncated = false;
1056
+ // The id lives on the element itself under a registry symbol: invisible to
1057
+ // application code (no attribute, no enumerable property), gone with the
1058
+ // element, and readable by every later observation of the same document.
1059
+ const REF_KEY = Symbol.for('e2e.observation.ref');
1060
+ const stamp = (el) => {
1061
+ const carrier = el;
1062
+ let id = carrier[REF_KEY];
1063
+ if (id === undefined) {
1064
+ id = `n${String(nextId)}`;
1065
+ nextId += 1;
1066
+ carrier[REF_KEY] = id;
1067
+ }
1068
+ return id;
1069
+ };
1070
+ const include = (el, parent, style = styleOf(el)) => {
1071
+ const data = describe(el, style);
1072
+ nodes.push({ ...data, parent });
1073
+ elements.push(el);
1074
+ ids.push(stamp(el));
1075
+ return nodes.length - 1;
1076
+ };
1077
+ /** True when a node carries semantics worth sending to a model. */
1078
+ const isInteresting = (el) => {
1079
+ if (el.hasAttribute(options.testIdAttribute))
1080
+ return true;
1081
+ const role = implicitRole(el);
1082
+ if (role !== null && role !== 'presentation' && role !== 'none')
1083
+ return true;
1084
+ if (accessibleName(el) !== null)
1085
+ return true;
1086
+ return directTextOf(el) !== '';
1087
+ };
1088
+ const walk = (el, parent) => {
1089
+ if (truncated)
1090
+ return;
1091
+ const tag = el.tagName.toLowerCase();
1092
+ if (SKIP_TAGS.indexOf(tag) !== -1)
1093
+ return;
1094
+ // Read once and share: `isHidden` and the empty-box test both need it, and
1095
+ // this walk already pays one `getComputedStyle` per node.
1096
+ const style = styleOf(el);
1097
+ if (hidesSubtree(el, style))
1098
+ return;
1099
+ const hidden = isHidden(el, style);
1100
+ // The two elements listed by a rule of their own carry nothing a person
1101
+ // sees past their edges: a frame with no box shows none of its document,
1102
+ // and a select with none offers no options.
1103
+ if (hidden && (tag === 'iframe' || tag === 'select'))
1104
+ return;
1105
+ // Iframes are emitted as boundary nodes and never entered: their content
1106
+ // lives in another document, which the engine captures per frame and
1107
+ // stitches under this node.
1108
+ if (tag === 'iframe') {
1109
+ if (nodes.length >= maxNodes) {
1110
+ truncated = true;
1111
+ return;
1112
+ }
1113
+ const index = include(el, parent, style);
1114
+ const node = nodes[index];
1115
+ node.frameSelector = frameSelectorOf(el);
1116
+ if (node.name === null) {
1117
+ const title = el.getAttribute('title');
1118
+ if (title !== null && title.trim() !== '')
1119
+ node.name = title.trim();
1120
+ }
1121
+ return;
1122
+ }
1123
+ let nextParent = parent;
1124
+ // An empty painted rectangle carries no semantics to be "interesting" by and
1125
+ // is still something a person sees and aims at; `roleOf` names it `box`.
1126
+ if (!hidden && (isInteresting(el) || isVisibleEmptyBox(el))) {
1127
+ if (nodes.length >= maxNodes) {
1128
+ truncated = true;
1129
+ return;
1130
+ }
1131
+ nextParent = include(el, parent, style);
1132
+ }
1133
+ // A closed select paints none of its options, so the hidden test would
1134
+ // drop every one of them — and a model shown `combobox "Category"` alone
1135
+ // has to guess the labels it may pick. The options are what the control
1136
+ // offers, so they are listed under it, bounded like any long list.
1137
+ if (tag === 'select') {
1138
+ const choices = Array.from(el.options).slice(0, MAX_SELECT_OPTIONS);
1139
+ for (const option of choices) {
1140
+ if (nodes.length >= maxNodes) {
1141
+ truncated = true;
1142
+ return;
1143
+ }
1144
+ const index = include(option, nextParent, styleOf(option));
1145
+ const node = nodes[index];
1146
+ // Listed on purpose, so not "hidden"; the label is the name and the
1147
+ // value attribute is the app's internal token, not something to show.
1148
+ node.states.hidden = false;
1149
+ node.value = null;
1150
+ }
1151
+ return;
1152
+ }
1153
+ if (OPAQUE_TAGS.indexOf(tag) !== -1)
1154
+ return;
1155
+ for (const child of Array.from(el.children))
1156
+ walk(child, nextParent);
1157
+ // A shadow root is part of what the user sees, so it is part of what the
1158
+ // model is shown. Walking the host's light children and its shadow tree
1159
+ // double-counts nothing: slotted elements are light children, and the shadow
1160
+ // tree holds the `<slot>` placeholders rather than copies of them. A closed
1161
+ // root is unreachable from `shadowRoot`, so it comes from the record the
1162
+ // context's init script kept when the page attached it.
1163
+ const shadow = shadowRootOf(el);
1164
+ if (shadow !== null) {
1165
+ for (const child of Array.from(shadow.children))
1166
+ walk(child, nextParent);
1167
+ }
1168
+ };
1169
+ // The root is the document the screen shows, listed whatever its own box: a
1170
+ // page of fixed controls leaves `<html>` with no height of its own.
1171
+ nodes[include(element, -1)].states.hidden = false;
1172
+ if (isInert(element, styleOf(element)))
1173
+ return { nodes, elements, ids, truncated };
1174
+ for (const child of Array.from(element.children))
1175
+ walk(child, 0);
1176
+ return { nodes, elements, ids, truncated };
1177
+ };
1178
+ //# sourceMappingURL=read-semantics.js.map