e2e 0.17.0 → 0.18.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 (166) hide show
  1. package/README.md +10 -0
  2. package/dist/.build.json +1 -1
  3. package/dist/agent/ai-sdk.js +5 -4
  4. package/dist/agent/error.js +3 -3
  5. package/dist/agent/invocation.js +3 -4
  6. package/dist/agent/model/tool-choice.d.ts +9 -0
  7. package/dist/agent/model/tool-choice.js +14 -0
  8. package/dist/agent/observation-feed.js +3 -4
  9. package/dist/agent/phases.d.ts +7 -1
  10. package/dist/agent/phases.js +12 -3
  11. package/dist/agent/prompts.js +3 -0
  12. package/dist/agent/replay.js +18 -15
  13. package/dist/agent/step-cache.d.ts +2 -1
  14. package/dist/agent/step-cache.js +9 -5
  15. package/dist/agent/tool-loop.js +92 -5
  16. package/dist/cache/trace.js +6 -2
  17. package/dist/cli/bin.js +2 -2
  18. package/dist/cli/index.js +35 -24
  19. package/dist/cli/init/mcp-config.d.ts +2 -1
  20. package/dist/cli/init/mcp-config.js +8 -3
  21. package/dist/cli/init/sibling-versions.json +1 -1
  22. package/dist/cli/mcp.d.ts +3 -2
  23. package/dist/cli/mcp.js +52 -16
  24. package/dist/cli/run-output.d.ts +17 -0
  25. package/dist/cli/run-output.js +45 -0
  26. package/dist/collect/collect.js +8 -4
  27. package/dist/collect/registry.js +27 -16
  28. package/dist/config/compiled-files.d.ts +36 -0
  29. package/dist/config/compiled-files.js +50 -0
  30. package/dist/config/diagnose.js +21 -19
  31. package/dist/config/esm-hooks.d.ts +39 -44
  32. package/dist/config/esm-hooks.js +238 -63
  33. package/dist/config/load.d.ts +1 -7
  34. package/dist/config/load.js +23 -66
  35. package/dist/config/register.d.ts +8 -0
  36. package/dist/config/register.js +9 -0
  37. package/dist/config/specifiers.d.ts +19 -0
  38. package/dist/config/specifiers.js +33 -0
  39. package/dist/config/tsconfig.d.ts +56 -0
  40. package/dist/config/tsconfig.js +115 -0
  41. package/dist/config/typescript.d.ts +19 -0
  42. package/dist/config/typescript.js +253 -0
  43. package/dist/engine/contract.d.ts +1 -0
  44. package/dist/engine/index.d.ts +2 -0
  45. package/dist/engine/session.js +2 -2
  46. package/dist/expect/async.js +2 -1
  47. package/dist/expect/poll.js +2 -2
  48. package/dist/internal/errors.d.ts +10 -3
  49. package/dist/internal/errors.js +23 -8
  50. package/dist/internal/node-version.d.ts +16 -7
  51. package/dist/internal/node-version.js +50 -15
  52. package/dist/internal/package-manager.js +1 -1
  53. package/dist/internal/realm-slot.d.ts +5 -4
  54. package/dist/internal/redact.d.ts +2 -0
  55. package/dist/internal/redact.js +4 -0
  56. package/dist/internal/time.d.ts +21 -5
  57. package/dist/internal/time.js +35 -3
  58. package/dist/locator/engine.js +47 -11
  59. package/dist/locator/screen.js +31 -5
  60. package/dist/mcp/output.d.ts +59 -0
  61. package/dist/mcp/output.js +129 -0
  62. package/dist/mcp/server.d.ts +4 -0
  63. package/dist/mcp/server.js +9 -12
  64. package/dist/mcp/session.d.ts +8 -1
  65. package/dist/mcp/session.js +55 -14
  66. package/dist/mcp/tools.js +7 -5
  67. package/dist/report/list.d.ts +11 -0
  68. package/dist/report/list.js +23 -6
  69. package/dist/run/artifacts.d.ts +4 -4
  70. package/dist/run/artifacts.js +3 -2
  71. package/dist/run/execute.d.ts +25 -5
  72. package/dist/run/execute.js +44 -20
  73. package/dist/run/failure-evidence.js +6 -2
  74. package/dist/run/in-process.js +51 -10
  75. package/dist/run/process-output.d.ts +73 -0
  76. package/dist/run/process-output.js +130 -0
  77. package/dist/run/provision.d.ts +2 -0
  78. package/dist/run/provision.js +1 -0
  79. package/dist/run/records.d.ts +4 -4
  80. package/dist/run/retry.d.ts +7 -0
  81. package/dist/run/retry.js +27 -11
  82. package/dist/run/runner.d.ts +8 -0
  83. package/dist/run/runner.js +32 -9
  84. package/dist/run/scheduler.js +255 -19
  85. package/dist/run/secrecy.d.ts +22 -19
  86. package/dist/run/secrecy.js +22 -18
  87. package/dist/run/serial.d.ts +73 -2
  88. package/dist/run/serial.js +239 -58
  89. package/dist/run/standalone.js +1 -1
  90. package/dist/run/trace-redaction.d.ts +5 -6
  91. package/dist/run/trace-redaction.js +5 -6
  92. package/dist/run/worker/entry.js +35 -14
  93. package/dist/run/worker/handle.js +10 -5
  94. package/dist/run/worker/protocol.d.ts +83 -3
  95. package/dist/run/worker/session.js +28 -0
  96. package/dist/telemetry/environment.d.ts +2 -1
  97. package/dist/telemetry/environment.js +12 -2
  98. package/dist/telemetry/project.d.ts +5 -6
  99. package/dist/telemetry/project.js +6 -11
  100. package/dist/telemetry/store.d.ts +5 -5
  101. package/dist/telemetry/store.js +5 -7
  102. package/dist/telemetry/telemetry.d.ts +2 -3
  103. package/dist/telemetry/telemetry.js +2 -4
  104. package/dist/types.d.ts +6 -3
  105. package/docs/agent-steps.mdx +1 -0
  106. package/docs/agents.mdx +1 -0
  107. package/docs/api-testing.mdx +2 -1
  108. package/docs/authentication.mdx +3 -2
  109. package/docs/browser.mdx +7 -6
  110. package/docs/bug-bash.mdx +2 -1
  111. package/docs/cache.mdx +2 -1
  112. package/docs/ci.mdx +3 -2
  113. package/docs/coding-agents.mdx +2 -1
  114. package/docs/core-concepts.mdx +208 -0
  115. package/docs/debugging.mdx +19 -9
  116. package/docs/decision-models.mdx +142 -0
  117. package/docs/example-projects.mdx +32 -0
  118. package/docs/executors.mdx +3 -0
  119. package/docs/explore.mdx +1 -0
  120. package/docs/github.mdx +2 -1
  121. package/docs/index.mdx +9 -5
  122. package/docs/integrations/eas.mdx +1 -0
  123. package/docs/integrations/index.mdx +1 -0
  124. package/docs/integrations/kernel.mdx +2 -1
  125. package/docs/migrate/cypress.mdx +26 -3
  126. package/docs/migrate/detox.mdx +24 -1
  127. package/docs/migrate/maestro.mdx +28 -4
  128. package/docs/migrate/playwright.mdx +27 -4
  129. package/docs/migrate/selenium.mdx +27 -3
  130. package/docs/mobile-ci.mdx +6 -5
  131. package/docs/mobile.mdx +79 -8
  132. package/docs/models.mdx +7 -2
  133. package/docs/quickstart.mdx +26 -15
  134. package/docs/reference/agent.mdx +13 -3
  135. package/docs/reference/app.mdx +1 -0
  136. package/docs/reference/cli.mdx +11 -5
  137. package/docs/reference/config.mdx +42 -6
  138. package/docs/reference/engine.mdx +22 -10
  139. package/docs/reference/environment.mdx +2 -1
  140. package/docs/reference/errors.mdx +11 -8
  141. package/docs/reference/expect.mdx +2 -1
  142. package/docs/reference/mcp.mdx +21 -8
  143. package/docs/reference/mobile.mdx +1 -0
  144. package/docs/reference/reporters.mdx +1 -0
  145. package/docs/reference/screen.mdx +19 -9
  146. package/docs/reference/test.mdx +21 -1
  147. package/docs/reference/web.mdx +8 -2
  148. package/docs/security.mdx +33 -27
  149. package/docs/subscriptions.mdx +1 -0
  150. package/docs/telemetry.mdx +8 -5
  151. package/docs/tools.mdx +1 -0
  152. package/docs/{starting-your-app.mdx → web.mdx} +157 -11
  153. package/docs/writing-an-engine.mdx +2 -1
  154. package/package.json +11 -7
  155. package/skills/e2e/SKILL.md +3 -1
  156. package/skills/e2e/references/agent.md +11 -0
  157. package/skills/e2e/references/debugging.md +1 -1
  158. package/skills/e2e/references/mcp.md +8 -3
  159. package/skills/e2e/references/running.md +2 -1
  160. package/skills/e2e/references/setup.md +9 -1
  161. package/skills/e2e/references/writing-tests.md +1 -1
  162. package/docs/assertions.mdx +0 -94
  163. package/docs/goals.mdx +0 -176
  164. package/docs/locators.mdx +0 -64
  165. package/docs/physical-devices.mdx +0 -76
  166. package/docs/viewports.mdx +0 -146
