@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.
Files changed (53) hide show
  1. package/README.md +15 -9
  2. package/dist/.build.json +1 -1
  3. package/dist/actions.d.ts +9 -3
  4. package/dist/actions.js +87 -9
  5. package/dist/attempt-session.d.ts +4 -3
  6. package/dist/attempt-session.js +17 -14
  7. package/dist/bin.d.ts +2 -0
  8. package/dist/bin.js +3 -0
  9. package/dist/browser-connection.d.ts +7 -3
  10. package/dist/browser-connection.js +7 -1
  11. package/dist/browser.d.ts +44 -7
  12. package/dist/browser.js +135 -72
  13. package/dist/capture-scope.d.ts +1 -1
  14. package/dist/cdp-recovery.d.ts +1 -1
  15. package/dist/cdp-selectors.d.ts +1 -1
  16. package/dist/cli.d.ts +9 -0
  17. package/dist/cli.js +42 -0
  18. package/dist/dialogs.d.ts +1 -1
  19. package/dist/downloads.d.ts +1 -1
  20. package/dist/engine.d.ts +1 -1
  21. package/dist/engine.js +74 -16
  22. package/dist/evaluation.d.ts +8 -3
  23. package/dist/evaluation.js +10 -5
  24. package/dist/in-page/read-semantics.d.ts +1 -0
  25. package/dist/in-page/read-semantics.js +166 -40
  26. package/dist/index.d.ts +2 -1
  27. package/dist/init-scripts.d.ts +30 -0
  28. package/dist/init-scripts.js +105 -0
  29. package/dist/install.d.ts +14 -1
  30. package/dist/install.js +77 -20
  31. package/dist/locators.d.ts +1 -1
  32. package/dist/locators.js +30 -18
  33. package/dist/observation-capture.d.ts +1 -1
  34. package/dist/observation.d.ts +1 -1
  35. package/dist/observation.js +3 -3
  36. package/dist/observe.d.ts +1 -1
  37. package/dist/operation-budget.d.ts +14 -0
  38. package/dist/operation-budget.js +31 -0
  39. package/dist/protected-app.d.ts +20 -4
  40. package/dist/protected-app.js +35 -4
  41. package/dist/provider-video.d.ts +1 -1
  42. package/dist/read-node.d.ts +9 -2
  43. package/dist/route-options.d.ts +44 -0
  44. package/dist/route-options.js +125 -0
  45. package/dist/selector-engines.js +2 -0
  46. package/dist/shown-selector.d.ts +20 -0
  47. package/dist/shown-selector.js +27 -0
  48. package/dist/support.d.ts +18 -12
  49. package/dist/support.js +41 -20
  50. package/dist/surface.d.ts +46 -6
  51. package/dist/surface.js +60 -19
  52. package/dist/video.d.ts +1 -1
  53. 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
- if (!this.persistent)
115
- await this.acquireBrowser(info.signal);
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, and translates raw errors at the contract
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 raceAbort(() => fn(operation), operation.signal, label)
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 fails loud instead of acting
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.11.2",
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.15.0 <1",
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.16.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": ">=22.12.0"
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",