@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 +1 -1
- package/dist/in-page/read-semantics.js +72 -4
- package/dist/locators.js +21 -16
- package/dist/support.js +3 -3
- package/package.json +3 -3
package/dist/.build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.15.3
|
|
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') === ''
|
|
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
|
-
|
|
1533
|
-
for (const option of
|
|
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
|
|
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.
|
|
53
|
+
"e2e": ">=0.18.0 <1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "26.6.2",
|
|
57
|
-
"e2e": "0.21.3
|
|
57
|
+
"e2e": "0.21.3",
|
|
58
58
|
"@types/pngjs": "6.0.5",
|
|
59
59
|
"pngjs": "7.0.0",
|
|
60
60
|
"typescript": "7.0.2",
|