@e2e-dev/web 0.11.1 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +15 -9
  2. package/dist/.build.json +1 -1
  3. package/dist/actions.d.ts +1 -2
  4. package/dist/actions.js +54 -6
  5. package/dist/attempt-session.d.ts +1 -2
  6. package/dist/attempt-session.js +0 -1
  7. package/dist/bin.d.ts +2 -0
  8. package/dist/bin.js +3 -0
  9. package/dist/browser-connection.d.ts +7 -4
  10. package/dist/browser-connection.js +7 -2
  11. package/dist/browser.d.ts +43 -7
  12. package/dist/browser.js +93 -36
  13. package/dist/capture-scope.d.ts +1 -2
  14. package/dist/capture-scope.js +0 -1
  15. package/dist/cdp-recovery.d.ts +1 -2
  16. package/dist/cdp-recovery.js +0 -1
  17. package/dist/cdp-selectors.d.ts +1 -2
  18. package/dist/cdp-selectors.js +0 -1
  19. package/dist/cli.d.ts +9 -0
  20. package/dist/cli.js +42 -0
  21. package/dist/closed-shadow.d.ts +0 -1
  22. package/dist/closed-shadow.js +0 -1
  23. package/dist/dialogs.d.ts +1 -2
  24. package/dist/dialogs.js +0 -1
  25. package/dist/downloads.d.ts +1 -2
  26. package/dist/downloads.js +0 -1
  27. package/dist/engine.d.ts +1 -2
  28. package/dist/engine.js +74 -17
  29. package/dist/evaluation.d.ts +2 -1
  30. package/dist/evaluation.js +3 -2
  31. package/dist/in-page/read-semantics.d.ts +4 -6
  32. package/dist/in-page/read-semantics.js +184 -54
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +0 -1
  35. package/dist/init-scripts.d.ts +30 -0
  36. package/dist/init-scripts.js +105 -0
  37. package/dist/install.d.ts +8 -2
  38. package/dist/install.js +35 -9
  39. package/dist/label-selector.d.ts +0 -1
  40. package/dist/label-selector.js +0 -1
  41. package/dist/locators.d.ts +1 -2
  42. package/dist/locators.js +30 -19
  43. package/dist/observation-capture.d.ts +1 -2
  44. package/dist/observation-capture.js +0 -1
  45. package/dist/observation.d.ts +1 -2
  46. package/dist/observation.js +3 -4
  47. package/dist/observe.d.ts +1 -2
  48. package/dist/observe.js +0 -1
  49. package/dist/operation-budget.d.ts +0 -1
  50. package/dist/operation-budget.js +0 -1
  51. package/dist/protected-app.d.ts +20 -5
  52. package/dist/protected-app.js +36 -6
  53. package/dist/provider-video.d.ts +1 -2
  54. package/dist/provider-video.js +0 -1
  55. package/dist/provider.d.ts +0 -1
  56. package/dist/provider.js +0 -1
  57. package/dist/read-node.d.ts +16 -44
  58. package/dist/read-node.js +7 -22
  59. package/dist/read-selector.d.ts +51 -0
  60. package/dist/read-selector.js +69 -0
  61. package/dist/refs.d.ts +0 -1
  62. package/dist/refs.js +0 -1
  63. package/dist/route-options.d.ts +44 -0
  64. package/dist/route-options.js +125 -0
  65. package/dist/route-pattern.d.ts +0 -1
  66. package/dist/route-pattern.js +0 -1
  67. package/dist/selector-engines.d.ts +0 -1
  68. package/dist/selector-engines.js +4 -1
  69. package/dist/shown-selector.d.ts +20 -0
  70. package/dist/shown-selector.js +27 -0
  71. package/dist/support.d.ts +4 -3
  72. package/dist/support.js +1 -2
  73. package/dist/surface.d.ts +40 -4
  74. package/dist/surface.js +62 -28
  75. package/dist/video.d.ts +1 -2
  76. package/dist/video.js +0 -1
  77. package/package.json +9 -5
  78. package/dist/actions.d.ts.map +0 -1
  79. package/dist/actions.js.map +0 -1
  80. package/dist/attempt-session.d.ts.map +0 -1
  81. package/dist/attempt-session.js.map +0 -1
  82. package/dist/browser-connection.d.ts.map +0 -1
  83. package/dist/browser-connection.js.map +0 -1
  84. package/dist/browser.d.ts.map +0 -1
  85. package/dist/browser.js.map +0 -1
  86. package/dist/capture-scope.d.ts.map +0 -1
  87. package/dist/capture-scope.js.map +0 -1
  88. package/dist/cdp-recovery.d.ts.map +0 -1
  89. package/dist/cdp-recovery.js.map +0 -1
  90. package/dist/cdp-selectors.d.ts.map +0 -1
  91. package/dist/cdp-selectors.js.map +0 -1
  92. package/dist/closed-shadow.d.ts.map +0 -1
  93. package/dist/closed-shadow.js.map +0 -1
  94. package/dist/dialogs.d.ts.map +0 -1
  95. package/dist/dialogs.js.map +0 -1
  96. package/dist/downloads.d.ts.map +0 -1
  97. package/dist/downloads.js.map +0 -1
  98. package/dist/engine.d.ts.map +0 -1
  99. package/dist/engine.js.map +0 -1
  100. package/dist/evaluation.d.ts.map +0 -1
  101. package/dist/evaluation.js.map +0 -1
  102. package/dist/in-page/read-semantics.d.ts.map +0 -1
  103. package/dist/in-page/read-semantics.js.map +0 -1
  104. package/dist/index.d.ts.map +0 -1
  105. package/dist/index.js.map +0 -1
  106. package/dist/install.d.ts.map +0 -1
  107. package/dist/install.js.map +0 -1
  108. package/dist/label-selector.d.ts.map +0 -1
  109. package/dist/label-selector.js.map +0 -1
  110. package/dist/locators.d.ts.map +0 -1
  111. package/dist/locators.js.map +0 -1
  112. package/dist/observation-capture.d.ts.map +0 -1
  113. package/dist/observation-capture.js.map +0 -1
  114. package/dist/observation.d.ts.map +0 -1
  115. package/dist/observation.js.map +0 -1
  116. package/dist/observe.d.ts.map +0 -1
  117. package/dist/observe.js.map +0 -1
  118. package/dist/operation-budget.d.ts.map +0 -1
  119. package/dist/operation-budget.js.map +0 -1
  120. package/dist/protected-app.d.ts.map +0 -1
  121. package/dist/protected-app.js.map +0 -1
  122. package/dist/provider-video.d.ts.map +0 -1
  123. package/dist/provider-video.js.map +0 -1
  124. package/dist/provider.d.ts.map +0 -1
  125. package/dist/provider.js.map +0 -1
  126. package/dist/read-node.d.ts.map +0 -1
  127. package/dist/read-node.js.map +0 -1
  128. package/dist/refs.d.ts.map +0 -1
  129. package/dist/refs.js.map +0 -1
  130. package/dist/route-pattern.d.ts.map +0 -1
  131. package/dist/route-pattern.js.map +0 -1
  132. package/dist/selector-engines.d.ts.map +0 -1
  133. package/dist/selector-engines.js.map +0 -1
  134. package/dist/support.d.ts.map +0 -1
  135. package/dist/support.js.map +0 -1
  136. package/dist/surface.d.ts.map +0 -1
  137. package/dist/surface.js.map +0 -1
  138. package/dist/video.d.ts.map +0 -1
  139. package/dist/video.js.map +0 -1
