@e2e-dev/web 0.15.3-nightly-20261021080401 → 0.15.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/.build.json CHANGED
@@ -1 +1 @@
1
- {"version":"0.15.3-nightly-20261021080401"}
1
+ {"version":"0.15.3"}
@@ -152,9 +152,60 @@ export const readSemanticsFunction = (element, options) => {
152
152
  el.isContentEditable &&
153
153
  NON_HOST_TAGS.indexOf(el.tagName.toLowerCase()) === -1 &&
154
154
  !(el.parentElement instanceof HTMLElement && el.parentElement.isContentEditable);
155
+ const NAME_PROHIBITED_ROLES = [
156
+ 'caption', 'code', 'deletion', 'emphasis', 'generic', 'insertion', 'paragraph', 'presentation', 'strong',
157
+ 'subscript', 'superscript',
158
+ ];
159
+ /** Global ARIA attributes, each with the roles it does not count on, as Playwright lists them. */
160
+ const GLOBAL_ARIA_ATTRIBUTES = [
161
+ ['aria-atomic', []],
162
+ ['aria-busy', []],
163
+ ['aria-controls', []],
164
+ ['aria-current', []],
165
+ ['aria-describedby', []],
166
+ ['aria-details', []],
167
+ ['aria-dropeffect', []],
168
+ ['aria-flowto', []],
169
+ ['aria-grabbed', []],
170
+ ['aria-hidden', []],
171
+ ['aria-keyshortcuts', []],
172
+ ['aria-label', NAME_PROHIBITED_ROLES],
173
+ ['aria-labelledby', NAME_PROHIBITED_ROLES],
174
+ ['aria-live', []],
175
+ ['aria-owns', []],
176
+ ['aria-relevant', []],
177
+ ['aria-roledescription', ['generic']],
178
+ ];
179
+ /** Playwright's implicit role for the tags whose role some global ARIA attributes do not count on. */
180
+ const NAME_PROHIBITED_TAG_ROLES = {
181
+ caption: 'caption', code: 'code', del: 'deletion', em: 'emphasis', ins: 'insertion', p: 'paragraph',
182
+ strong: 'strong', sub: 'subscript', sup: 'superscript',
183
+ };
184
+ /** True when the element carries a global ARIA attribute that counts on `role`. */
185
+ const hasGlobalAriaAttribute = (el, role) => GLOBAL_ARIA_ATTRIBUTES.some(([name, prohibited]) => (role === null || prohibited.indexOf(role) === -1) && el.hasAttribute(name));
186
+ /** True when `tabindex` parses as a number, as Playwright reads it. */
187
+ const hasTabIndex = (el) => !Number.isNaN(Number(String(el.getAttribute('tabindex'))));
188
+ /** Whether a person can focus the element, as Playwright decides presentational-role conflicts. */
189
+ const isFocusable = (el) => {
190
+ const tag = el.tagName.toLowerCase();
191
+ if (['button', 'input', 'select', 'textarea', 'option', 'optgroup'].indexOf(tag) !== -1 && el.matches(':disabled')) {
192
+ return false;
193
+ }
194
+ if (hasTabIndex(el) || ['button', 'details', 'select', 'textarea'].indexOf(tag) !== -1)
195
+ return true;
196
+ if (tag === 'a' || tag === 'area')
197
+ return el.hasAttribute('href');
198
+ return tag === 'input' && !el.hidden;
199
+ };
200
+ /**
201
+ * WAI-ARIA presentational-role conflict resolution: `presentation` or
202
+ * `none` is ignored on a focusable element or one with a global ARIA
203
+ * attribute, and the tag's role stands.
204
+ */
205
+ const hasPresentationConflict = (el) => isFocusable(el) || hasGlobalAriaAttribute(el, NAME_PROHIBITED_TAG_ROLES[el.tagName.toLowerCase()] ?? null);
155
206
  const implicitRole = memoized((el) => {
156
207
  const explicit = explicitRole(el);
157
- if (explicit !== null) {
208
+ if (explicit !== null && !((explicit === 'presentation' || explicit === 'none') && hasPresentationConflict(el))) {
158
209
  // The vocabulary spells ARIA's `img` as `image`.
159
210
  return explicit === 'img' ? 'image' : explicit;
160
211
  }
@@ -176,7 +227,9 @@ export const readSemanticsFunction = (element, options) => {
176
227
  case 'textarea':
177
228
  return 'textbox';
178
229
  case 'img':
179
- return el.getAttribute('alt') === '' ? 'presentation' : 'image';
230
+ return el.getAttribute('alt') === '' && !el.getAttribute('title') && !hasGlobalAriaAttribute(el, null) && !hasTabIndex(el)
231
+ ? 'presentation'
232
+ : 'image';
180
233
  // An inline icon is a picture whether or not anything names it, as
181
234
  // Playwright reads it; Chrome ignores an unnamed one.
182
235
  case 'svg':
@@ -1061,6 +1114,16 @@ export const readSemanticsFunction = (element, options) => {
1061
1114
  * of the page is not in the style and stays listed.
1062
1115
  */
1063
1116
  const isInert = (el, style) => el.hasAttribute('inert') || style?.getPropertyValue('interactivity') === 'inert';
1117
+ /** A closed select has no option boxes; explicit exclusions still remove choices it does not offer. */
1118
+ const isOfferedOption = (option) => {
1119
+ if (isSkippedContent(option))
1120
+ return false;
1121
+ for (let current = option; current !== null && current.tagName.toLowerCase() !== 'select'; current = parentOrHostOf(current)) {
1122
+ if (hidesSubtree(current, styleOf(current)))
1123
+ return false;
1124
+ }
1125
+ return true;
1126
+ };
1064
1127
  /** Smallest side, in CSS pixels, an empty box must have to be worth reporting. */
1065
1128
  const MIN_BOX_SIDE = 12;
1066
1129
  /** True when a computed style paints something a person can see. */
@@ -1529,8 +1592,13 @@ export const readSemanticsFunction = (element, options) => {
1529
1592
  // has to guess the labels it may pick. The options are what the control
1530
1593
  // offers, so they are listed under it, bounded like any long list.
1531
1594
  if (tag === 'select') {
1532
- const choices = Array.from(el.options).slice(0, MAX_SELECT_OPTIONS);
1533
- for (const option of choices) {
1595
+ let offered = 0;
1596
+ for (const option of Array.from(el.options)) {
1597
+ if (offered >= MAX_SELECT_OPTIONS)
1598
+ break;
1599
+ if (!isOfferedOption(option))
1600
+ continue;
1601
+ offered += 1;
1534
1602
  if (nodes.length >= maxNodes) {
1535
1603
  truncated = true;
1536
1604
  return;
package/dist/locators.js CHANGED
@@ -164,6 +164,10 @@ function narrowedToVisible(located, query) {
164
164
  function positioned(locator, index) {
165
165
  return index === 'first' ? locator.first() : index === 'last' ? locator.last() : locator.nth(index);
166
166
  }
167
+ /** A projection whose matches are exactly `locator`'s, with nothing left for the surface to apply. */
168
+ function plain(locator) {
169
+ return { locator, displayValue: null, name: null, composable: null, steps: [], visible: false };
170
+ }
167
171
  const DISPLAY_VALUE_COMPOSITION_MESSAGE = 'displayValue queries cannot scope child queries or serve as a has-filter in this engine';
168
172
  /**
169
173
  * Projects a complete immutable expression onto a Playwright locator within
@@ -190,6 +194,11 @@ const DISPLAY_VALUE_COMPOSITION_MESSAGE = 'displayValue queries cannot scope chi
190
194
  */
191
195
  function project(scope, expression, testIdAttribute) {
192
196
  switch (expression.kind) {
197
+ case 'union': {
198
+ const left = requireComposable(project(scope, expression.left, testIdAttribute));
199
+ const right = requireComposable(project(scope, expression.right, testIdAttribute));
200
+ return plain(left.or(right));
201
+ }
193
202
  case 'query': {
194
203
  const inner = expression.scope === undefined
195
204
  ? scope
@@ -233,24 +242,10 @@ function project(scope, expression, testIdAttribute) {
233
242
  steps: [...source.steps, { kind: 'index', index: expression.index }],
234
243
  };
235
244
  }
236
- return {
237
- locator: positioned(source.locator, expression.index),
238
- displayValue: null,
239
- name: null,
240
- composable: null,
241
- steps: [],
242
- visible: false,
243
- };
245
+ return plain(positioned(source.locator, expression.index));
244
246
  }
245
247
  case 'selector':
246
- return {
247
- locator: scope.locator(expression.selector),
248
- displayValue: null,
249
- name: null,
250
- composable: null,
251
- steps: [],
252
- visible: false,
253
- };
248
+ return plain(scope.locator(expression.selector));
254
249
  case 'frame':
255
250
  return project(scope.frameLocator(expression.selector), expression.source, testIdAttribute);
256
251
  }
@@ -306,6 +301,16 @@ export async function applyPostSteps(matches, steps, passesFilter) {
306
301
  /** Returns every frame selector along an expression, outermost first. */
307
302
  export function frameSelectors(expression) {
308
303
  switch (expression.kind) {
304
+ case 'union': {
305
+ const left = frameSelectors(expression.left);
306
+ const right = frameSelectors(expression.right);
307
+ if (left.length !== right.length || left.some((selector, index) => selector !== right[index])) {
308
+ throw new EngineError('UNSUPPORTED_CAPABILITY', 'locator.or requires both branches in the same document', {
309
+ retryable: false,
310
+ });
311
+ }
312
+ return left;
313
+ }
309
314
  case 'query':
310
315
  return expression.scope === undefined ? [] : frameSelectors(expression.scope);
311
316
  case 'filter':
package/dist/support.js CHANGED
@@ -290,7 +290,7 @@ export function isNavigationRace(cause) {
290
290
  * be captured in time is not a race.
291
291
  */
292
292
  export function navigationStaleOr(cause, operation) {
293
- if (!isClassified(cause) && isNavigationRace(cause)) {
293
+ if (!isClassified(cause) && !isPwTimeout(cause) && isNavigationRace(cause)) {
294
294
  return new EngineError('NODE_STALE', `${operation}: ${message(cause)}`, {
295
295
  retryable: true,
296
296
  cause,
@@ -304,8 +304,8 @@ export function navigationStaleOr(cause, operation) {
304
304
  * waits, so a `TimeoutError` there is a budget that ran out, not a miss.
305
305
  */
306
306
  export function staleOr(cause, operation) {
307
- if (isClassified(cause))
308
- return cause;
307
+ if (isClassified(cause) || isPwTimeout(cause))
308
+ return translatePwError(cause, operation);
309
309
  const text = message(cause);
310
310
  if (STALE_PATTERN.test(text) || isNavigationRace(cause)) {
311
311
  return new EngineError('NODE_STALE', `${operation}: ${text}`, { retryable: true, cause });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e2e-dev/web",
3
- "version": "0.15.3-nightly-20261021080401",
3
+ "version": "0.15.3",
4
4
  "description": "Web engine for e2e agentic end-to-end testing, built on Playwright",
5
5
  "keywords": [
6
6
  "e2e",
@@ -50,11 +50,11 @@
50
50
  "playwright-core": "1.64.1"
51
51
  },
52
52
  "peerDependencies": {
53
- "e2e": "0.21.3-nightly-20261021080401"
53
+ "e2e": ">=0.18.0 <1"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/node": "26.6.2",
57
- "e2e": "0.21.3-nightly-20261021080401",
57
+ "e2e": "0.21.3",
58
58
  "@types/pngjs": "6.0.5",
59
59
  "pngjs": "7.0.0",
60
60
  "typescript": "7.0.2",