@e2e-dev/web 0.15.1 → 0.15.2
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/browser.js +1 -0
- package/dist/engine.js +34 -5
- package/dist/observation.js +1 -0
- package/dist/support.js +45 -5
- package/package.json +2 -2
package/dist/.build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.15.
|
|
1
|
+
{"version":"0.15.2"}
|
package/dist/browser.js
CHANGED
|
@@ -237,6 +237,7 @@ export function createBrowserFixture(surface, context) {
|
|
|
237
237
|
url: response.url(),
|
|
238
238
|
status: response.status(),
|
|
239
239
|
headers: response.headers(),
|
|
240
|
+
// oxlint-disable-next-line e2e/no-unchecked-json -- public API: the caller names T, unknown by default
|
|
240
241
|
json: async () => JSON.parse(await text()),
|
|
241
242
|
text,
|
|
242
243
|
};
|
package/dist/engine.js
CHANGED
|
@@ -36,8 +36,8 @@ export function web(options = {}) {
|
|
|
36
36
|
if (provider !== undefined && options.connect !== undefined) {
|
|
37
37
|
throw new ConfigurationError('INVALID_CONFIG', `web({ browser, connect }) names two browser sources; browser provider "${provider.name}" leases its own browsers, so remove connect`);
|
|
38
38
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
39
|
+
if (options.connect !== undefined)
|
|
40
|
+
validateConnect(options.connect);
|
|
41
41
|
if (options.connect !== undefined && typeof options.browser === 'string' && options.browser !== 'chromium') {
|
|
42
42
|
throw new ConfigurationError('INVALID_CONFIG', `web({ connect }) requires the chromium browser; CDP attach is chromium-only, got "${options.browser}"`);
|
|
43
43
|
}
|
|
@@ -59,9 +59,6 @@ export function web(options = {}) {
|
|
|
59
59
|
if (options.screencast !== undefined)
|
|
60
60
|
validateScreencast(options.screencast);
|
|
61
61
|
const reconnecting = options.connect?.reconnectEndpoint !== undefined;
|
|
62
|
-
if (reconnecting && typeof options.connect?.reconnectEndpoint !== 'function') {
|
|
63
|
-
throw new ConfigurationError('INVALID_CONFIG', 'connect.reconnectEndpoint must be a function');
|
|
64
|
-
}
|
|
65
62
|
// A per-attempt lease rides the same persistent context as `reconnectEndpoint`, with the same limits.
|
|
66
63
|
const recoverable = reconnecting || provider?.scope === 'attempt';
|
|
67
64
|
const mode = provider === undefined ? 'connect.reconnectEndpoint' : `browser provider "${provider.name}" with scope "attempt"`;
|
|
@@ -155,6 +152,38 @@ function persistentContextLimit(what, mode, leased) {
|
|
|
155
152
|
function isRecord(value) {
|
|
156
153
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
157
154
|
}
|
|
155
|
+
/** Names a value's kind for a config error without echoing it: a CDP URL can carry a session token. */
|
|
156
|
+
function kindOf(value) {
|
|
157
|
+
if (value === null)
|
|
158
|
+
return 'null';
|
|
159
|
+
if (Array.isArray(value))
|
|
160
|
+
return 'an array';
|
|
161
|
+
const type = typeof value;
|
|
162
|
+
return type === 'object' ? 'an object' : `a ${type}`;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* `cdpEndpoint` is the one resolver every connect mode needs: the shared
|
|
166
|
+
* browser's source at init, and the fresh browser each persistent attempt
|
|
167
|
+
* provisions. Without it the attach would call an undefined resolver, so a
|
|
168
|
+
* config missing it fails here, at load, instead of at worker init or on
|
|
169
|
+
* every attempt with `ENGINE_FAILURE: ... resolve is not a function`.
|
|
170
|
+
*/
|
|
171
|
+
function validateConnect(connect) {
|
|
172
|
+
if (!isRecord(connect)) {
|
|
173
|
+
throw new ConfigurationError('INVALID_CONFIG', `web({ connect }) must be a plain object: { cdpEndpoint, reconnectEndpoint? }, got ${kindOf(connect)}`);
|
|
174
|
+
}
|
|
175
|
+
rejectUnknownKeys('web({ connect })', connect, ['cdpEndpoint', 'reconnectEndpoint']);
|
|
176
|
+
const { cdpEndpoint, reconnectEndpoint } = connect;
|
|
177
|
+
if (cdpEndpoint === undefined) {
|
|
178
|
+
throw new ConfigurationError('INVALID_CONFIG', `web({ connect }) requires cdpEndpoint, the resolver that provisions the browser: (signal) => url${reconnectEndpoint === undefined ? '' : '; reconnectEndpoint only reconnects to the browser cdpEndpoint provisioned'}`);
|
|
179
|
+
}
|
|
180
|
+
if (typeof cdpEndpoint !== 'function') {
|
|
181
|
+
throw new ConfigurationError('INVALID_CONFIG', `web({ connect: { cdpEndpoint } }) must be a resolver function (signal) => url, got ${kindOf(cdpEndpoint)}`);
|
|
182
|
+
}
|
|
183
|
+
if (reconnectEndpoint !== undefined && typeof reconnectEndpoint !== 'function') {
|
|
184
|
+
throw new ConfigurationError('INVALID_CONFIG', `web({ connect: { reconnectEndpoint } }) must be a resolver function (signal) => url, got ${kindOf(reconnectEndpoint)}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
158
187
|
/**
|
|
159
188
|
* Refuses a header the browser could not send, at config load rather than at
|
|
160
189
|
* the first request: a name outside the token grammar, a value carrying a
|
package/dist/observation.js
CHANGED
|
@@ -83,6 +83,7 @@ async function captureInto(deps, scope, stage, host, framePath, budget, signal)
|
|
|
83
83
|
return data;
|
|
84
84
|
}));
|
|
85
85
|
const decoded = typeof metadata === 'string'
|
|
86
|
+
// oxlint-disable-next-line e2e/no-unchecked-json -- page-reported metadata, as unchecked as the structured branch; only the ids and nodes alignment below is checked
|
|
86
87
|
? JSON.parse(metadata)
|
|
87
88
|
: metadata;
|
|
88
89
|
const { nodes, ids, truncated: walkTruncated } = decoded;
|
package/dist/support.js
CHANGED
|
@@ -316,7 +316,7 @@ export async function performViewportSwipe(page, direction, momentum) {
|
|
|
316
316
|
const viewport = await currentViewport(page);
|
|
317
317
|
const distance = swipeDistance(direction === 'up' || direction === 'down' ? viewport.height : viewport.width, momentum);
|
|
318
318
|
const [deltaX, deltaY] = wheelDelta(direction, distance);
|
|
319
|
-
|
|
319
|
+
await wheelRendered(page, deltaX, deltaY);
|
|
320
320
|
}
|
|
321
321
|
/**
|
|
322
322
|
* The browser truncates a fractional pointer coordinate, so a point composed
|
|
@@ -354,10 +354,16 @@ export async function performPointDrag(mouse, start, end, durationMs, signal) {
|
|
|
354
354
|
}
|
|
355
355
|
}
|
|
356
356
|
export async function performElementSwipe(target, direction, momentum, timeout) {
|
|
357
|
-
//
|
|
358
|
-
//
|
|
359
|
-
|
|
360
|
-
|
|
357
|
+
// A node the pointer already hovers takes the wheel where it is; one hit at
|
|
358
|
+
// its centre takes it there, at the box that hit test read; otherwise hover
|
|
359
|
+
// waits for it to be actionable, scrolls it into view, and reports whatever
|
|
360
|
+
// intercepts the pointer.
|
|
361
|
+
const aimed = await (target.kind === 'locator'
|
|
362
|
+
? target.locator.evaluate(wheelAim, undefined, { timeout })
|
|
363
|
+
: target.element.evaluate(wheelAim));
|
|
364
|
+
if (aimed.aim === 'blocked')
|
|
365
|
+
await asActionable(target).hover({ timeout });
|
|
366
|
+
const box = aimed.aim === 'blocked' ? await targetBoundingBox(target, timeout) : aimed.box;
|
|
361
367
|
if (box === null) {
|
|
362
368
|
throw new EngineError('NOT_ACTIONABLE', 'element has no visible bounding box', {
|
|
363
369
|
retryable: false,
|
|
@@ -366,7 +372,41 @@ export async function performElementSwipe(target, direction, momentum, timeout)
|
|
|
366
372
|
const distance = swipeDistance(direction === 'up' || direction === 'down' ? box.height : box.width, momentum);
|
|
367
373
|
const [deltaX, deltaY] = wheelDelta(direction, distance);
|
|
368
374
|
const page = await targetPage(target);
|
|
375
|
+
if (aimed.aim === 'centre')
|
|
376
|
+
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
|
377
|
+
await wheelRendered(page, deltaX, deltaY);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Wheels the page and resolves once a frame rendered the scroll it caused.
|
|
381
|
+
* The scroll lands on the compositor and reaches script on the next frame, so
|
|
382
|
+
* a read right after a bare `mouse.wheel` can see the document it left. A
|
|
383
|
+
* hidden document renders no frames, so it resolves at once.
|
|
384
|
+
*/
|
|
385
|
+
async function wheelRendered(page, deltaX, deltaY) {
|
|
369
386
|
await page.mouse.wheel(deltaX, deltaY);
|
|
387
|
+
await page.evaluate(nextRenderedFrame);
|
|
388
|
+
}
|
|
389
|
+
/** Page-side: resolves after the next rendered frame, or at once in a hidden document. */
|
|
390
|
+
function nextRenderedFrame() {
|
|
391
|
+
if (document.visibilityState === 'hidden')
|
|
392
|
+
return Promise.resolve();
|
|
393
|
+
return new Promise((resolve) => requestAnimationFrame(() => window.setTimeout(resolve, 0)));
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Page-side: where a wheel reaches `element` in a top-level document, whose
|
|
397
|
+
* box is the page's: at the pointer when it already hovers the element, at
|
|
398
|
+
* its centre when the hit target there is the element or inside it, or
|
|
399
|
+
* `blocked` when neither holds.
|
|
400
|
+
*/
|
|
401
|
+
function wheelAim(element) {
|
|
402
|
+
if (window !== window.top || element.getClientRects().length === 0)
|
|
403
|
+
return { aim: 'blocked' };
|
|
404
|
+
const rect = element.getBoundingClientRect();
|
|
405
|
+
const box = { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
|
|
406
|
+
if (element.matches(':hover'))
|
|
407
|
+
return { aim: 'pointer', box };
|
|
408
|
+
const hit = document.elementFromPoint(rect.x + rect.width / 2, rect.y + rect.height / 2);
|
|
409
|
+
return hit !== null && element.contains(hit) ? { aim: 'centre', box } : { aim: 'blocked' };
|
|
370
410
|
}
|
|
371
411
|
function swipeDistance(extent, momentum) {
|
|
372
412
|
const ratio = momentum === 'fast' ? 1.5 : momentum === 'slow' ? 0.75 : 0.5;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e2e-dev/web",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "Web engine for e2e agentic end-to-end testing, built on Playwright",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"e2e",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "26.6.2",
|
|
57
|
-
"e2e": "0.21.
|
|
57
|
+
"e2e": "0.21.2",
|
|
58
58
|
"@types/pngjs": "6.0.5",
|
|
59
59
|
"pngjs": "7.0.0",
|
|
60
60
|
"typescript": "7.0.2",
|