@e2e-dev/web 0.11.2 → 0.13.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.
- package/README.md +15 -9
- package/dist/.build.json +1 -1
- package/dist/actions.d.ts +9 -3
- package/dist/actions.js +87 -9
- package/dist/attempt-session.d.ts +4 -3
- package/dist/attempt-session.js +17 -14
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/browser-connection.d.ts +7 -3
- package/dist/browser-connection.js +7 -1
- package/dist/browser.d.ts +44 -7
- package/dist/browser.js +135 -72
- package/dist/capture-scope.d.ts +1 -1
- package/dist/cdp-recovery.d.ts +1 -1
- package/dist/cdp-selectors.d.ts +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +42 -0
- package/dist/dialogs.d.ts +1 -1
- package/dist/downloads.d.ts +1 -1
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +74 -16
- package/dist/evaluation.d.ts +8 -3
- package/dist/evaluation.js +10 -5
- package/dist/in-page/read-semantics.d.ts +1 -0
- package/dist/in-page/read-semantics.js +166 -40
- package/dist/index.d.ts +2 -1
- package/dist/init-scripts.d.ts +30 -0
- package/dist/init-scripts.js +105 -0
- package/dist/install.d.ts +14 -1
- package/dist/install.js +77 -20
- package/dist/locators.d.ts +1 -1
- package/dist/locators.js +30 -18
- package/dist/observation-capture.d.ts +1 -1
- package/dist/observation.d.ts +1 -1
- package/dist/observation.js +3 -3
- package/dist/observe.d.ts +1 -1
- package/dist/operation-budget.d.ts +14 -0
- package/dist/operation-budget.js +31 -0
- package/dist/protected-app.d.ts +20 -4
- package/dist/protected-app.js +35 -4
- package/dist/provider-video.d.ts +1 -1
- package/dist/read-node.d.ts +9 -2
- package/dist/route-options.d.ts +44 -0
- package/dist/route-options.js +125 -0
- package/dist/selector-engines.js +2 -0
- package/dist/shown-selector.d.ts +20 -0
- package/dist/shown-selector.js +27 -0
- package/dist/support.d.ts +18 -12
- package/dist/support.js +41 -20
- package/dist/surface.d.ts +46 -6
- package/dist/surface.js +60 -19
- package/dist/video.d.ts +1 -1
- package/package.json +10 -6
package/dist/surface.js
CHANGED
|
@@ -10,7 +10,7 @@ import { mkdirSync } from 'node:fs';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { EngineError, raceAbort, TestError, withinCleanupBudget, } from 'e2e/engine';
|
|
12
12
|
import { matchesText } from 'e2e/engine';
|
|
13
|
-
import { classifyActionError, dispatchLocatorAction, dispatchPointerAction } from './actions.js';
|
|
13
|
+
import { classifyActionError, classifyInputError, dispatchLocatorAction, dispatchPointerAction } from './actions.js';
|
|
14
14
|
import { BrowserConnection, connectCdp } from './browser-connection.js';
|
|
15
15
|
import { AttemptSession } from './attempt-session.js';
|
|
16
16
|
import { LeasedBrowsers } from './provider.js';
|
|
@@ -20,11 +20,12 @@ import { applyPostSteps, frameSelectors, projectExpression } from './locators.js
|
|
|
20
20
|
import { ROOT_NODE_ID, toSemanticNode } from './observation.js';
|
|
21
21
|
import { captureObservation } from './observation-capture.js';
|
|
22
22
|
import { maskOptions, secureFieldMasks } from './observe.js';
|
|
23
|
-
import { connectionAbort } from './operation-budget.js';
|
|
23
|
+
import { connectionAbort, withOperationDeadline } from './operation-budget.js';
|
|
24
24
|
import { CLOSED_SHADOW_ROOTS_INIT_SCRIPT } from './closed-shadow.js';
|
|
25
|
+
import { ConfiguredInitScripts } from './init-scripts.js';
|
|
25
26
|
import { SECURE_FIELD_SELECTOR } from './read-node.js';
|
|
26
27
|
import { readSelector, takeReadsFunction } from './read-selector.js';
|
|
27
|
-
import { httpCredentials, installSiteHeaders, lowercaseNames } from './protected-app.js';
|
|
28
|
+
import { httpCredentials, installSiteHeaders, lowercaseNames, siteHeadersFor } from './protected-app.js';
|
|
28
29
|
import { RefRegistry } from './refs.js';
|
|
29
30
|
import { cancelled, DEFAULT_VIEWPORT, ErrorLatch, invalidState, message, navigationStaleOr, performViewportSwipe, sanitizeFilename, staleOr, translatePwError, } from './support.js';
|
|
30
31
|
/** Browser launch budget when the harness init budget is not otherwise expressed. */
|
|
@@ -62,7 +63,13 @@ export class PlaywrightSurface {
|
|
|
62
63
|
basicAuth;
|
|
63
64
|
testIdAttribute;
|
|
64
65
|
userAgent;
|
|
66
|
+
locale;
|
|
67
|
+
timezoneId;
|
|
68
|
+
configuredInitScripts;
|
|
69
|
+
/** The attempt's init scripts, configured then added, applied to each context the attempt opens. */
|
|
70
|
+
initScripts = [];
|
|
65
71
|
app = {};
|
|
72
|
+
projectRoot = '';
|
|
66
73
|
headed = false;
|
|
67
74
|
artifactsDir = '';
|
|
68
75
|
artifactCounter = 0;
|
|
@@ -83,6 +90,9 @@ export class PlaywrightSurface {
|
|
|
83
90
|
this.basicAuth = options.basicAuth;
|
|
84
91
|
this.testIdAttribute = options.testIdAttribute ?? DEFAULT_TEST_ID_ATTRIBUTE;
|
|
85
92
|
this.userAgent = options.userAgent;
|
|
93
|
+
this.locale = options.locale;
|
|
94
|
+
this.timezoneId = options.timezoneId;
|
|
95
|
+
this.configuredInitScripts = new ConfiguredInitScripts(options.initScripts ?? []);
|
|
86
96
|
}
|
|
87
97
|
// --- lifecycle ---
|
|
88
98
|
/**
|
|
@@ -90,14 +100,16 @@ export class PlaywrightSurface {
|
|
|
90
100
|
* leases the run's browsers from a provider. A CDP attach uses the
|
|
91
101
|
* remote's browser, so only a local launch needs the browser here. The
|
|
92
102
|
* download narrates through `info.log` and is bounded by the run's
|
|
93
|
-
* interrupt alone, never by a launch budget.
|
|
103
|
+
* interrupt alone, never by a launch budget. Init scripts are read first
|
|
104
|
+
* so a missing file fails the run before any worker starts.
|
|
94
105
|
*/
|
|
95
106
|
async prepare(info) {
|
|
107
|
+
await this.configuredInitScripts.load(info.projectRoot);
|
|
96
108
|
if (this.leases !== undefined)
|
|
97
109
|
return this.leases.prepare(info);
|
|
98
110
|
if (this.connect !== undefined)
|
|
99
111
|
return;
|
|
100
|
-
await ensureBrowsersInstalled([this.browserName], { env: info.env, signal: info.signal, log: info.log });
|
|
112
|
+
await ensureBrowsersInstalled([this.browserName], { env: info.env, signal: info.signal, log: info.log, headed: info.headed });
|
|
101
113
|
}
|
|
102
114
|
/** Releases the browsers `prepare` leased; a local launch or a `connect` has nothing to release. */
|
|
103
115
|
async finish(info) {
|
|
@@ -106,13 +118,20 @@ export class PlaywrightSurface {
|
|
|
106
118
|
/** Provisions the shared browser once per worker: a local launch, or a CDP attach. */
|
|
107
119
|
async init(info) {
|
|
108
120
|
this.app = info.app;
|
|
121
|
+
this.projectRoot = info.projectRoot;
|
|
109
122
|
this.headed = info.headed;
|
|
110
123
|
this.leases?.init(info);
|
|
111
124
|
// The browser was installed in `prepare`; a launch or attach is the one
|
|
112
125
|
// boot step left that can outlive a launch budget, and it honours the
|
|
113
|
-
// init signal.
|
|
114
|
-
|
|
115
|
-
|
|
126
|
+
// init signal. A failed init-script read throws only after the launch
|
|
127
|
+
// settles, so `dispose` owns the browser.
|
|
128
|
+
const settled = await Promise.allSettled([
|
|
129
|
+
this.configuredInitScripts.load(info.projectRoot),
|
|
130
|
+
this.persistent ? undefined : this.acquireBrowser(info.signal),
|
|
131
|
+
]);
|
|
132
|
+
for (const outcome of settled)
|
|
133
|
+
if (outcome.status === 'rejected')
|
|
134
|
+
throw outcome.reason;
|
|
116
135
|
}
|
|
117
136
|
/** Whether attempts ride a persistent remote context, provisioned per attempt, instead of contexts on one shared browser. */
|
|
118
137
|
get persistent() {
|
|
@@ -217,9 +236,11 @@ export class PlaywrightSurface {
|
|
|
217
236
|
this.artifactsDir = context.artifactsDir;
|
|
218
237
|
this.artifactCounter = 0;
|
|
219
238
|
const routes = [];
|
|
239
|
+
const initScripts = this.configuredInitScripts.forAttempt();
|
|
220
240
|
this.latch = new ErrorLatch();
|
|
221
241
|
const dialogs = new DialogRouter(this.latch);
|
|
222
242
|
this.routes = routes;
|
|
243
|
+
this.initScripts = initScripts;
|
|
223
244
|
this.dialogs = dialogs;
|
|
224
245
|
const session = new AttemptSession({
|
|
225
246
|
artifactsDir: context.artifactsDir,
|
|
@@ -231,10 +252,14 @@ export class PlaywrightSurface {
|
|
|
231
252
|
acceptDownloads: true,
|
|
232
253
|
...(credentials === undefined ? {} : { httpCredentials: credentials }),
|
|
233
254
|
...(this.userAgent === undefined ? {} : { userAgent: this.userAgent }),
|
|
255
|
+
...(this.locale === undefined ? {} : { locale: this.locale }),
|
|
256
|
+
...(this.timezoneId === undefined ? {} : { timezoneId: this.timezoneId }),
|
|
234
257
|
...(this.headers === undefined ? {} : { serviceWorkers: 'block' }),
|
|
235
258
|
},
|
|
236
259
|
configure: async (target) => {
|
|
237
260
|
await target.addInitScript(CLOSED_SHADOW_ROOTS_INIT_SCRIPT);
|
|
261
|
+
for (const script of initScripts)
|
|
262
|
+
await target.addInitScript(script);
|
|
238
263
|
target.setDefaultTimeout(CONTEXT_DEFAULT_TIMEOUT_MS);
|
|
239
264
|
target.on('dialog', (dialog) => { void dialogs.dispatch(dialog); });
|
|
240
265
|
await installSiteHeaders(target, this.app.site, this.headers);
|
|
@@ -303,6 +328,12 @@ export class PlaywrightSurface {
|
|
|
303
328
|
throw invalidState('no attempt is running');
|
|
304
329
|
return this.session;
|
|
305
330
|
}
|
|
331
|
+
/** Adds one attempt-scoped init script to the current context, for every document it creates from now on. */
|
|
332
|
+
async addInitScript(source) {
|
|
333
|
+
const context = this.requireContext();
|
|
334
|
+
this.initScripts.push(source);
|
|
335
|
+
await context.addInitScript(source);
|
|
336
|
+
}
|
|
306
337
|
// --- network routes shared with the browser fixture ---
|
|
307
338
|
/** Registers one attempt-scoped route on the current context. */
|
|
308
339
|
async route(predicate, handler) {
|
|
@@ -310,6 +341,14 @@ export class PlaywrightSurface {
|
|
|
310
341
|
this.routes.push({ predicate, handler });
|
|
311
342
|
await context.route(predicate, handler);
|
|
312
343
|
}
|
|
344
|
+
/** The configured headers a request to `url` carries, lowercased; `undefined` off the app's site. */
|
|
345
|
+
siteHeaders(url) {
|
|
346
|
+
return siteHeadersFor(url, this.app.site, this.headers);
|
|
347
|
+
}
|
|
348
|
+
/** Resolves a file a test names against the project root, as config paths do, never `process.cwd()`. */
|
|
349
|
+
projectPath(file) {
|
|
350
|
+
return path.resolve(this.projectRoot, file);
|
|
351
|
+
}
|
|
313
352
|
/** Removes one registered route from the attempt and the current context. */
|
|
314
353
|
async unroute(predicate, handler) {
|
|
315
354
|
const context = this.requireContext();
|
|
@@ -356,16 +395,18 @@ export class PlaywrightSurface {
|
|
|
356
395
|
* The single entry of every operation: rethrows an error latched on an
|
|
357
396
|
* unawaited path, refuses a cancelled operation, races `fn` against the
|
|
358
397
|
* operation signal so an abort mid-call surfaces as `CANCELLED` instead of
|
|
359
|
-
* waiting out Playwright,
|
|
398
|
+
* waiting out Playwright, bounds it by the operation's budget so a call a
|
|
399
|
+
* hung page never answers is `OPERATION_TIMEOUT`, and translates raw errors at the contract
|
|
360
400
|
* boundary. `translate` overrides the default translation for operations
|
|
361
|
-
* with a documented retryable failure mode
|
|
401
|
+
* with a documented retryable failure mode; `bound` is `test-code` for an
|
|
402
|
+
* operation that runs the test's own code, which the deadline never cuts.
|
|
362
403
|
*/
|
|
363
|
-
async guard(operation, label, fn, translate = translatePwError) {
|
|
404
|
+
async guard(operation, label, fn, translate = translatePwError, bound = 'deadline') {
|
|
364
405
|
this.latch.throwPending();
|
|
365
406
|
try {
|
|
366
407
|
return this.session === undefined
|
|
367
|
-
? await
|
|
368
|
-
: await this.session.run(operation, label, fn);
|
|
408
|
+
? await withOperationDeadline(operation, label, (remaining) => fn({ ...operation, ...remaining() }), bound)
|
|
409
|
+
: await this.session.run(operation, label, fn, bound);
|
|
369
410
|
}
|
|
370
411
|
catch (cause) {
|
|
371
412
|
throw translate(cause, label);
|
|
@@ -480,7 +521,7 @@ export class PlaywrightSurface {
|
|
|
480
521
|
return matches.map(({ raw, index }) => {
|
|
481
522
|
const pinned = handles?.[index];
|
|
482
523
|
// A single match keeps the strict locator, so a ref that turns
|
|
483
|
-
// ambiguous between locate and perform
|
|
524
|
+
// ambiguous between locate and perform is stale instead of acting
|
|
484
525
|
// on whichever element is first.
|
|
485
526
|
const locator = reads.length === 1 ? projected.locator : projected.locator.nth(index);
|
|
486
527
|
const id = refs.storeLocated(pinned === undefined ? { kind: 'locator', locator } : { kind: 'element', element: pinned });
|
|
@@ -506,10 +547,10 @@ export class PlaywrightSurface {
|
|
|
506
547
|
}
|
|
507
548
|
/** One pointer action at a viewport point in CSS pixels, with nothing resolved behind it; see `dispatchPointerAction`. */
|
|
508
549
|
performAt(point, action, operation) {
|
|
509
|
-
return this.guard(operation, `${action.kind} at point`, () => {
|
|
550
|
+
return this.guard(operation, `${action.kind} at point`, (currentOperation) => {
|
|
510
551
|
this.requireSession().requireObservation();
|
|
511
|
-
return dispatchPointerAction(this.requirePage(), point, action);
|
|
512
|
-
});
|
|
552
|
+
return dispatchPointerAction(this.requirePage(), point, action, currentOperation.signal);
|
|
553
|
+
}, classifyInputError);
|
|
513
554
|
}
|
|
514
555
|
/**
|
|
515
556
|
* Types into whatever holds focus. Keystrokes into a document body are
|
|
@@ -539,14 +580,14 @@ export class PlaywrightSurface {
|
|
|
539
580
|
checkpoint();
|
|
540
581
|
}
|
|
541
582
|
await page.keyboard.type(text);
|
|
542
|
-
});
|
|
583
|
+
}, classifyInputError);
|
|
543
584
|
}
|
|
544
585
|
/** Sends one key to whatever holds focus, in the contract's key grammar Playwright shares. */
|
|
545
586
|
pressKey(key, operation) {
|
|
546
587
|
return this.guard(operation, 'keyboard.press', () => {
|
|
547
588
|
this.requireSession().requireObservation();
|
|
548
589
|
return this.requirePage().keyboard.press(key);
|
|
549
|
-
});
|
|
590
|
+
}, classifyInputError);
|
|
550
591
|
}
|
|
551
592
|
/**
|
|
552
593
|
* Refuses focused typing when nothing that takes keystrokes has focus, in
|
package/dist/video.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* timestamps on it. The first segment is `video/video.webm`; later ones are
|
|
14
14
|
* `video/video-part<n>.webm`. Without a recording, every hook here is a no-op.
|
|
15
15
|
*/
|
|
16
|
-
import type { Page } from 'playwright';
|
|
16
|
+
import type { Page } from 'playwright-core';
|
|
17
17
|
import { type VideoFile, type VideoSegment } from 'e2e/engine';
|
|
18
18
|
import type { WebScreencastOptions } from './surface.ts';
|
|
19
19
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e2e-dev/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Web engine for e2e agentic end-to-end testing, built on Playwright",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"e2e",
|
|
@@ -39,25 +39,29 @@
|
|
|
39
39
|
"default": "./dist/index.js"
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
|
+
"bin": {
|
|
43
|
+
"e2e-web": "./dist/bin.js"
|
|
44
|
+
},
|
|
42
45
|
"files": [
|
|
43
46
|
"dist",
|
|
44
47
|
"NOTICE"
|
|
45
48
|
],
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"playwright-core": "1.63.0"
|
|
51
|
+
},
|
|
46
52
|
"peerDependencies": {
|
|
47
|
-
"e2e": ">=0.
|
|
48
|
-
"playwright": ">=1.63.0 <2"
|
|
53
|
+
"e2e": ">=0.18.0 <1"
|
|
49
54
|
},
|
|
50
55
|
"devDependencies": {
|
|
51
56
|
"@types/node": "26.6.2",
|
|
52
|
-
"e2e": "0.
|
|
57
|
+
"e2e": "0.18.0",
|
|
53
58
|
"@types/pngjs": "6.0.5",
|
|
54
59
|
"pngjs": "7.0.0",
|
|
55
|
-
"playwright": "1.63.0",
|
|
56
60
|
"typescript": "7.0.2",
|
|
57
61
|
"vitest": "5.0.1"
|
|
58
62
|
},
|
|
59
63
|
"engines": {
|
|
60
|
-
"node": "
|
|
64
|
+
"node": "^22.22.3 || >=24.8.0"
|
|
61
65
|
},
|
|
62
66
|
"scripts": {
|
|
63
67
|
"build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc --project tsconfig.build.json && node ../../scripts/stamp-dist.ts",
|