@@ -0,0 +1,115 @@
1
+ /**
2
+ * What the loader reads about the project besides the modules themselves:
3
+ * the tsconfig.json that governs a file (the nearest one above it, the way
4
+ * `tsc` finds it, with `extends` applied, or the project it references that
5
+ * includes the file) and which candidate files exist.
6
+ *
7
+ * Both are kept with the module graph that read them, the way the graph's
8
+ * modules are: a fresh graph (a config loaded with `graph: true`, which
9
+ * carries `e2e-graph` on every project file) reads its tsconfig.json and
10
+ * file lookups once, as they are when it loads. Every other load in the
11
+ * process shares one view, whose tsconfig.json is read once (collecting
12
+ * fifty test files reads the `extends` chain once) and whose file lookups
13
+ * ask the disk each time, as Node.js's own resolution does, so a test file
14
+ * imported again sees files added or removed since.
15
+ */
16
+ import { statSync } from 'node:fs';
17
+ import path from 'node:path';
18
+ import { createFilesMatcher, createPathsMatcher, findTsconfig, parseTsconfig } from 'get-tsconfig';
19
+ /** Carries the tsconfig.json files the runner warned about to its workers, so each file is reported once per run. */
20
+ export const WARNED_TSCONFIGS_ENV = 'E2E_WARNED_TSCONFIGS';
21
+ /** The tsconfig.json files a warning was printed for: in this process, or in the runner that spawned it. */
22
+ const warned = new Set((process.env[WARNED_TSCONFIGS_ENV] ?? '').split(path.delimiter).filter((file) => file !== ''));
23
+ /** The tsconfig.json files warned about so far, as the value of `WARNED_TSCONFIGS_ENV` for a worker. */
24
+ export function warnedTsconfigs() {
25
+ return [...warned].join(path.delimiter);
26
+ }
27
+ /** The tsconfig.json a project reference names: the file, or `tsconfig.json` in the directory. */
28
+ function referencedTsconfig(owner, reference) {
29
+ const target = path.resolve(path.dirname(owner), reference);
30
+ return target.endsWith('.json') ? target : path.join(target, 'tsconfig.json');
31
+ }
32
+ /** The project as one module graph sees it. */
33
+ export class ProjectView {
34
+ /** The nearest tsconfig.json of each directory looked up so far; undefined where none is above it. */
35
+ nearest = new Map();
36
+ /** Each tsconfig.json read so far; undefined for one that could not be read. */
37
+ read = new Map();
38
+ /** get-tsconfig's own cache of the files it read. */
39
+ reads = new Map();
40
+ /** File lookups answered so far, for a graph's view; undefined where the disk is asked each time. */
41
+ files;
42
+ constructor(memoizeFiles) {
43
+ this.files = memoizeFiles ? new Map() : undefined;
44
+ }
45
+ /**
46
+ * The tsconfig that governs `file`, or undefined when no readable
47
+ * tsconfig.json is above it. A solution-style tsconfig.json (`files: []`
48
+ * and `references`, as Vite writes) hands a file to the referenced
49
+ * tsconfig that includes it, the way TypeScript's editor support does.
50
+ */
51
+ tsconfigFor(file) {
52
+ const directory = path.dirname(file);
53
+ if (!this.nearest.has(directory))
54
+ this.nearest.set(directory, findTsconfig(directory, 'tsconfig.json', this.reads));
55
+ const nearest = this.nearest.get(directory);
56
+ const root = nearest === undefined ? undefined : this.readTsconfig(nearest);
57
+ if (root === undefined || root.references.length === 0 || root.includes(file) !== undefined)
58
+ return root?.tsconfig;
59
+ for (const reference of root.references) {
60
+ const referenced = this.readTsconfig(reference);
61
+ if (referenced !== undefined && referenced.includes(file) !== undefined)
62
+ return referenced.tsconfig;
63
+ }
64
+ return root.tsconfig;
65
+ }
66
+ /**
67
+ * `tsconfigPath` read with its `extends` chain, or undefined when it
68
+ * cannot be, with a warning: an `extends` naming a package that is not
69
+ * installed (`@tsconfig/node22`) should not stop a run, so TypeScript
70
+ * compiles with default settings and no `paths` instead.
71
+ */
72
+ readTsconfig(tsconfigPath) {
73
+ if (this.read.has(tsconfigPath))
74
+ return this.read.get(tsconfigPath);
75
+ let read;
76
+ try {
77
+ const result = { path: tsconfigPath, config: parseTsconfig(tsconfigPath, this.reads) };
78
+ read = {
79
+ tsconfig: { compilerOptions: result.config.compilerOptions ?? {}, paths: createPathsMatcher(result) },
80
+ includes: createFilesMatcher(result),
81
+ references: (result.config.references ?? []).map((reference) => referencedTsconfig(tsconfigPath, reference.path)),
82
+ };
83
+ }
84
+ catch (cause) {
85
+ if (!warned.has(tsconfigPath)) {
86
+ warned.add(tsconfigPath);
87
+ const reason = cause instanceof Error ? cause.message : String(cause);
88
+ process.emitWarning(`e2e ignores ${tsconfigPath}, which it cannot read (${reason}): TypeScript compiles with default settings and no paths`);
89
+ }
90
+ }
91
+ this.read.set(tsconfigPath, read);
92
+ return read;
93
+ }
94
+ /** Whether `file` is a regular file. */
95
+ isFile(file) {
96
+ let known = this.files?.get(file);
97
+ if (known === undefined) {
98
+ known = statSync(file, { throwIfNoEntry: false })?.isFile() === true;
99
+ this.files?.set(file, known);
100
+ }
101
+ return known;
102
+ }
103
+ }
104
+ /** One view per graph key; the empty key is every load outside a fresh graph. */
105
+ const views = new Map();
106
+ /** The view of the module graph `graph` names (the `e2e-graph` value of a URL), or the shared one for null. */
107
+ export function projectView(graph) {
108
+ const key = graph ?? '';
109
+ let view = views.get(key);
110
+ if (view === undefined) {
111
+ view = new ProjectView(graph !== null);
112
+ views.set(key, view);
113
+ }
114
+ return view;
115
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Compiles a TypeScript or JSX file to JavaScript this Node.js runs, with
3
+ * oxc. The output carries an inline source map, so stack traces, a test's
4
+ * location, and failure code frames point at the source. Syntax the running
5
+ * Node.js lacks (`using`, for one) is lowered for it, with helpers from e2e's
6
+ * own copy of `@oxc-project/runtime`, which the project need not install.
7
+ * The caller passes the compiler options that change what runs: JSX, legacy
8
+ * decorators, class field semantics, and import elision.
9
+ */
10
+ import { type CompiledExtension } from './compiled-files.ts';
11
+ import type { CompilerOptions } from './tsconfig.ts';
12
+ /**
13
+ * `source`, the content of `file` as the next loader handed it over,
14
+ * compiled to JavaScript for its kind with `compilerOptions`, with an
15
+ * inline source map. When an earlier loader already compiled the file and
16
+ * left its own inline map, the two are composed, so positions still lead to
17
+ * the file on disk.
18
+ */
19
+ export declare function compileTypeScript(file: string, source: string, kind: CompiledExtension, compilerOptions: CompilerOptions): string;
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Compiles a TypeScript or JSX file to JavaScript this Node.js runs, with
3
+ * oxc. The output carries an inline source map, so stack traces, a test's
4
+ * location, and failure code frames point at the source. Syntax the running
5
+ * Node.js lacks (`using`, for one) is lowered for it, with helpers from e2e's
6
+ * own copy of `@oxc-project/runtime`, which the project need not install.
7
+ * The caller passes the compiler options that change what runs: JSX, legacy
8
+ * decorators, class field semantics, and import elision.
9
+ */
10
+ import remapping, {} from '@jridgewell/remapping';
11
+ import { createRequire } from 'node:module';
12
+ import { pathToFileURL } from 'node:url';
13
+ import { InfrastructureError } from '../internal/errors.js';
14
+ import { HOOKED_REQUIRE_KEY } from './compiled-files.js';
15
+ const ownRequire = createRequire(import.meta.url);
16
+ /** oxc's transformer and parser, loaded on first use. */
17
+ let compiler;
18
+ /**
19
+ * oxc, loaded when the first file compiles rather than when e2e loads, so
20
+ * a command that compiles nothing (`--version`, `--help`) still runs
21
+ * without oxc's native binding. npm leaves that binding out with
22
+ * `--omit=optional`, and from a lockfile written on another platform
23
+ * (npm/cli#4828); the error names it and the way back.
24
+ */
25
+ function oxc() {
26
+ if (compiler !== undefined)
27
+ return compiler;
28
+ try {
29
+ compiler = { transform: ownRequire('oxc-transform'), parser: ownRequire('oxc-parser') };
30
+ }
31
+ catch (cause) {
32
+ const reason = (cause instanceof Error ? cause.message : String(cause)).split('\n')[0];
33
+ // napi-rs's loader says so when no platform package is installed.
34
+ const missingBinding = /Cannot find native binding/.test(String(cause?.message));
35
+ const platform = `${process.platform}-${process.arch}`;
36
+ const fix = missingBinding
37
+ ? `its native bindings for ${platform} (@oxc-transform/binding-${platform}* and @oxc-parser/binding-${platform}*) are not installed. npm skips it with --omit=optional, or when package-lock.json was written on another platform (npm/cli#4828): install again without --omit=optional, or delete package-lock.json and node_modules and run npm install`
38
+ : `it could not be loaded (${reason})`;
39
+ throw new InfrastructureError('TYPESCRIPT_COMPILER_UNAVAILABLE', `e2e compiles TypeScript with oxc, and ${fix}`, { cause });
40
+ }
41
+ return compiler;
42
+ }
43
+ /** The first ECMAScript edition that defines class fields rather than assigning them. */
44
+ const DEFINED_FIELDS_SINCE = 2022;
45
+ /** JSX as the tsconfig asks for it; classic `React.createElement` when it names no automatic runtime. */
46
+ function jsxOptions(options) {
47
+ if (options.jsx === 'react-jsx' || options.jsx === 'react-jsxdev') {
48
+ return { runtime: 'automatic', importSource: options.jsxImportSource ?? 'react', development: options.jsx === 'react-jsxdev' };
49
+ }
50
+ return {
51
+ runtime: 'classic',
52
+ pragma: options.jsxFactory ?? 'React.createElement',
53
+ pragmaFrag: options.jsxFragmentFactory ?? 'React.Fragment',
54
+ };
55
+ }
56
+ /**
57
+ * The ECMAScript year a tsconfig `target` names, or undefined for `ESNext`
58
+ * and an unset target. `ES6` is 2015; `ES3` and `ES5` count as 2009, the
59
+ * year of ES5.
60
+ */
61
+ function targetYear(target) {
62
+ const edition = /^es(\d+)$/i.exec(target ?? '')?.[1];
63
+ if (edition === undefined)
64
+ return undefined;
65
+ const number = Number(edition);
66
+ if (number === 6)
67
+ return 2015;
68
+ return number < 2015 ? 2009 : number;
69
+ }
70
+ /**
71
+ * `useDefineForClassFields` as TypeScript reads it: explicit, or false for a
72
+ * target below ES2022. An unset target keeps define semantics, as esbuild did.
73
+ */
74
+ function definesClassFields(options) {
75
+ if (options.useDefineForClassFields !== undefined)
76
+ return options.useDefineForClassFields;
77
+ const year = targetYear(options.target);
78
+ return year === undefined || year >= DEFINED_FIELDS_SINCE;
79
+ }
80
+ /** The oxc options a tsconfig's compiler options call for. */
81
+ function transformOptions(options) {
82
+ const assignsFields = !definesClassFields(options);
83
+ return {
84
+ jsx: jsxOptions(options),
85
+ typescript: {
86
+ onlyRemoveTypeImports: options.verbatimModuleSyntax === true,
87
+ removeClassFieldsWithoutInitializer: assignsFields,
88
+ },
89
+ assumptions: { setPublicClassFields: assignsFields },
90
+ ...(options.experimentalDecorators === true
91
+ ? { decorator: { legacy: true, emitDecoratorMetadata: options.emitDecoratorMetadata === true } }
92
+ : {}),
93
+ };
94
+ }
95
+ /** `file:line:column: message` for one oxc-transform error; it counts UTF-8 bytes, an editor counts characters. */
96
+ function describeError(file, source, error) {
97
+ const start = error.labels[0]?.start;
98
+ if (start === undefined)
99
+ return `${file}: ${error.message}`;
100
+ return `${file}:${position(source.subarray(0, start).toString('utf8'))}: ${error.message}`;
101
+ }
102
+ /** `line:column` just past `text`, both counted from 1. */
103
+ function position(text) {
104
+ const lines = text.split('\n');
105
+ return `${lines.length}:${lines.at(-1).length + 1}`;
106
+ }
107
+ /**
108
+ * Syntax oxc 0.152 passes through unchanged that Node.js cannot run, found
109
+ * before compiling so the error names the line and the fix instead of a
110
+ * bare `SyntaxError` from Node.js with no location. Each check only parses
111
+ * a file whose text could hold the construct.
112
+ */
113
+ function rejectUnsupportedSyntax(file, source, kind, options) {
114
+ const legacyDecorators = options.experimentalDecorators === true;
115
+ const commonJs = kind.format === 'commonjs';
116
+ // A decorator's `@` starts a line or follows whitespace or punctuation, which rules out a package import ('@scope/pkg').
117
+ const mayHold = (commonJs && /\b(?:import|export)\b/.test(source)) || /\baccessor\b/.test(source) || (!legacyDecorators && /(?:^|[\s(,;{}])@[A-Za-z_$]/m.test(source));
118
+ if (!mayHold)
119
+ return;
120
+ const parsed = oxc().parser.parseSync(file, source, { lang: kind.lang, sourceType: kind.format });
121
+ const problems = [];
122
+ if (commonJs) {
123
+ for (const declaration of parsed.module.staticImports) {
124
+ if (declaration.entries.length === 0 || declaration.entries.some((entry) => !entry.isType)) {
125
+ problems.push({ start: declaration.start, message: 'a .cts file is CommonJS, and e2e does not turn an import declaration into require(): write `import name = require(...)`, or `import type` for types' });
126
+ }
127
+ }
128
+ for (const declaration of parsed.module.staticExports) {
129
+ // `export interface`, `export type`, and `export declare` emit nothing, as tsc allows in .cts.
130
+ if (declaration.entries.every((entry) => entry.isType))
131
+ continue;
132
+ problems.push({ start: declaration.start, message: 'a .cts file is CommonJS, and e2e does not turn an export declaration into module.exports: write `export = ...`' });
133
+ }
134
+ }
135
+ const { Visitor } = oxc().parser;
136
+ new Visitor({
137
+ AccessorProperty(node) {
138
+ problems.push({ start: node.start, message: '`accessor` class fields (auto-accessors) are not supported: oxc, which compiles e2e\'s TypeScript, does not lower them yet; write a getter and setter over a private field' });
139
+ },
140
+ Decorator(node) {
141
+ if (!legacyDecorators) {
142
+ problems.push({ start: node.start, message: 'decorators need `"experimentalDecorators": true` in tsconfig.json: e2e compiles TypeScript\'s legacy decorators, and Node.js does not run standard decorators yet' });
143
+ }
144
+ },
145
+ }).visit(parsed.program);
146
+ if (problems.length > 0) {
147
+ throw new SyntaxError(problems.map(({ start, message }) => `${file}:${position(source.slice(0, start))}: ${message}`).join('\n'));
148
+ }
149
+ }
150
+ /**
151
+ * A `require` that runs resolve hooks, for compiled CommonJS. The `require`
152
+ * Node.js hands a CommonJS module an ES module imported skips them: before
153
+ * nodejs/node#62920 (24.18.0, 26.2.0; not in Node.js 22), and on every
154
+ * version once a `module.register` loader is in the chain too (Yarn PnP,
155
+ * tsx through NODE_OPTIONS). Without it, `require('./helper')` from such a
156
+ * module misses `helper.ts`, a tsconfig alias, and the helpers compiled code
157
+ * requires from e2e's install. The loader puts the function on `globalThis`
158
+ * when it registers (`esm-hooks.ts`): the `require` such a module starts
159
+ * with cannot load another ES module, e2e's included.
160
+ */
161
+ const HOOKED_REQUIRE = `require = globalThis[Symbol.for(${JSON.stringify(HOOKED_REQUIRE_KEY)})](__filename);`;
162
+ /**
163
+ * Compiled CommonJS as Node.js can run it, read from oxc's own parse of
164
+ * the output:
165
+ * - Oxc keeps a file whose only imports were type-only a module by printing
166
+ * `export {};`, even for CommonJS, where that statement is a syntax error
167
+ * and TypeScript emits nothing. It is an oxc bug, not yet reported
168
+ * upstream (oxc 0.152.0, `crates/oxc_transformer/src/typescript/
169
+ * annotations.rs`, the `no_modules_remaining && some_modules_deleted`
170
+ * branch). The statement is blanked out, so nothing after it moves.
171
+ * - `HOOKED_REQUIRE` follows the last directive on
172
+ * its line, so the prologue stays first and no mapped position moves; with
173
+ * no directive it takes a line of its own at the top (after a hashbang),
174
+ * and the source map gains an unmapped line there.
175
+ */
176
+ function runnableCommonJs(file, code, mappings) {
177
+ const { program } = oxc().parser.parseSync(file, code, { lang: 'js', sourceType: 'commonjs' });
178
+ let runnable = code;
179
+ for (const statement of program.body) {
180
+ if (statement.type === 'ExportNamedDeclaration' && statement.declaration === null && statement.source === null && statement.specifiers.length === 0) {
181
+ runnable = `${runnable.slice(0, statement.start)}${' '.repeat(statement.end - statement.start)}${runnable.slice(statement.end)}`;
182
+ }
183
+ }
184
+ const directives = program.body.filter((statement) => statement.type === 'ExpressionStatement' && typeof statement.directive === 'string');
185
+ const last = directives.at(-1);
186
+ if (last !== undefined)
187
+ return { code: `${runnable.slice(0, last.end)} ${HOOKED_REQUIRE}${runnable.slice(last.end)}`, mappings };
188
+ const line = program.hashbang === null ? 0 : 1;
189
+ const lines = runnable.split('\n');
190
+ lines.splice(line, 0, HOOKED_REQUIRE);
191
+ const mappedLines = mappings.split(';');
192
+ mappedLines.splice(line, 0, '');
193
+ return { code: lines.join('\n'), mappings: mappedLines.join(';') };
194
+ }
195
+ /** Each helper's file in e2e's copy of `@oxc-project/runtime`, resolved once. */
196
+ const helperFiles = new Map();
197
+ /**
198
+ * The compiled code with each runtime helper it imports or requires taken
199
+ * from e2e's own copy: by URL for `import`, by path for `require`. Oxc 0.152
200
+ * has no option to name the helper source, so the import and require
201
+ * specifiers it printed (`helpersUsed`) are rewritten; a redirect of the
202
+ * package name in the resolver would also catch a project's own
203
+ * `@oxc-project/runtime` imports.
204
+ */
205
+ function withOwnHelpers(code, helpers, format) {
206
+ let rewritten = code;
207
+ for (const specifier of new Set(Object.values(helpers))) {
208
+ let file = helperFiles.get(specifier);
209
+ if (file === undefined) {
210
+ file = ownRequire.resolve(specifier);
211
+ helperFiles.set(specifier, file);
212
+ }
213
+ const written = JSON.stringify(specifier);
214
+ rewritten =
215
+ format === 'module'
216
+ ? rewritten.replaceAll(` from ${written};`, ` from ${JSON.stringify(pathToFileURL(file).href)};`)
217
+ : rewritten.replaceAll(`require(${written})`, `require(${JSON.stringify(file)})`);
218
+ }
219
+ return rewritten;
220
+ }
221
+ /** An inline source map at the end of `source`, as another loader earlier in the chain leaves one. */
222
+ const INLINE_MAP = /\n\/\/# sourceMappingURL=data:application\/json(?:;charset=utf-8)?;base64,([A-Za-z0-9+/=]+)\s*$/;
223
+ /**
224
+ * `source`, the content of `file` as the next loader handed it over,
225
+ * compiled to JavaScript for its kind with `compilerOptions`, with an
226
+ * inline source map. When an earlier loader already compiled the file and
227
+ * left its own inline map, the two are composed, so positions still lead to
228
+ * the file on disk.
229
+ */
230
+ export function compileTypeScript(file, source, kind, compilerOptions) {
231
+ rejectUnsupportedSyntax(file, source, kind, compilerOptions);
232
+ const result = oxc().transform.transformSync(file, source, {
233
+ ...transformOptions(compilerOptions),
234
+ lang: kind.lang,
235
+ sourceType: kind.format,
236
+ target: `node${process.versions.node}`,
237
+ sourcemap: true,
238
+ });
239
+ const errors = result.errors.filter((error) => error.severity === 'Error');
240
+ if (errors.length > 0) {
241
+ const bytes = Buffer.from(source, 'utf8');
242
+ throw new SyntaxError(errors.map((error) => describeError(file, bytes, error)).join('\n'));
243
+ }
244
+ let code = withOwnHelpers(result.code, result.helpersUsed, kind.format);
245
+ let mappings = result.map?.mappings ?? '';
246
+ if (kind.format === 'commonjs')
247
+ ({ code, mappings } = runnableCommonJs(file, code, mappings));
248
+ // Absolute, so a frame names the file without the loader's query, whatever characters its name holds.
249
+ const own = { version: 3, mappings, names: result.map?.names ?? [], sources: [pathToFileURL(file).href] };
250
+ const incoming = INLINE_MAP.exec(source)?.[1];
251
+ const map = incoming === undefined ? own : remapping([own, JSON.parse(Buffer.from(incoming, 'base64').toString('utf8'))], () => null);
252
+ return `${code}\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(map)).toString('base64')}\n`;
253
+ }
@@ -297,6 +297,7 @@ export type PointerAction = {
297
297
  } | {
298
298
  readonly kind: 'swipeTo';
299
299
  readonly target: ViewportPoint;
300
+ readonly durationMs?: number;
300
301
  };
301
302
  /** Every pointer action kind; what an engine declares in `pointerActions`. */
302
303
  export type PointerActionKind = PointerAction['kind'];
@@ -362,6 +362,8 @@ export interface EnginePrepareInfo {
362
362
  * fail every test behind it.
363
363
  */
364
364
  readonly signal: AbortSignal;
365
+ /** Whether the run asked for a visible surface (`--headed`), as `EngineInitInfo` declares it. */
366
+ readonly headed: boolean;
365
367
  /**
366
368
  * Reports one line of progress. The runner streams it as a `notice` run
367
369
  * event, so it reaches the reporter and every host sink instead of being
@@ -11,7 +11,7 @@
11
11
  * a non-retryable `ENGINE_FAILURE`, so a crashed engine is an infrastructure
12
12
  * failure everywhere and never a retry-eligible test failure.
13
13
  */
14
- import { asEngineError, ConfigurationError, E2EError, errorMessage, isForeignE2EError, TestError, } from '../internal/errors.js';
14
+ import { asEngineError, ConfigurationError, errorMessage, isE2EError, TestError, } from '../internal/errors.js';
15
15
  import { requireKey } from '../internal/keys.js';
16
16
  import { EngineError, } from './surface.js';
17
17
  /** Located refs are pruned oldest-first past this bound so the map cannot grow unboundedly. */
@@ -45,7 +45,7 @@ function countSecure(node) {
45
45
  * engine failing outside its contract, reported as infrastructure.
46
46
  */
47
47
  function normalize(cause, label) {
48
- if (cause instanceof E2EError || isForeignE2EError(cause) || asEngineError(cause) !== undefined) {
48
+ if (isE2EError(cause) || asEngineError(cause) !== undefined) {
49
49
  throw cause;
50
50
  }
51
51
  throw new EngineError('ENGINE_FAILURE', `${label} failed: ${errorMessage(cause)}`, {
@@ -106,7 +106,7 @@ class AsyncExpectationImpl {
106
106
  return undefined;
107
107
  return spec.predicate(sample);
108
108
  },
109
- onTimeout: () => {
109
+ onTimeout: (cause) => {
110
110
  const expected = `${this.negated ? 'not ' : ''}${spec.describeExpected}`;
111
111
  const observed = spec.observed(lastSample);
112
112
  return new TestError('ASSERTION_FAILED', [
@@ -117,6 +117,7 @@ class AsyncExpectationImpl {
117
117
  ].join('\n'), {
118
118
  // The same facts, one per field, for a reporter that lays them out.
119
119
  details: { locator: this.label, expected, observed, matches: lastSample.count },
120
+ ...(cause === undefined ? {} : { cause }),
120
121
  });
121
122
  },
122
123
  });
@@ -1,5 +1,5 @@
1
1
  /** expect.poll: re-reads a value until a synchronous matcher passes. */
2
- import { ConfigurationError, E2EError, TestError } from '../internal/errors.js';
2
+ import { ConfigurationError, E2EError, isE2EError, TestError } from '../internal/errors.js';
3
3
  import { Deadline, POLL_INTERVAL_MS, sleep, withAbort, withTimeout } from '../internal/time.js';
4
4
  import { currentAttempt } from './attempt.js';
5
5
  import { currentPollScope } from './poll-scope.js';
@@ -39,7 +39,7 @@ function describeError(error) {
39
39
  return error instanceof Error ? error.message : String(error);
40
40
  }
41
41
  function isNotYet(error) {
42
- return error instanceof E2EError && error.code === 'ASSERTION_FAILED';
42
+ return isE2EError(error) && error.code === 'ASSERTION_FAILED';
43
43
  }
44
44
  function cancelled() {
45
45
  return new E2EError('infrastructure', 'CANCELLED', 'operation cancelled');
@@ -3,6 +3,8 @@ import { type SourceLocation } from './source.ts';
3
3
  import { type EngineErrorCode } from '../engine/contract.ts';
4
4
  /** Message of an arbitrary thrown value, for diagnostics that must not throw. */
5
5
  export declare function errorMessage(cause: unknown): string;
6
+ /** Installs the process-wide redactor `serializeError` uses when none is given; `undefined` clears it. */
7
+ export declare function setErrorRedactor(redact: ((text: string) => string) | undefined): void;
6
8
  /**
7
9
  * The message of a thrown value led by its code when it is an e2e error, as
8
10
  * the CLI prints one: `POLICY_DENIED: forbidden URL scheme: file:`. Any
@@ -107,8 +109,13 @@ export declare class E2EError extends Error {
107
109
  readonly [E2E_ERROR_MARKER] = true;
108
110
  constructor(category: ErrorCategory, code: string, message: string, options?: E2EErrorOptions);
109
111
  }
110
- /** Detects an E2EError created by another copy of this module. */
111
- export declare function isForeignE2EError(value: unknown): value is Error & {
112
+ /**
113
+ * True for an E2EError from this module copy or another. An engine imported by
114
+ * a config file loads its own copy of the runner, so its errors fail
115
+ * `instanceof E2EError` and every subclass check; test for runner errors with
116
+ * this, and read the subclass off `category` and `code`.
117
+ */
118
+ export declare function isE2EError(value: unknown): value is Error & {
112
119
  category: ErrorCategory;
113
120
  code: string;
114
121
  retryable: boolean;
@@ -159,7 +166,7 @@ export declare function serializeError(error: E2EError, extras?: {
159
166
  /**
160
167
  * Replaces secret values in the message, the details, and the stack: an
161
168
  * assertion that observed a secret on screen, or a message that quoted
162
- * one, must not carry it into the report. Identity when omitted.
169
+ * one, must not carry it into the report. Defaults to `setErrorRedactor`'s.
163
170
  */
164
171
  redact?: ((text: string) => string) | undefined;
165
172
  }): SerializedError;
@@ -1,18 +1,28 @@
1
1
  /** Runner-owned error taxonomy and exit-code mapping. */
2
2
  import { stripVTControlCharacters } from 'node:util';
3
+ import { realmSlot } from './realm-slot.js';
3
4
  import { sourceLocation } from './source.js';
4
5
  import { ENGINE_ERROR_CODES, EngineError, RETRYABLE_ENGINE_ERROR_CODES, } from '../engine/contract.js';
5
6
  /** Message of an arbitrary thrown value, for diagnostics that must not throw. */
6
7
  export function errorMessage(cause) {
7
8
  return cause instanceof Error ? cause.message : String(cause);
8
9
  }
10
+ /** The redactor `serializeError` falls back to, on `globalThis` so a project's copy of e2e shares it. */
11
+ const redactorSlot = realmSlot('e2e.error-redactor.v1');
12
+ /** Installs the process-wide redactor `serializeError` uses when none is given; `undefined` clears it. */
13
+ export function setErrorRedactor(redact) {
14
+ if (redact === undefined)
15
+ redactorSlot.delete(globalThis);
16
+ else
17
+ redactorSlot.set(globalThis, redact);
18
+ }
9
19
  /**
10
20
  * The message of a thrown value led by its code when it is an e2e error, as
11
21
  * the CLI prints one: `POLICY_DENIED: forbidden URL scheme: file:`. Any
12
22
  * other value is its message alone.
13
23
  */
14
24
  export function codedMessage(cause) {
15
- const code = cause instanceof E2EError || isForeignE2EError(cause) ? `${cause.code}: ` : '';
25
+ const code = isE2EError(cause) ? `${cause.code}: ` : '';
16
26
  return `${code}${errorMessage(cause)}`;
17
27
  }
18
28
  /**
@@ -129,8 +139,13 @@ const CATEGORIES = new Set([
129
139
  'internal',
130
140
  'interrupted',
131
141
  ]);
132
- /** Detects an E2EError created by another copy of this module. */
133
- export function isForeignE2EError(value) {
142
+ /**
143
+ * True for an E2EError from this module copy or another. An engine imported by
144
+ * a config file loads its own copy of the runner, so its errors fail
145
+ * `instanceof E2EError` and every subclass check; test for runner errors with
146
+ * this, and read the subclass off `category` and `code`.
147
+ */
148
+ export function isE2EError(value) {
134
149
  return (value instanceof Error &&
135
150
  value[E2E_ERROR_MARKER] === true &&
136
151
  CATEGORIES.has(value.category) &&
@@ -197,8 +212,8 @@ export function combineExitCodes(codes) {
197
212
  * non-retryable infrastructure ENGINE_FAILURE.
198
213
  */
199
214
  export function translateEngineError(cause, suffix = '') {
200
- if (cause instanceof E2EError)
201
- return cause;
215
+ if (isE2EError(cause))
216
+ return classifyError(cause);
202
217
  const engineError = asEngineError(cause);
203
218
  if (engineError !== undefined) {
204
219
  switch (engineError.code) {
@@ -235,7 +250,7 @@ export function translateEngineError(cause, suffix = '') {
235
250
  * `Error` from an installer - is infrastructure.
236
251
  */
237
252
  export function translateProvisioningError(cause, suffix = '') {
238
- if (cause instanceof E2EError || isForeignE2EError(cause))
253
+ if (isE2EError(cause))
239
254
  return classifyError(cause);
240
255
  if (asEngineError(cause)?.code === 'CANCELLED')
241
256
  return translateEngineError(cause);
@@ -245,7 +260,7 @@ export function translateProvisioningError(cause, suffix = '') {
245
260
  export function classifyError(value) {
246
261
  if (value instanceof E2EError)
247
262
  return value;
248
- if (isForeignE2EError(value)) {
263
+ if (isE2EError(value)) {
249
264
  const details = value.details;
250
265
  return new E2EError(value.category, value.code, value.message, {
251
266
  retryable: value.retryable,
@@ -264,7 +279,7 @@ export function classifyError(value) {
264
279
  const MAX_MESSAGE_BYTES = 8192;
265
280
  /** Serializes an error into the bounded report-1 error shape. */
266
281
  export function serializeError(error, extras = {}) {
267
- const redact = extras.redact ?? ((text) => text);
282
+ const redact = extras.redact ?? redactorSlot.get(globalThis) ?? ((text) => text);
268
283
  const serialized = {
269
284
  category: error.category,
270
285
  code: error.code,
@@ -1,9 +1,18 @@
1
1
  /**
2
- * The Node.js floor, checked before the CLI loads anything else. Package
3
- * managers only warn about `engines`, so an unsupported runtime would
4
- * otherwise surface as an unrelated TypeError deep inside a run.
2
+ * The Node.js releases e2e runs on, as a set of release ranges. The CLI checks the floor before it loads
3
+ * anything else: package managers only warn about `engines`, so an
4
+ * unsupported runtime would otherwise surface as an unrelated TypeError deep
5
+ * inside a run.
5
6
  */
6
- /** Mirrors `engines.node` in package.json; a unit test keeps the two in step. */
7
- export declare const MINIMUM_NODE_VERSION = "22.12.0";
8
- /** Explains an unsupported runtime, or undefined when `current` meets the floor. */
9
- export declare function unsupportedNodeMessage(current: string, minimum?: string): string | undefined;
7
+ /** `engines.node` in package.json; a unit test keeps the two in step. */
8
+ export declare const SUPPORTED_NODE_RANGE: string;
9
+ /**
10
+ * Explains a runtime e2e cannot run on, or undefined for one it can: a
11
+ * Node.js outside `SUPPORTED_NODE_RANGE`, or a runtime that reports a
12
+ * supported Node.js version without Node.js's `module.registerHooks` (Bun),
13
+ * which e2e's TypeScript loader runs on, or Deno.
14
+ */
15
+ export declare function unsupportedRuntimeMessage(versions?: Pick<NodeJS.ProcessVersions, 'node'> & {
16
+ readonly bun?: string;
17
+ readonly deno?: string;
18
+ }, hasRegisterHooks?: boolean): string | undefined;