@@ -9,11 +9,10 @@
9
9
  * Serialized into the page by Playwright. Must stay self-contained: no outer
10
10
  * captures beyond its two arguments.
11
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.
12
+ * `mode.kind === 'node'` reads exactly one element for locator reads, in
13
+ * the selector engine's task that found it (`read-selector.ts`,
14
+ * `label-selector.ts`), so the element is always in its document.
15
+ * `mode.kind === 'hidden'` answers that one element's `states.hidden` alone.
17
16
  * `mode.kind === 'tree'` walks the subtree for one agent observation and
18
17
  * returns live element handles aligned with the flattened node list. The two
19
18
  * modes also project nodes differently; those differences are data (see
@@ -56,8 +55,10 @@ export const readSemanticsFunction = (element, options) => {
56
55
  /**
57
56
  * How the active mode projects one node, expressed as data so `describe`
58
57
  * 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.
58
+ * a bounded attribute projection, hrefs reduced to origin+path, the root
59
+ * document named by its title, and no value on a checkbox or radio, whose
60
+ * value attribute is an app token its checked state already says more
61
+ * than. Node mode is the full locator-read surface.
61
62
  */
62
63
  const projection = options.mode.kind === 'tree'
63
64
  ? {
@@ -67,6 +68,7 @@ export const readSemanticsFunction = (element, options) => {
67
68
  redactHref: true,
68
69
  directTextOnly: true,
69
70
  documentRoot: true,
71
+ checkableValue: false,
70
72
  }
71
73
  : {
72
74
  attributes: null,
@@ -75,6 +77,7 @@ export const readSemanticsFunction = (element, options) => {
75
77
  redactHref: false,
76
78
  directTextOnly: false,
77
79
  documentRoot: false,
80
+ checkableValue: true,
78
81
  };
79
82
  /**
80
83
  * Per-element memo for the facts several passes need. A tree walk asks for
@@ -148,6 +151,10 @@ export const readSemanticsFunction = (element, options) => {
148
151
  return 'textbox';
149
152
  case 'img':
150
153
  return el.getAttribute('alt') === '' ? 'presentation' : 'image';
154
+ // An inline icon is a picture whether or not anything names it, as
155
+ // Playwright reads it; Chrome ignores an unnamed one.
156
+ case 'svg':
157
+ return 'image';
151
158
  case 'nav':
152
159
  return 'navigation';
153
160
  case 'main':
@@ -260,11 +267,11 @@ export const readSemanticsFunction = (element, options) => {
260
267
  /** Computed style of any element, SVG included, or undefined in a document with no view. */
261
268
  const styleOf = (el) => el.ownerDocument.defaultView?.getComputedStyle(el);
262
269
  /** A walk from the top, with the elements that may not contribute again already visited. */
263
- const nameWalk = (visited, generated) => ({
270
+ const nameWalk = (visited, accessible) => ({
264
271
  inReference: false,
265
272
  hiddenAllowed: false,
266
273
  visited: new Set(visited),
267
- generated,
274
+ accessible,
268
275
  });
269
276
  /**
270
277
  * Private-use code points: icon-font glyphs (Font Awesome's `\f090`) that
@@ -355,6 +362,24 @@ export const readSemanticsFunction = (element, options) => {
355
362
  const alt = named ? el.getAttribute('alt') : null;
356
363
  return alt !== null && alt.trim() !== '' ? alt.trim() : null;
357
364
  };
365
+ /**
366
+ * The `<title>` child an SVG element is named by (SVG-AAM), as Playwright
367
+ * reads it: `<svg><title>Close</title></svg>` is the image "Close". An svg
368
+ * marked `presentation` or `none` takes no name of its own from it, though
369
+ * the title still names a link or button around it.
370
+ */
371
+ const svgTitleOf = (el) => {
372
+ if (!(el instanceof SVGElement))
373
+ return null;
374
+ const title = Array.from(el.children).find((child) => child instanceof SVGTitleElement);
375
+ const text = (title?.textContent ?? '').replace(/\s+/g, ' ').trim();
376
+ return text === '' ? null : text;
377
+ };
378
+ /** True for an element whose role removes its semantics: `presentation` or `none`. */
379
+ const isPresentational = (el) => {
380
+ const role = implicitRole(el);
381
+ return role === 'presentation' || role === 'none';
382
+ };
358
383
  /**
359
384
  * True for a referenced target accname 2A reads whole: hidden itself, or
360
385
  * under an `aria-hidden` ancestor, which excludes it from the tree as
@@ -395,21 +420,86 @@ export const readSemanticsFunction = (element, options) => {
395
420
  inReference: true,
396
421
  hiddenAllowed: isReferenceHidden(target),
397
422
  visited: walk.visited,
398
- generated: walk.generated,
423
+ accessible: walk.accessible,
399
424
  }));
400
425
  }
401
426
  if (contributions.join(' ') === '')
402
427
  return null;
403
428
  return contributions.map((text) => text.replace(/\s+/g, ' ').trim()).filter((name) => name !== '');
404
429
  };
430
+ /**
431
+ * What an embedded control contributes to a name computed through another
432
+ * element (accname 2C), by the rules Playwright's role selector applies, so
433
+ * `<button>Flash the screen <input value="3"> times</button>` is the button
434
+ * "Flash the screen 3 times": a textbox or searchbox its current value (an
435
+ * editable element its text), a combobox or listbox its selected options'
436
+ * names (a native select with none selected its first option, a text field
437
+ * with none its value), and a range widget its `aria-valuetext`, else its
438
+ * `aria-valuenow`, else its `value` attribute. A menu contributes nothing,
439
+ * and neither does a secure field, whose value no name may carry. Null for
440
+ * any other element, and for one its own `aria-labelledby` names, which is
441
+ * named, not embedded.
442
+ */
443
+ const embeddedControlNameOf = (el, walk) => {
444
+ const role = implicitRole(el);
445
+ if (role === null)
446
+ return null;
447
+ if (el.id !== '' && (el.getAttribute('aria-labelledby') ?? '').trim().split(/\s+/).indexOf(el.id) !== -1)
448
+ return null;
449
+ if (el.matches(options.secureFieldSelector))
450
+ return '';
451
+ if (role === 'textbox' || role === 'searchbox') {
452
+ return el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement ? el.value : el.textContent ?? '';
453
+ }
454
+ if (role === 'combobox' || role === 'listbox') {
455
+ let selected;
456
+ if (el instanceof HTMLSelectElement) {
457
+ selected = Array.from(el.selectedOptions);
458
+ if (selected.length === 0 && el.options.length > 0)
459
+ selected.push(el.options[0]);
460
+ }
461
+ else {
462
+ const listbox = role === 'combobox' ? ariaOwnedOf(el).find((owned) => implicitRole(owned) === 'listbox') : el;
463
+ selected = listbox === undefined
464
+ ? []
465
+ : ariaOwnedOf(listbox).filter((owned) => owned.getAttribute('aria-selected') === 'true' && implicitRole(owned) === 'option');
466
+ }
467
+ if (selected.length === 0 && el instanceof HTMLInputElement)
468
+ return el.value;
469
+ return selected.map((option) => contentNameOf(option, styleOf(option), walk)).join(' ');
470
+ }
471
+ if (role === 'progressbar' || role === 'scrollbar' || role === 'slider' || role === 'spinbutton' || role === 'meter') {
472
+ if (el.hasAttribute('aria-valuetext'))
473
+ return el.getAttribute('aria-valuetext') ?? '';
474
+ if (el.hasAttribute('aria-valuenow'))
475
+ return el.getAttribute('aria-valuenow') ?? '';
476
+ return el.getAttribute('value') ?? '';
477
+ }
478
+ return role === 'menu' ? '' : null;
479
+ };
480
+ /**
481
+ * Every element under `el` and every element its `aria-owns` names, with
482
+ * theirs: what a composite widget owns, as Playwright's role selector
483
+ * collects a combobox's listbox and a listbox's options.
484
+ */
485
+ const ariaOwnedOf = (el) => {
486
+ const owned = Array.from(el.querySelectorAll('*'));
487
+ for (const id of (el.getAttribute('aria-owns') ?? '').trim().split(/\s+/)) {
488
+ const target = id === '' ? null : referencedElementOf(el, id);
489
+ if (target !== null)
490
+ owned.push(target, ...Array.from(target.querySelectorAll('*')));
491
+ }
492
+ return owned;
493
+ };
405
494
  /**
406
495
  * What one element contributes to a name computed through another: as an
407
496
  * aria-labelledby target (accname 2B) or as a descendant read for name from
408
- * content (2F). Its own references, else its `aria-label`, its `alt`, its
409
- * children, then its `title`. So `<button><img alt="Search"></button>` is
410
- * the button "Search", an icon whose only child is
497
+ * content (2F). Its own references, else its value as an embedded control,
498
+ * its `aria-label`, its `alt`, its children, then its `title`. So
499
+ * `<button><img alt="Search"></button>` is the button "Search", an icon
500
+ * whose only child is
411
501
  * `<svg aria-labelledby="t"><title id="t">Close</title></svg>` is "Close",
412
- * and a hidden `<span>` a button references still names it. A form
502
+ * and a hidden `<span>` a button references still names it. Any other form
413
503
  * control's content is its value, not label text, so it contributes nothing.
414
504
  * The title is the fallback only for empty content, untrimmed as Playwright
415
505
  * reads it: the spaces a block-level child adds count, so a flex container's
@@ -425,12 +515,15 @@ export const readSemanticsFunction = (element, options) => {
425
515
  const referenced = referencedNamesOf(el, walk);
426
516
  if (referenced !== null)
427
517
  return referenced.join(' ');
518
+ const embedded = walk.accessible ? embeddedControlNameOf(el, walk) : null;
519
+ if (embedded !== null)
520
+ return embedded;
428
521
  const ariaLabel = el.getAttribute('aria-label');
429
522
  if (ariaLabel !== null && ariaLabel.trim() !== '')
430
523
  return ariaLabel.trim();
431
- const alt = altOf(el);
432
- if (alt !== null)
433
- return alt;
524
+ const alternative = altOf(el) ?? svgTitleOf(el);
525
+ if (alternative !== null)
526
+ return alternative;
434
527
  if (NAME_OPAQUE_TAGS.has(el.tagName))
435
528
  return '';
436
529
  const content = childrenNameOf(el, walk);
@@ -449,7 +542,7 @@ export const readSemanticsFunction = (element, options) => {
449
542
  * `.next::after { content: " \2192" }` is the button "Go →".
450
543
  */
451
544
  const childrenNameOf = (el, walk) => {
452
- let out = walk.generated ? beforeContentOf(el) : '';
545
+ let out = walk.accessible ? beforeContentOf(el) : '';
453
546
  for (const child of contentChildrenOf(el)) {
454
547
  if (child.nodeType === 3) {
455
548
  out += child.nodeValue ?? '';
@@ -462,7 +555,7 @@ export const readSemanticsFunction = (element, options) => {
462
555
  const block = child.tagName === 'BR' || (style?.display ?? 'inline') !== 'inline';
463
556
  out += block ? ` ${token} ` : token;
464
557
  }
465
- return walk.generated ? out + afterContentOf(el) : out;
558
+ return walk.accessible ? out + afterContentOf(el) : out;
466
559
  };
467
560
  /**
468
561
  * The nodes a name from content reads under an element, in the order the
@@ -496,15 +589,17 @@ export const readSemanticsFunction = (element, options) => {
496
589
  * its descendants' contributions, hidden and aria-hidden subtrees dropped (a
497
590
  * required-field marker, a decorative glyph), whitespace collapsed. The
498
591
  * element's own attributes are `accessibleName`'s business; this reads what
499
- * is inside it. `generated` reads CSS generated content too, as an
500
- * accessible name does and label text does not.
592
+ * is inside it. `accessible` reads CSS generated content and embedded
593
+ * controls' values too, as an accessible name does and label text does not.
594
+ * `named` is the element the text names when that is not `el` (a control
595
+ * read through its `<label>`), which never contributes to its own name.
501
596
  */
502
- const nameTextOf = (el, generated) => {
597
+ const nameTextOf = (el, accessible, named = el) => {
503
598
  if (isNameHidden(el, styleOf(el)))
504
599
  return '';
505
600
  if (NAME_OPAQUE_TAGS.has(el.tagName))
506
601
  return '';
507
- return childrenNameOf(el, nameWalk([el], generated)).replace(/\s+/g, ' ').trim();
602
+ return childrenNameOf(el, nameWalk([el, named], accessible)).replace(/\s+/g, ' ').trim();
508
603
  };
509
604
  const NAME_OPAQUE_TAGS = new Set(['TEXTAREA', 'SELECT', 'INPUT', 'SCRIPT', 'STYLE']);
510
605
  /**
@@ -558,8 +653,14 @@ export const readSemanticsFunction = (element, options) => {
558
653
  const shown = labels.map(withoutGlyphs).filter((label) => label !== '');
559
654
  return shown.length === 0 ? null : shown;
560
655
  };
561
- /** Text owned directly by an element, excluding descendant elements. */
656
+ /**
657
+ * Text owned directly by an element, excluding descendant elements. An
658
+ * element with `content-visibility: hidden` renders none of it, as its
659
+ * `innerText` reads empty, so it owns none.
660
+ */
562
661
  const directTextOf = memoized((el) => {
662
+ if (styleOf(el)?.getPropertyValue('content-visibility') === 'hidden')
663
+ return '';
563
664
  let out = '';
564
665
  for (const child of Array.from(el.childNodes)) {
565
666
  if (child.nodeType === 3)
@@ -578,15 +679,15 @@ export const readSemanticsFunction = (element, options) => {
578
679
  const labels = associatedLabels(el);
579
680
  if (labels.length > 0) {
580
681
  const joined = labels
581
- .map((label) => nameTextOf(label, true))
682
+ .map((label) => nameTextOf(label, true, el))
582
683
  .join(' ')
583
684
  .trim();
584
685
  if (joined !== '')
585
686
  return joined;
586
687
  }
587
- const alt = altOf(el);
588
- if (alt !== null)
589
- return alt;
688
+ const alternative = altOf(el) ?? (isPresentational(el) ? null : svgTitleOf(el));
689
+ if (alternative !== null)
690
+ return alternative;
590
691
  const captionTag = NAMING_CHILD_TAGS[el.tagName.toLowerCase()];
591
692
  if (captionTag !== undefined) {
592
693
  const caption = Array.from(el.children).find((child) => child.tagName.toLowerCase() === captionTag);
@@ -732,25 +833,42 @@ export const readSemanticsFunction = (element, options) => {
732
833
  return false;
733
834
  };
734
835
  /**
735
- * The visibility Playwright's `toBeHidden` reads, so a semantic `hidden`
736
- * state and the platform's own filter agree: `aria-hidden` on the element,
836
+ * True when an ancestor in the flat tree has `content-visibility: hidden`:
837
+ * it keeps its own box and skips rendering everything under it, slotted
838
+ * and shadow content included, as `checkVisibility()` reads it. Memoized,
839
+ * so siblings under one ancestor share the walk up.
840
+ */
841
+ const isSkippedContent = memoized((el) => {
842
+ const parent = el.assignedSlot ?? parentOrHostOf(el);
843
+ if (parent === null)
844
+ return false;
845
+ return styleOf(parent)?.getPropertyValue('content-visibility') === 'hidden' || isSkippedContent(parent);
846
+ });
847
+ /**
848
+ * Render visibility, the one Playwright's `toBeVisible` and `toBeHidden`
849
+ * read, so a semantic `hidden` state and the platform's own filter agree:
737
850
  * `display: none` or a `visibility` other than `visible` on any element (an
738
- * SVG included), content a closed `<details>` folds away, and a box with no
739
- * width or no height. `display: contents` generates no box of its own while
740
- * every child still paints (Shopify's one-page checkout form is one), so
741
- * such an element is shown when some child element or text is, and hidden
742
- * when nothing under it is.
851
+ * SVG included), content a `content-visibility: hidden` ancestor skips or a
852
+ * closed `<details>` folds away, and a box with no width or no height.
853
+ * `aria-hidden` is not in it: it removes a node from the accessibility tree
854
+ * (`hidesSubtree`, a role query) while the node still paints, as a spinner
855
+ * marked decorative does. `display: contents` generates no box of its own
856
+ * while every child still paints (Shopify's one-page checkout form is one),
857
+ * so such an element is shown when some child element or text is, and
858
+ * hidden when nothing under it is. Its text paints in its own `visibility`,
859
+ * which a range's box does not reflect, so text under a hidden one is
860
+ * hidden, where Playwright calls it visible.
743
861
  */
744
862
  const isHidden = (el, style = styleOf(el)) => {
745
- if (el.getAttribute('aria-hidden') === 'true')
746
- return true;
747
863
  if (style === undefined)
748
864
  return true;
865
+ if (isSkippedContent(el))
866
+ return true;
749
867
  if (style.display === 'contents') {
750
868
  for (let child = el.firstChild; child !== null; child = child.nextSibling) {
751
869
  if (child instanceof Element && !isHidden(child))
752
870
  return false;
753
- if (child.nodeType === Node.TEXT_NODE && isVisibleText(child))
871
+ if (child.nodeType === Node.TEXT_NODE && style.visibility === 'visible' && isVisibleText(child))
754
872
  return false;
755
873
  }
756
874
  return true;
@@ -763,18 +881,20 @@ export const readSemanticsFunction = (element, options) => {
763
881
  return !(rect.width > 0 && rect.height > 0);
764
882
  };
765
883
  /**
766
- * The part of `isHidden` that holds for everything under the element too,
767
- * which is what lets the tree walk stop there. A box with no size is not in
768
- * it: a zero-height `<html>` or wrapper still shows the fixed, absolute, and
769
- * overflowing descendants laid out past its edges, so the walk goes on
770
- * through it and only the element itself stays unlisted. An inert element
771
- * is in it too (`isInert`).
884
+ * What takes the element and everything under it out of the tree walk: an
885
+ * `aria-hidden` or inert subtree, which the accessibility tree drops though
886
+ * it may paint (`isInert`), and `display: none` or a closed `<details>`
887
+ * body, which no descendant can undo. A `visibility` other than `visible`
888
+ * is not in it, since a descendant may set `visibility: visible` and paint
889
+ * again; nor is a box with no size: a zero-height `<html>` or wrapper still
890
+ * shows the fixed, absolute, and overflowing descendants laid out past its
891
+ * edges. The walk goes on through both and only the element itself stays
892
+ * unlisted.
772
893
  */
773
894
  const hidesSubtree = (el, style) => el.getAttribute('aria-hidden') === 'true' ||
774
895
  isInert(el, style) ||
775
896
  style === undefined ||
776
897
  style.display === 'none' ||
777
- (style.display !== 'contents' && style.visibility !== 'visible') ||
778
898
  isInClosedDetails(el);
779
899
  /**
780
900
  * Inert content takes no input and is hidden from assistive technology, as
@@ -1014,9 +1134,13 @@ export const readSemanticsFunction = (element, options) => {
1014
1134
  let checked = null;
1015
1135
  let selectedState = null;
1016
1136
  if (el instanceof HTMLInputElement) {
1017
- if (el.type === 'checkbox' || el.type === 'radio')
1137
+ // A checkbox or radio submits its value attribute (`on` by default)
1138
+ // whatever its checked state, so a locator read reports both; the tree
1139
+ // drops that token as it drops an option's.
1140
+ const checkable = el.type === 'checkbox' || el.type === 'radio';
1141
+ if (checkable)
1018
1142
  checked = el.checked;
1019
- else
1143
+ if (!checkable || projection.checkableValue)
1020
1144
  value = el.value;
1021
1145
  }
1022
1146
  else if (el instanceof HTMLTextAreaElement) {
@@ -1114,7 +1238,10 @@ export const readSemanticsFunction = (element, options) => {
1114
1238
  const isDocumentRoot = projection.documentRoot && tag === 'html';
1115
1239
  if (isDocumentRoot)
1116
1240
  name = el.ownerDocument.title;
1117
- const rect = el.getBoundingClientRect();
1241
+ // No layout box at all (`display: none`, an empty `display: contents`) is
1242
+ // no rect, as Playwright's `boundingBox()` answers null; a box with no
1243
+ // size, or one `visibility: hidden` keeps, is still a rect.
1244
+ const rect = el.getClientRects().length === 0 ? null : el.getBoundingClientRect();
1118
1245
  const role = isDocumentRoot ? 'document' : roleOf(el, tag);
1119
1246
  let level = null;
1120
1247
  if (role === 'heading') {
@@ -1148,13 +1275,15 @@ export const readSemanticsFunction = (element, options) => {
1148
1275
  level,
1149
1276
  attributes,
1150
1277
  testId: el.getAttribute(options.testIdAttribute),
1151
- rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
1278
+ rect: rect === null ? null : { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
1152
1279
  };
1153
1280
  };
1154
1281
  // The conditional return type resolves per call site; inside the body the
1155
- // discriminant narrows the value but not the generic, hence the two casts.
1282
+ // discriminant narrows the value but not the generic, hence the casts.
1283
+ if (options.mode.kind === 'hidden')
1284
+ return isHidden(element);
1156
1285
  if (options.mode.kind === 'node') {
1157
- return (element.isConnected ? describe(element) : { detached: true, value: describe(element).value });
1286
+ return describe(element);
1158
1287
  }
1159
1288
  const maxNodes = options.mode.maxNodes;
1160
1289
  const nodes = [];
@@ -1187,8 +1316,7 @@ export const readSemanticsFunction = (element, options) => {
1187
1316
  const isInteresting = (el) => {
1188
1317
  if (el.hasAttribute(options.testIdAttribute))
1189
1318
  return true;
1190
- const role = implicitRole(el);
1191
- if (role !== null && role !== 'presentation' && role !== 'none')
1319
+ if (implicitRole(el) !== null && !isPresentational(el))
1192
1320
  return true;
1193
1321
  if (accessibleName(el) !== null)
1194
1322
  return true;
@@ -1261,6 +1389,9 @@ export const readSemanticsFunction = (element, options) => {
1261
1389
  }
1262
1390
  if (OPAQUE_TAGS.indexOf(tag) !== -1)
1263
1391
  return;
1392
+ // The element keeps its box, and nothing under it renders (`isSkippedContent`).
1393
+ if (style?.getPropertyValue('content-visibility') === 'hidden')
1394
+ return;
1264
1395
  for (const child of Array.from(el.children))
1265
1396
  walk(child, nextParent);
1266
1397
  // A shadow root is part of what the user sees, so it is part of what the
@@ -1284,4 +1415,3 @@ export const readSemanticsFunction = (element, options) => {
1284
1415
  walk(child, 0);
1285
1416
  return { nodes, elements, ids, truncated };
1286
1417
  };
1287
- //# sourceMappingURL=read-semantics.js.map
package/dist/index.d.ts CHANGED
@@ -8,10 +8,11 @@ import type { Browser } from './browser.ts';
8
8
  export { web, surfaceOf } from './engine.ts';
9
9
  export type { PlaywrightLiveSurface } from './engine.ts';
10
10
  export type { WebBasicAuth, WebConnectOptions, WebOptions, WebScreencastOptions } from './surface.ts';
11
+ export type { WebInitScript } from './init-scripts.ts';
11
12
  export type { BrowserDownloadContext, BrowserLease, BrowserProvider, BrowserProviderDownloads, BrowserProviderScope, BrowserReleaseContext, BrowserRequest, } from './provider.ts';
12
13
  export type { BrowserName } from './browser-connection.ts';
13
14
  export type { Dialog, DialogHandler } from './dialogs.ts';
14
- export type { Browser, BrowserExpectation, Cookie, CookieFields, FrameScreen, RouteFulfillResponse, WebResponse, WebRoute, } from './browser.ts';
15
+ export type { Browser, BrowserExpectation, Cookie, CookieFields, FrameScreen, RouteContinueOverrides, RouteFulfillResponse, WebResponse, WebRoute, } from './browser.ts';
15
16
  /**
16
17
  * `test` typed with this engine's contributed `browser` fixture. The same
17
18
  * runtime `test` as `e2e`'s; only the fixture types differ.
@@ -28,4 +29,3 @@ export declare const describe: {
28
29
  }>) => void, afterEach: (fn: import("e2e").TestHookFn<import("e2e").TestFixtures & {
29
30
  browser: Browser;
30
31
  }>) => void, beforeAll: (fn: import("e2e").SuiteHookFn) => void, afterAll: (fn: import("e2e").SuiteHookFn) => void;
31
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -13,4 +13,3 @@ export { web, surfaceOf } from './engine.js';
13
13
  export const test = base.extend();
14
14
  /** `describe` and the hooks, the same functions as `test.describe` and `test.beforeEach`, typed with the `browser` fixture. */
15
15
  export const { describe, beforeEach, afterEach, beforeAll, afterAll } = test;
16
- //# sourceMappingURL=index.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * One init script: a string of JavaScript source, a `{ path }` to a file of
3
+ * it relative to the project root, or a function serialized into the page,
4
+ * which can close over nothing from the test process.
5
+ */
6
+ export type WebInitScript = string | {
7
+ readonly path: string;
8
+ } | (() => unknown);
9
+ /** Refuses, at config load, a `web({ initScripts })` value that is not a list of scripts. */
10
+ export declare function validateInitScripts(scripts: unknown): void;
11
+ /** The configured `initScripts`, read into page source once per process and handed to each attempt as the start of its own list. */
12
+ export declare class ConfiguredInitScripts {
13
+ private readonly scripts;
14
+ private sources;
15
+ constructor(scripts: readonly WebInitScript[]);
16
+ /** Reads every script, a `path` against the project root; a file that cannot be read is `INVALID_CONFIG`. */
17
+ load(projectRoot: string): Promise<void>;
18
+ /** A new attempt's init scripts: the configured ones, to which `browser.addInitScript` appends. */
19
+ forAttempt(): string[];
20
+ }
21
+ /**
22
+ * Validates one script `browser.addInitScript` was given and reads it into
23
+ * page source, a `path` through `resolvePath`. Only a function takes an
24
+ * argument, as in Playwright.
25
+ */
26
+ export declare function testInitScriptSource(script: unknown, argument: {
27
+ readonly arg: unknown;
28
+ } | undefined, resolvePath: (file: string) => string): Promise<string>;
29
+ /** The step label of a script: the file a `path` names, else its kind, never its source. */
30
+ export declare function initScriptLabel(script: unknown): string;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Init scripts: test code every document of an attempt runs after it is
3
+ * created and before any of its own scripts, in every tab and frame, as
4
+ * Playwright's `addInitScript` runs it. The configured ones apply to every
5
+ * attempt; `browser.addInitScript` adds more for one attempt.
6
+ */
7
+ import { readFile } from 'node:fs/promises';
8
+ import path from 'node:path';
9
+ import { ConfigurationError, TestError, validateJsonValue } from 'e2e/engine';
10
+ import { KEEP_NAMES_HELPER } from './evaluation.js';
11
+ import { message } from './support.js';
12
+ /** Refuses, at config load, a `web({ initScripts })` value that is not a list of scripts. */
13
+ export function validateInitScripts(scripts) {
14
+ if (!Array.isArray(scripts)) {
15
+ throw new ConfigurationError('INVALID_CONFIG', 'web({ initScripts }) must be an array of scripts');
16
+ }
17
+ // Indexed, not `forEach`, so a hole in a sparse array is refused like `undefined`.
18
+ for (let index = 0; index < scripts.length; index += 1) {
19
+ assertInitScript(scripts[index], (problem) => new ConfigurationError('INVALID_CONFIG', `web({ initScripts })[${index}] ${problem}`));
20
+ }
21
+ }
22
+ /** The configured `initScripts`, read into page source once per process and handed to each attempt as the start of its own list. */
23
+ export class ConfiguredInitScripts {
24
+ scripts;
25
+ sources = [];
26
+ constructor(scripts) {
27
+ this.scripts = scripts;
28
+ }
29
+ /** Reads every script, a `path` against the project root; a file that cannot be read is `INVALID_CONFIG`. */
30
+ async load(projectRoot) {
31
+ if (this.scripts.length === 0)
32
+ return;
33
+ this.sources = await Promise.all(this.scripts.map(async (script, index) => {
34
+ try {
35
+ return await initScriptSource(script, undefined, (file) => path.resolve(projectRoot, file));
36
+ }
37
+ catch (cause) {
38
+ throw new ConfigurationError('INVALID_CONFIG', `web({ initScripts })[${index}] ${message(cause)}`, { cause });
39
+ }
40
+ }));
41
+ }
42
+ /** A new attempt's init scripts: the configured ones, to which `browser.addInitScript` appends. */
43
+ forAttempt() {
44
+ return [...this.sources];
45
+ }
46
+ }
47
+ /**
48
+ * Validates one script `browser.addInitScript` was given and reads it into
49
+ * page source, a `path` through `resolvePath`. Only a function takes an
50
+ * argument, as in Playwright.
51
+ */
52
+ export async function testInitScriptSource(script, argument, resolvePath) {
53
+ assertInitScript(script, (problem) => new TestError('INVALID_ARGUMENT', `browser.addInitScript script ${problem}`));
54
+ if (argument !== undefined && typeof script !== 'function') {
55
+ throw new TestError('INVALID_ARGUMENT', 'browser.addInitScript takes an argument only with a function script');
56
+ }
57
+ validateJsonValue(argument?.arg, 'addInitScript argument');
58
+ try {
59
+ return await initScriptSource(script, argument?.arg, resolvePath);
60
+ }
61
+ catch (cause) {
62
+ throw new TestError('INVALID_ARGUMENT', `browser.addInitScript ${message(cause)}`, { cause });
63
+ }
64
+ }
65
+ /** The step label of a script: the file a `path` names, else its kind, never its source. */
66
+ export function initScriptLabel(script) {
67
+ if (typeof script === 'object' && script !== null && 'path' in script)
68
+ return String(script.path);
69
+ return typeof script === 'function' ? 'function' : 'source';
70
+ }
71
+ /** Throws what `fail` makes of the problem unless `script` is one init script. */
72
+ function assertInitScript(script, fail) {
73
+ if (typeof script === 'string' || typeof script === 'function')
74
+ return;
75
+ if (typeof script !== 'object' || script === null || Array.isArray(script)) {
76
+ const got = script === null ? 'null' : Array.isArray(script) ? 'an array' : typeof script;
77
+ throw fail(`must be a string of source, a { path }, or a function, got ${got}`);
78
+ }
79
+ const unknown = Object.keys(script).filter((key) => key !== 'path');
80
+ if (unknown.length > 0)
81
+ throw fail(`takes only path, got ${unknown.join(', ')}`);
82
+ if (!('path' in script) || typeof script.path !== 'string' || script.path === '')
83
+ throw fail('path must be a non-empty string');
84
+ }
85
+ /**
86
+ * The page source of one script. A function is called with its JSON argument
87
+ * as Playwright calls it, beside the `__name` helper tsx's output needs; the
88
+ * argument is parsed in the page, since an object literal would turn an own
89
+ * `__proto__` key into the prototype. A
90
+ * file gets a `sourceURL`, so a stack trace in the page names it.
91
+ */
92
+ async function initScriptSource(script, arg, resolvePath) {
93
+ if (typeof script === 'string')
94
+ return script;
95
+ if (typeof script === 'function') {
96
+ return `(() => {\n${KEEP_NAMES_HELPER}\n(${script.toString()}\n)(${arg === undefined ? '' : `JSON.parse(${JSON.stringify(JSON.stringify(arg))})`});\n})();`;
97
+ }
98
+ const file = resolvePath(script.path);
99
+ try {
100
+ return `${await readFile(file, 'utf8')}\n//# sourceURL=${file.replace(/[\r\n]/g, '')}`;
101
+ }
102
+ catch (cause) {
103
+ throw new Error(`path cannot be read: ${file} (${cause.code ?? message(cause)})`, { cause });
104
+ }
105
+ }
package/dist/install.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  /** First-run browser provisioning for the Playwright engine. */
2
2
  import { type BrowserName } from './browser-connection.ts';
3
+ /**
4
+ * Runs the pinned Playwright CLI with `args`, sharing this process's terminal,
5
+ * and resolves to its exit code, 128 plus the signal number when a signal
6
+ * ended it. SIGINT and SIGTERM sent to this process reach the child, so a
7
+ * cancelled CI job does not leave a download running. Backs the `e2e-web` command.
8
+ */
9
+ export declare function runPlaywrightCli(args: readonly string[]): Promise<number>;
3
10
  /** What an installer receives besides the browser names. */
4
11
  export interface InstallContext {
5
12
  /** Receives every line the installer prints: the download URL, progress, warnings. */
@@ -30,11 +37,10 @@ export interface EnsureBrowsersOptions {
30
37
  }
31
38
  /**
32
39
  * Ensures the given Playwright browsers are installed, downloading any
33
- * missing ones via `playwright install`. Called from the engine's `prepare`,
40
+ * missing ones via `playwright-core install`. Called from the engine's `prepare`,
34
41
  * once per run before any worker starts, so a download is never charged
35
42
  * against a launch timeout and never runs once per worker. Concurrent
36
43
  * installs (two runs at once) are still safe: the Playwright CLI serializes
37
44
  * them with a registry lock.
38
45
  */
39
46
  export declare function ensureBrowsersInstalled(names: readonly BrowserName[], options?: EnsureBrowsersOptions): Promise<void>;
40
- //# sourceMappingURL=install.d.ts.map