@e2e-dev/web 0.12.0 → 0.14.0-nightly-20261008205824
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/actions.d.ts +10 -3
- package/dist/actions.js +79 -33
- package/dist/app-log.d.ts +20 -0
- package/dist/app-log.js +127 -0
- package/dist/attempt-session.d.ts +5 -19
- package/dist/attempt-session.js +19 -91
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +59 -47
- package/dist/dialogs.js +5 -0
- package/dist/engine.js +0 -2
- package/dist/evaluation.d.ts +8 -3
- package/dist/evaluation.js +38 -4
- package/dist/in-page/read-semantics.js +155 -16
- package/dist/init-scripts.js +2 -2
- package/dist/install.d.ts +6 -0
- package/dist/install.js +44 -14
- package/dist/observation-capture.d.ts +1 -2
- package/dist/observation.d.ts +16 -0
- package/dist/observation.js +47 -12
- package/dist/operation-budget.d.ts +14 -0
- package/dist/operation-budget.js +31 -0
- package/dist/provider-video.d.ts +0 -2
- package/dist/provider-video.js +0 -2
- package/dist/support.d.ts +14 -10
- package/dist/support.js +40 -19
- package/dist/surface.d.ts +29 -14
- package/dist/surface.js +117 -44
- package/dist/trace-feed.d.ts +40 -0
- package/dist/trace-feed.js +93 -0
- package/dist/video.d.ts +0 -4
- package/dist/video.js +23 -5
- package/package.json +4 -4
package/dist/install.js
CHANGED
|
@@ -1,29 +1,45 @@
|
|
|
1
1
|
/** First-run browser provisioning for the Playwright engine. */
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
3
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { EngineError, InfrastructureError } from 'e2e/engine';
|
|
8
8
|
import { browserType } from './browser-connection.js';
|
|
9
|
+
/** Whether `chromium_headless_shell-<revision>` under `browsersPath` finished installing and holds its executable. */
|
|
10
|
+
function headlessShellInstalled(browsersPath, revision) {
|
|
11
|
+
const shellRoot = path.join(browsersPath, `chromium_headless_shell-${revision}`);
|
|
12
|
+
if (!existsSync(path.join(shellRoot, 'INSTALLATION_COMPLETE')))
|
|
13
|
+
return false;
|
|
14
|
+
const executable = process.platform === 'win32' ? 'chrome-headless-shell.exe' : 'chrome-headless-shell';
|
|
15
|
+
return readdirSync(shellRoot, { withFileTypes: true }).some((entry) => entry.isDirectory() && existsSync(path.join(shellRoot, entry.name, executable)));
|
|
16
|
+
}
|
|
9
17
|
/**
|
|
10
|
-
* Whether the
|
|
11
|
-
*
|
|
12
|
-
* `PLAYWRIGHT_BROWSERS_PATH` at module load, so the in-process check is only
|
|
13
|
-
* authoritative when the run's environment agrees with this process's. A run
|
|
14
|
-
* pointed at another cache is left to the CLI, which resolves against the
|
|
15
|
-
* environment it is spawned with and is a no-op when nothing is missing.
|
|
18
|
+
* Whether the build the run launches (headless shell for headless chromium, else the full build)
|
|
19
|
+
* is on disk, or undefined when `env` points at another cache than this process loaded.
|
|
16
20
|
*/
|
|
17
|
-
function isBrowserInstalled(name, env) {
|
|
21
|
+
function isBrowserInstalled(name, env, headed) {
|
|
18
22
|
if (env['PLAYWRIGHT_BROWSERS_PATH'] !== process.env['PLAYWRIGHT_BROWSERS_PATH'])
|
|
19
23
|
return undefined;
|
|
20
24
|
try {
|
|
21
|
-
|
|
25
|
+
const executable = browserType(name).executablePath();
|
|
26
|
+
if (headed || name !== 'chromium')
|
|
27
|
+
return existsSync(executable);
|
|
28
|
+
return headlessShellAt(executable);
|
|
22
29
|
}
|
|
23
30
|
catch {
|
|
24
31
|
return false;
|
|
25
32
|
}
|
|
26
33
|
}
|
|
34
|
+
/** Whether the shell beside `executable`'s full build is installed. */
|
|
35
|
+
function headlessShellAt(executable) {
|
|
36
|
+
const marker = `${path.sep}chromium-`;
|
|
37
|
+
const at = executable.lastIndexOf(marker);
|
|
38
|
+
if (at === -1)
|
|
39
|
+
return false;
|
|
40
|
+
const revision = executable.slice(at + marker.length).split(path.sep)[0] ?? '';
|
|
41
|
+
return revision !== '' && headlessShellInstalled(executable.slice(0, at), revision);
|
|
42
|
+
}
|
|
27
43
|
/** The `node` arguments that run the pinned `playwright-core` CLI with `args`. */
|
|
28
44
|
function playwrightCliArgs(args) {
|
|
29
45
|
const require = createRequire(import.meta.url);
|
|
@@ -37,7 +53,10 @@ function playwrightCliArgs(args) {
|
|
|
37
53
|
*/
|
|
38
54
|
export function runPlaywrightCli(args) {
|
|
39
55
|
return new Promise((resolve, reject) => {
|
|
40
|
-
const child = spawn(process.execPath, playwrightCliArgs(args), {
|
|
56
|
+
const child = spawn(process.execPath, playwrightCliArgs(args), {
|
|
57
|
+
stdio: 'inherit',
|
|
58
|
+
env: installEnvironment(),
|
|
59
|
+
});
|
|
41
60
|
const forward = (signal) => child.kill(signal);
|
|
42
61
|
process.on('SIGINT', forward);
|
|
43
62
|
process.on('SIGTERM', forward);
|
|
@@ -55,6 +74,16 @@ export function runPlaywrightCli(args) {
|
|
|
55
74
|
});
|
|
56
75
|
});
|
|
57
76
|
}
|
|
77
|
+
/** The `playwright install` arguments for `names`: `--only-shell` on a headless chromium run. */
|
|
78
|
+
export function installArgs(names, headed) {
|
|
79
|
+
if (headed || !names.includes('chromium'))
|
|
80
|
+
return [...names];
|
|
81
|
+
return ['--only-shell', ...names];
|
|
82
|
+
}
|
|
83
|
+
/** `env` with `PLAYWRIGHT_SKIP_BROWSER_GC=1` unless already set, so an install keeps other tools' browsers. */
|
|
84
|
+
function installEnvironment(env = process.env) {
|
|
85
|
+
return { ...env, PLAYWRIGHT_SKIP_BROWSER_GC: env['PLAYWRIGHT_SKIP_BROWSER_GC'] ?? '1' };
|
|
86
|
+
}
|
|
58
87
|
/**
|
|
59
88
|
* Ensures the given Playwright browsers are installed, downloading any
|
|
60
89
|
* missing ones via `playwright-core install`. Called from the engine's `prepare`,
|
|
@@ -65,7 +94,8 @@ export function runPlaywrightCli(args) {
|
|
|
65
94
|
*/
|
|
66
95
|
export async function ensureBrowsersInstalled(names, options = {}) {
|
|
67
96
|
const env = options.env ?? process.env;
|
|
68
|
-
const
|
|
97
|
+
const headed = options.headed ?? false;
|
|
98
|
+
const isInstalled = options.isInstalled ?? ((name) => isBrowserInstalled(name, env, headed));
|
|
69
99
|
const verdicts = new Map([...new Set(names)].map((name) => [name, isInstalled(name)]));
|
|
70
100
|
const missing = [...verdicts.keys()].filter((name) => verdicts.get(name) !== true);
|
|
71
101
|
if (missing.length === 0)
|
|
@@ -78,7 +108,7 @@ export async function ensureBrowsersInstalled(names, options = {}) {
|
|
|
78
108
|
if (knownMissing)
|
|
79
109
|
log(`Downloading missing Playwright browsers (first run): ${missing.join(', ')}...`);
|
|
80
110
|
const install = options.install ?? runPlaywrightInstall;
|
|
81
|
-
await install(missing, { log, signal: options.signal, env });
|
|
111
|
+
await install(missing, { log, signal: options.signal, env: installEnvironment(env), headed });
|
|
82
112
|
if (knownMissing)
|
|
83
113
|
log('Browser download complete.');
|
|
84
114
|
}
|
|
@@ -109,13 +139,13 @@ function forwardLines(stream, log) {
|
|
|
109
139
|
stream.on('end', () => flush(pending));
|
|
110
140
|
}
|
|
111
141
|
/** Spawns `node <playwright-core>/cli.js install <names>`, narrating its output through `log`. */
|
|
112
|
-
function runPlaywrightInstall(names, { log, signal, env }) {
|
|
142
|
+
function runPlaywrightInstall(names, { log, signal, env, headed }) {
|
|
113
143
|
return new Promise((resolve, reject) => {
|
|
114
144
|
if (signal?.aborted === true) {
|
|
115
145
|
reject(new EngineError('CANCELLED', 'browser install cancelled', { retryable: false }));
|
|
116
146
|
return;
|
|
117
147
|
}
|
|
118
|
-
const child = spawn(process.execPath, playwrightCliArgs(['install', ...names]), {
|
|
148
|
+
const child = spawn(process.execPath, playwrightCliArgs(['install', ...installArgs(names, headed)]), {
|
|
119
149
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
120
150
|
env,
|
|
121
151
|
});
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
import type { Page } from 'playwright-core';
|
|
3
3
|
import { type EngineObserveOptions, type OperationContext } from 'e2e/engine';
|
|
4
4
|
import { RefRegistry, type CapturedObservation } from './refs.ts';
|
|
5
|
-
interface CaptureSettings {
|
|
5
|
+
export interface CaptureSettings {
|
|
6
6
|
readonly testIdAttribute: string;
|
|
7
7
|
readonly site: string | undefined;
|
|
8
8
|
}
|
|
9
9
|
/** Captures under one deadline, retaining no refs from a failed or abandoned document. */
|
|
10
10
|
export declare function captureObservation(page: Page, refs: RefRegistry, settings: CaptureSettings, operation: OperationContext, options: EngineObserveOptions | undefined): Promise<CapturedObservation>;
|
|
11
|
-
export {};
|
package/dist/observation.d.ts
CHANGED
|
@@ -59,4 +59,20 @@ export interface CapturedDocument {
|
|
|
59
59
|
* observation. The node budget is shared across all documents.
|
|
60
60
|
*/
|
|
61
61
|
export declare function captureDocument(deps: CaptureDeps, host: DocumentHost, options: CaptureOptions): Promise<CapturedDocument>;
|
|
62
|
+
type Rect = NonNullable<SemanticNode['rect']>;
|
|
63
|
+
/**
|
|
64
|
+
* Where a frame element's content box, the box its document's viewport
|
|
65
|
+
* fills, sits inside its border box: past the border and the padding. Runs in
|
|
66
|
+
* the page.
|
|
67
|
+
*/
|
|
68
|
+
export declare function frameContentInset(element: Element): Rect;
|
|
69
|
+
/** A frame element's content box, from its border box and `frameContentInset`. */
|
|
70
|
+
export declare function contentBox(border: Rect, inset: Rect): Rect;
|
|
71
|
+
/**
|
|
72
|
+
* A box a child document measured against its own viewport, in the top-level
|
|
73
|
+
* viewport's space: shifted by the frame's box and clipped to it; undefined
|
|
74
|
+
* when nothing of it is inside the frame.
|
|
75
|
+
*/
|
|
76
|
+
export declare function placeRectInFrame(rect: Rect, frame: Rect): Rect | undefined;
|
|
62
77
|
export declare function toSemanticNode(ref: NodeRef, raw: RawNodeData, children?: readonly SemanticNode[], framePath?: readonly string[]): SemanticNode;
|
|
78
|
+
export {};
|
package/dist/observation.js
CHANGED
|
@@ -129,6 +129,11 @@ async function captureInto(deps, scope, stage, host, framePath, budget, signal)
|
|
|
129
129
|
truncated = true;
|
|
130
130
|
continue;
|
|
131
131
|
}
|
|
132
|
+
const inset = await scope.read(() => elements[index].evaluate(frameContentInset).catch(() => undefined));
|
|
133
|
+
if (inset === undefined) {
|
|
134
|
+
truncated = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
132
137
|
const child = await scope.read(() => captureDocument({ ...deps, commit: stage }, frame, {
|
|
133
138
|
framePath: [...framePath, selector],
|
|
134
139
|
budget: remaining,
|
|
@@ -139,7 +144,7 @@ async function captureInto(deps, scope, stage, host, framePath, budget, signal)
|
|
|
139
144
|
truncated = true;
|
|
140
145
|
continue;
|
|
141
146
|
}
|
|
142
|
-
frameChildren.set(index, child.tree);
|
|
147
|
+
frameChildren.set(index, { tree: child.tree, inset });
|
|
143
148
|
nodeCount += child.nodeCount;
|
|
144
149
|
truncated ||= child.truncated;
|
|
145
150
|
}
|
|
@@ -205,16 +210,16 @@ function assembleTree(nodes, ids, framePath, frameChildren) {
|
|
|
205
210
|
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
206
211
|
const raw = nodes[index];
|
|
207
212
|
const embedded = frameChildren.get(index);
|
|
208
|
-
// A child document measured its nodes against its own viewport
|
|
209
|
-
// the
|
|
210
|
-
// in the top-level viewport's CSS
|
|
211
|
-
// point tap share, and a child that
|
|
212
|
-
// point over the page around it.
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
213
|
+
// A child document measured its nodes against its own viewport, which
|
|
214
|
+
// starts at the frame element's content box. Shifted by that box and
|
|
215
|
+
// clipped to it, every box in the tree is in the top-level viewport's CSS
|
|
216
|
+
// pixels, the space the screenshot and a point tap share, and a child that
|
|
217
|
+
// overflows its frame cannot claim a point over the page around it. A
|
|
218
|
+
// frame under a CSS transform is not unwound: its boxes are where the
|
|
219
|
+
// untransformed frame would put them.
|
|
220
|
+
if (embedded !== undefined && raw.rect !== null) {
|
|
221
|
+
childLists[index].unshift(placeInFrame(embedded.tree, contentBox(raw.rect, embedded.inset)));
|
|
222
|
+
}
|
|
218
223
|
const node = toSemanticNode({ id: ids[index], revision: '' }, raw, childLists[index], framePath);
|
|
219
224
|
built[index] = node;
|
|
220
225
|
if (raw.parent >= 0)
|
|
@@ -222,16 +227,46 @@ function assembleTree(nodes, ids, framePath, frameChildren) {
|
|
|
222
227
|
}
|
|
223
228
|
return built[0];
|
|
224
229
|
}
|
|
230
|
+
/**
|
|
231
|
+
* Where a frame element's content box, the box its document's viewport
|
|
232
|
+
* fills, sits inside its border box: past the border and the padding. Runs in
|
|
233
|
+
* the page.
|
|
234
|
+
*/
|
|
235
|
+
export function frameContentInset(element) {
|
|
236
|
+
const style = getComputedStyle(element);
|
|
237
|
+
const left = Number.parseFloat(style.paddingLeft) || 0;
|
|
238
|
+
const top = Number.parseFloat(style.paddingTop) || 0;
|
|
239
|
+
const right = Number.parseFloat(style.paddingRight) || 0;
|
|
240
|
+
const bottom = Number.parseFloat(style.paddingBottom) || 0;
|
|
241
|
+
return {
|
|
242
|
+
x: element.clientLeft + left,
|
|
243
|
+
y: element.clientTop + top,
|
|
244
|
+
width: Math.max(0, element.clientWidth - left - right),
|
|
245
|
+
height: Math.max(0, element.clientHeight - top - bottom),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
/** A frame element's content box, from its border box and `frameContentInset`. */
|
|
249
|
+
export function contentBox(border, inset) {
|
|
250
|
+
return { x: border.x + inset.x, y: border.y + inset.y, width: inset.width, height: inset.height };
|
|
251
|
+
}
|
|
225
252
|
/** The tree with every box translated into the frame's space and clipped to its box; a box left empty by the clip is dropped. */
|
|
226
253
|
function placeInFrame(node, frame) {
|
|
227
254
|
const { rect, ...rest } = node;
|
|
228
|
-
const placed = rect === undefined ? undefined :
|
|
255
|
+
const placed = rect === undefined ? undefined : placeRectInFrame(rect, frame);
|
|
229
256
|
return {
|
|
230
257
|
...rest,
|
|
231
258
|
...(placed === undefined ? {} : { rect: placed }),
|
|
232
259
|
...(node.children === undefined ? {} : { children: node.children.map((child) => placeInFrame(child, frame)) }),
|
|
233
260
|
};
|
|
234
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* A box a child document measured against its own viewport, in the top-level
|
|
264
|
+
* viewport's space: shifted by the frame's box and clipped to it; undefined
|
|
265
|
+
* when nothing of it is inside the frame.
|
|
266
|
+
*/
|
|
267
|
+
export function placeRectInFrame(rect, frame) {
|
|
268
|
+
return clipRect(offsetRect(rect, frame), frame);
|
|
269
|
+
}
|
|
235
270
|
function offsetRect(rect, by) {
|
|
236
271
|
return { x: rect.x + by.x, y: rect.y + by.y, width: rect.width, height: rect.height };
|
|
237
272
|
}
|
|
@@ -6,5 +6,19 @@ export interface ConnectionBudget {
|
|
|
6
6
|
}
|
|
7
7
|
/** Preserves the reason for an engine-owned deadline without relabeling caller cancellation. */
|
|
8
8
|
export declare function connectionAbort(signal: AbortSignal, label: string): EngineError;
|
|
9
|
+
/**
|
|
10
|
+
* Whether the operation's deadline may end it. `test-code` is for an
|
|
11
|
+
* operation that runs the test's own code (a download trigger): that code
|
|
12
|
+
* keeps its own steps and their budgets, so the operation is not cut under
|
|
13
|
+
* it, and only cancellation ends the wait.
|
|
14
|
+
*/
|
|
15
|
+
export type OperationBound = 'deadline' | 'test-code';
|
|
16
|
+
/**
|
|
17
|
+
* Bounds one operation by its budget: a call still pending at the deadline
|
|
18
|
+
* is abandoned as `OPERATION_TIMEOUT` instead of holding the test until its
|
|
19
|
+
* own timeout. `remaining` hands `work` the time left before Playwright's
|
|
20
|
+
* own timeout, the one to pass Playwright.
|
|
21
|
+
*/
|
|
22
|
+
export declare function withOperationDeadline<T>(budget: ConnectionBudget, label: string, work: (remaining: () => ConnectionBudget) => Promise<T>, bound?: OperationBound): Promise<T>;
|
|
9
23
|
/** Bounds the entire transition, aborts abandoned work, and never grants a fresh dispatch budget. */
|
|
10
24
|
export declare function withConnectionBudget<T>(budget: ConnectionBudget, label: string, work: (remaining: () => ConnectionBudget) => Promise<T>): Promise<T>;
|
package/dist/operation-budget.js
CHANGED
|
@@ -6,6 +6,37 @@ export function connectionAbort(signal, label) {
|
|
|
6
6
|
? signal.reason
|
|
7
7
|
: new EngineError('CANCELLED', `${label} cancelled`, { retryable: false });
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* How much sooner than an operation's deadline Playwright's own timeout
|
|
11
|
+
* fires: its error names what blocked an action, or that the input was
|
|
12
|
+
* already dispatched, and this lead lets that answer arrive before the
|
|
13
|
+
* deadline does. The deadline is for the calls Playwright never answers: an
|
|
14
|
+
* evaluate, which takes no timeout, and a point tap on a page whose renderer
|
|
15
|
+
* is stuck in a script. A budget shorter than twice the
|
|
16
|
+
* lead splits in half.
|
|
17
|
+
*/
|
|
18
|
+
const PLAYWRIGHT_TIMEOUT_LEAD_MS = 250;
|
|
19
|
+
/**
|
|
20
|
+
* Bounds one operation by its budget: a call still pending at the deadline
|
|
21
|
+
* is abandoned as `OPERATION_TIMEOUT` instead of holding the test until its
|
|
22
|
+
* own timeout. `remaining` hands `work` the time left before Playwright's
|
|
23
|
+
* own timeout, the one to pass Playwright.
|
|
24
|
+
*/
|
|
25
|
+
export function withOperationDeadline(budget, label, work, bound = 'deadline') {
|
|
26
|
+
if (bound === 'test-code') {
|
|
27
|
+
const endsAt = Date.now() + budget.timeoutMs;
|
|
28
|
+
return raceAbort(() => work(() => {
|
|
29
|
+
if (budget.signal.aborted)
|
|
30
|
+
throw connectionAbort(budget.signal, label);
|
|
31
|
+
return { signal: budget.signal, timeoutMs: Math.max(1, endsAt - Date.now()) };
|
|
32
|
+
}), budget.signal, label);
|
|
33
|
+
}
|
|
34
|
+
return withConnectionBudget(budget, label, (remaining) => work(() => {
|
|
35
|
+
const current = remaining();
|
|
36
|
+
const lead = Math.min(PLAYWRIGHT_TIMEOUT_LEAD_MS, current.timeoutMs / 2);
|
|
37
|
+
return { signal: current.signal, timeoutMs: Math.ceil(current.timeoutMs - lead) };
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
9
40
|
/** Bounds the entire transition, aborts abandoned work, and never grants a fresh dispatch budget. */
|
|
10
41
|
export async function withConnectionBudget(budget, label, work) {
|
|
11
42
|
const controller = new AbortController();
|
package/dist/provider-video.d.ts
CHANGED
|
@@ -13,8 +13,6 @@ export declare class ProviderVideo implements AttemptVideo {
|
|
|
13
13
|
private readonly artifactsDir;
|
|
14
14
|
/** The recording covers the browser, so no page starts a segment of its own. */
|
|
15
15
|
readonly isArmed = false;
|
|
16
|
-
/** The provider records the browser from outside; the page's screencast stays the trace's. */
|
|
17
|
-
readonly startsScreencast = false;
|
|
18
16
|
private started;
|
|
19
17
|
/** The stop in flight, shared: a close that retries while a timed-out stop is still running waits for it instead of stopping twice. */
|
|
20
18
|
private stopping;
|
package/dist/provider-video.js
CHANGED
|
@@ -13,8 +13,6 @@ export class ProviderVideo {
|
|
|
13
13
|
artifactsDir;
|
|
14
14
|
/** The recording covers the browser, so no page starts a segment of its own. */
|
|
15
15
|
isArmed = false;
|
|
16
|
-
/** The provider records the browser from outside; the page's screencast stays the trace's. */
|
|
17
|
-
startsScreencast = false;
|
|
18
16
|
started;
|
|
19
17
|
/** The stop in flight, shared: a close that retries while a timed-out stop is still running waits for it instead of stopping twice. */
|
|
20
18
|
stopping;
|
package/dist/support.d.ts
CHANGED
|
@@ -102,19 +102,23 @@ export declare function isTestErrorCode(cause: unknown, code: string): cause is
|
|
|
102
102
|
* | `TimeoutError` on a read, navigation, or artifact call | OPERATION_TIMEOUT |
|
|
103
103
|
* | `TimeoutError` on an action, log ends before the input dispatch | NOT_ACTIONABLE |
|
|
104
104
|
* | `TimeoutError` on an action, log shows the dispatch started | ACTION_MAY_HAVE_COMMITTED |
|
|
105
|
+
* | operation deadline cut off an action, pointer, or keyboard call | ACTION_MAY_HAVE_COMMITTED |
|
|
105
106
|
* | element detached / not attached / no element / resolved hidden | NODE_STALE (retryable) |
|
|
106
107
|
* | execution context destroyed / frame detached by a navigation | NODE_STALE (retryable) |
|
|
108
|
+
* | strict mode violation, log ends before the input dispatch | NODE_STALE (retryable) |
|
|
109
|
+
* | strict mode violation, log shows the dispatch started | ACTION_MAY_HAVE_COMMITTED |
|
|
107
110
|
* | not an input / not editable / not checkable | NOT_ACTIONABLE |
|
|
108
|
-
* |
|
|
111
|
+
* | anything else | ENGINE_FAILURE |
|
|
109
112
|
*
|
|
110
113
|
* The action split is read from the call log Playwright appends to a timeout
|
|
111
|
-
* message: `performing <x> action`, `<x> action done`, and
|
|
112
|
-
* scheduled navigations to finish` are only logged once the input
|
|
113
|
-
* has been) dispatched, so a
|
|
114
|
-
* the harness must not blindly repeat it. Every earlier line
|
|
115
|
-
* element to be visible, enabled and stable`, `scrolling into
|
|
116
|
-
* needed`, `retrying <x> action`) precedes dispatch and is a plain
|
|
117
|
-
* actionability miss.
|
|
114
|
+
* or strict mode message: `performing <x> action`, `<x> action done`, and
|
|
115
|
+
* `waiting for scheduled navigations to finish` are only logged once the input
|
|
116
|
+
* is being (or has been) dispatched, so a failure whose log reaches them is
|
|
117
|
+
* uncertain and the harness must not blindly repeat it. Every earlier line
|
|
118
|
+
* (`waiting for element to be visible, enabled and stable`, `scrolling into
|
|
119
|
+
* view if needed`, `retrying <x> action`) precedes dispatch and is a plain
|
|
120
|
+
* actionability miss. Only whole `- ` log lines count, so a locator or element
|
|
121
|
+
* text quoting those words never does.
|
|
118
122
|
*/
|
|
119
123
|
export declare const POST_DISPATCH_PATTERN: RegExp;
|
|
120
124
|
/** Translates an unexpected Playwright error at the contract boundary. */
|
|
@@ -142,7 +146,7 @@ export declare function performViewportSwipe(page: Page, direction: ScrollDirect
|
|
|
142
146
|
* CSS pixel instead.
|
|
143
147
|
*/
|
|
144
148
|
export declare function nearestPixel(point: ViewportPoint): ViewportPoint;
|
|
145
|
-
/** A pointer drag from one viewport point to another, on whole pixels, with an intermediate move so drag handlers see motion. */
|
|
146
|
-
export declare function performPointDrag(mouse: Mouse, start: ViewportPoint, end: ViewportPoint): Promise<void>;
|
|
149
|
+
/** A pointer drag from one viewport point to another, on whole pixels, with an intermediate move so drag handlers see motion; paced over `durationMs` when given. */
|
|
150
|
+
export declare function performPointDrag(mouse: Mouse, start: ViewportPoint, end: ViewportPoint, durationMs?: number, signal?: AbortSignal): Promise<void>;
|
|
147
151
|
export declare function performElementSwipe(target: ActionTarget, direction: ScrollDirection, momentum: Momentum, timeout: number): Promise<void>;
|
|
148
152
|
export {};
|
package/dist/support.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Shared error translation, filename, and swipe helpers for the Playwright engine. */
|
|
2
|
+
import { setTimeout } from 'node:timers/promises';
|
|
2
3
|
import { EngineError, withinCleanupBudget } from 'e2e/engine';
|
|
3
4
|
import { ConfigurationError, InfrastructureError, TestError } from 'e2e/engine';
|
|
4
5
|
export const DEFAULT_VIEWPORT = { width: 1280, height: 720 };
|
|
@@ -193,11 +194,12 @@ export class ErrorLatch {
|
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
/**
|
|
196
|
-
* The runner error classes as their `name` reads. A project
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* `name` and `code` still
|
|
197
|
+
* The runner error classes as their `name` reads. A project can load this
|
|
198
|
+
* engine against a copy of `e2e` other than the one the runner's core runs
|
|
199
|
+
* from (and e2e before 0.17 always loaded config in a module realm of its
|
|
200
|
+
* own), so a runner error reaching the engine can be a `TestError` this
|
|
201
|
+
* module never imported: `instanceof` says no while `name` and `code` still
|
|
202
|
+
* tell the truth.
|
|
201
203
|
*/
|
|
202
204
|
const CLASSIFIED_NAMES = new Set([
|
|
203
205
|
'EngineError',
|
|
@@ -236,21 +238,25 @@ export function isTestErrorCode(cause, code) {
|
|
|
236
238
|
* | `TimeoutError` on a read, navigation, or artifact call | OPERATION_TIMEOUT |
|
|
237
239
|
* | `TimeoutError` on an action, log ends before the input dispatch | NOT_ACTIONABLE |
|
|
238
240
|
* | `TimeoutError` on an action, log shows the dispatch started | ACTION_MAY_HAVE_COMMITTED |
|
|
241
|
+
* | operation deadline cut off an action, pointer, or keyboard call | ACTION_MAY_HAVE_COMMITTED |
|
|
239
242
|
* | element detached / not attached / no element / resolved hidden | NODE_STALE (retryable) |
|
|
240
243
|
* | execution context destroyed / frame detached by a navigation | NODE_STALE (retryable) |
|
|
244
|
+
* | strict mode violation, log ends before the input dispatch | NODE_STALE (retryable) |
|
|
245
|
+
* | strict mode violation, log shows the dispatch started | ACTION_MAY_HAVE_COMMITTED |
|
|
241
246
|
* | not an input / not editable / not checkable | NOT_ACTIONABLE |
|
|
242
|
-
* |
|
|
247
|
+
* | anything else | ENGINE_FAILURE |
|
|
243
248
|
*
|
|
244
249
|
* The action split is read from the call log Playwright appends to a timeout
|
|
245
|
-
* message: `performing <x> action`, `<x> action done`, and
|
|
246
|
-
* scheduled navigations to finish` are only logged once the input
|
|
247
|
-
* has been) dispatched, so a
|
|
248
|
-
* the harness must not blindly repeat it. Every earlier line
|
|
249
|
-
* element to be visible, enabled and stable`, `scrolling into
|
|
250
|
-
* needed`, `retrying <x> action`) precedes dispatch and is a plain
|
|
251
|
-
* actionability miss.
|
|
250
|
+
* or strict mode message: `performing <x> action`, `<x> action done`, and
|
|
251
|
+
* `waiting for scheduled navigations to finish` are only logged once the input
|
|
252
|
+
* is being (or has been) dispatched, so a failure whose log reaches them is
|
|
253
|
+
* uncertain and the harness must not blindly repeat it. Every earlier line
|
|
254
|
+
* (`waiting for element to be visible, enabled and stable`, `scrolling into
|
|
255
|
+
* view if needed`, `retrying <x> action`) precedes dispatch and is a plain
|
|
256
|
+
* actionability miss. Only whole `- ` log lines count, so a locator or element
|
|
257
|
+
* text quoting those words never does.
|
|
252
258
|
*/
|
|
253
|
-
export const POST_DISPATCH_PATTERN =
|
|
259
|
+
export const POST_DISPATCH_PATTERN = /^\s*- (performing \w+ action|[\w ]+ action done|waiting for scheduled navigations to finish)\s*$/im;
|
|
254
260
|
/** Translates an unexpected Playwright error at the contract boundary. */
|
|
255
261
|
export function translatePwError(cause, operation) {
|
|
256
262
|
if (isClassified(cause))
|
|
@@ -321,16 +327,31 @@ export async function performViewportSwipe(page, direction, momentum) {
|
|
|
321
327
|
export function nearestPixel(point) {
|
|
322
328
|
return { x: Math.round(point.x), y: Math.round(point.y) };
|
|
323
329
|
}
|
|
324
|
-
/** A pointer drag from one viewport point to another, on whole pixels, with an intermediate move so drag handlers see motion. */
|
|
325
|
-
export async function performPointDrag(mouse, start, end) {
|
|
330
|
+
/** A pointer drag from one viewport point to another, on whole pixels, with an intermediate move so drag handlers see motion; paced over `durationMs` when given. */
|
|
331
|
+
export async function performPointDrag(mouse, start, end, durationMs, signal) {
|
|
326
332
|
const from = nearestPixel(start);
|
|
327
333
|
const to = nearestPixel(end);
|
|
328
334
|
const middle = nearestPixel({ x: (from.x + to.x) / 2, y: (from.y + to.y) / 2 });
|
|
329
335
|
await mouse.move(from.x, from.y);
|
|
330
336
|
await mouse.down();
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
337
|
+
try {
|
|
338
|
+
if (durationMs !== undefined) {
|
|
339
|
+
const steps = Math.max(2, Math.round(durationMs / 16));
|
|
340
|
+
const startedAt = performance.now();
|
|
341
|
+
for (let step = 1; step <= steps; step += 1) {
|
|
342
|
+
const due = startedAt + (durationMs * step) / steps;
|
|
343
|
+
await setTimeout(Math.max(0, due - performance.now()), undefined, { signal });
|
|
344
|
+
await mouse.move(from.x + ((to.x - from.x) * step) / steps, from.y + ((to.y - from.y) * step) / steps);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
else {
|
|
348
|
+
await mouse.move(middle.x, middle.y);
|
|
349
|
+
await mouse.move(to.x, to.y);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
finally {
|
|
353
|
+
await mouse.up();
|
|
354
|
+
}
|
|
334
355
|
}
|
|
335
356
|
export async function performElementSwipe(target, direction, momentum, timeout) {
|
|
336
357
|
// Hover first: it auto-waits for visibility on both target kinds, so the
|
package/dist/surface.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { type EngineAttemptContext, type EngineCleanupContext, type EngineInitIn
|
|
|
11
11
|
import { type BrowserName } from './browser-connection.ts';
|
|
12
12
|
import { type BrowserProvider, type LeaseDownloads } from './provider.ts';
|
|
13
13
|
import { DialogRouter } from './dialogs.ts';
|
|
14
|
+
import { type OperationBound } from './operation-budget.ts';
|
|
14
15
|
import { type WebInitScript } from './init-scripts.ts';
|
|
15
16
|
import { ErrorLatch } from './support.ts';
|
|
16
17
|
type RoutePredicate = (url: URL) => boolean;
|
|
@@ -106,8 +107,7 @@ export interface WebOptions {
|
|
|
106
107
|
* page, deterministic and agent-driven alike. Injecting headers routes
|
|
107
108
|
* every request of the attempt, which turns the browser's HTTP cache off
|
|
108
109
|
* and blocks service workers (a worker's requests bypass routing, so a
|
|
109
|
-
* page it controlled would reach the gate bare)
|
|
110
|
-
* records request headers.
|
|
110
|
+
* page it controlled would reach the gate bare).
|
|
111
111
|
*/
|
|
112
112
|
readonly headers?: Readonly<Record<string, string>>;
|
|
113
113
|
/**
|
|
@@ -186,6 +186,10 @@ export declare class PlaywrightSurface {
|
|
|
186
186
|
private projectRoot;
|
|
187
187
|
private headed;
|
|
188
188
|
private artifactsDir;
|
|
189
|
+
/** Navigations the engine has in flight, which the app log leaves to the step that started them. */
|
|
190
|
+
private ownNavigations;
|
|
191
|
+
/** What the attempt tells its trace beside the app log; absent between attempts. */
|
|
192
|
+
private trace;
|
|
189
193
|
private artifactCounter;
|
|
190
194
|
/**
|
|
191
195
|
* Attempt-scoped network routes. Registered on the
|
|
@@ -260,8 +264,8 @@ export declare class PlaywrightSurface {
|
|
|
260
264
|
* Releases attempt resources, the worker's shared browser process, and a
|
|
261
265
|
* lease the worker acquired for itself: three independent tasks, in that
|
|
262
266
|
* order, every one attempted whatever the earlier ones did, and the first
|
|
263
|
-
* failure reported once all ran. A billed browser outlives a failed
|
|
264
|
-
* flush otherwise.
|
|
267
|
+
* failure reported once all ran. A billed browser outlives a failed
|
|
268
|
+
* recording flush otherwise.
|
|
265
269
|
*/
|
|
266
270
|
dispose(context: EngineCleanupContext): Promise<void>;
|
|
267
271
|
/** Returns the active attempt, including its connection generation and references. */
|
|
@@ -276,6 +280,12 @@ export declare class PlaywrightSurface {
|
|
|
276
280
|
projectPath(file: string): string;
|
|
277
281
|
/** Removes one registered route from the attempt and the current context. */
|
|
278
282
|
unroute(predicate: RoutePredicate, handler: RouteHandler): Promise<void>;
|
|
283
|
+
/**
|
|
284
|
+
* The attempt's open page, or undefined, for a reader beside the test (the
|
|
285
|
+
* trace): unlike `requirePage`, it leaves an error a handler latched for
|
|
286
|
+
* the test's next operation to throw.
|
|
287
|
+
*/
|
|
288
|
+
private openPage;
|
|
279
289
|
requirePage(): Page;
|
|
280
290
|
requireContext(): BrowserContext;
|
|
281
291
|
/**
|
|
@@ -297,14 +307,20 @@ export declare class PlaywrightSurface {
|
|
|
297
307
|
* The single entry of every operation: rethrows an error latched on an
|
|
298
308
|
* unawaited path, refuses a cancelled operation, races `fn` against the
|
|
299
309
|
* operation signal so an abort mid-call surfaces as `CANCELLED` instead of
|
|
300
|
-
* waiting out Playwright,
|
|
310
|
+
* waiting out Playwright, bounds it by the operation's budget so a call a
|
|
311
|
+
* hung page never answers is `OPERATION_TIMEOUT`, and translates raw errors at the contract
|
|
301
312
|
* boundary. `translate` overrides the default translation for operations
|
|
302
|
-
* with a documented retryable failure mode
|
|
313
|
+
* with a documented retryable failure mode; `bound` is `test-code` for an
|
|
314
|
+
* operation that runs the test's own code, which the deadline never cuts.
|
|
303
315
|
*/
|
|
304
|
-
guard<T>(operation: OperationContext, label: string, fn: (operation: OperationContext) => Promise<T>, translate?: (cause: unknown, label: string) => Error): Promise<T>;
|
|
316
|
+
guard<T>(operation: OperationContext, label: string, fn: (operation: OperationContext) => Promise<T>, translate?: (cause: unknown, label: string) => Error, bound?: OperationBound): Promise<T>;
|
|
305
317
|
/** Opens one URL the harness resolved; the attempt's page is created on first use. */
|
|
306
318
|
open(url: string, operation: OperationContext): Promise<void>;
|
|
307
319
|
back(operation: OperationContext): Promise<void>;
|
|
320
|
+
/** Runs one navigation the engine itself makes, so the app log leaves it to the step that asked for it. */
|
|
321
|
+
navigating<T>(work: () => Promise<T>): Promise<T>;
|
|
322
|
+
/** Resolves with what an action resolved with once the trace has the screen it left (`TraceFeed.screenAfterAction`); an action that failed leaves none. */
|
|
323
|
+
private acted;
|
|
308
324
|
/** Restarts the document while retaining this attempt's context and storage. */
|
|
309
325
|
restart(operation: OperationContext): Promise<void>;
|
|
310
326
|
/** Replaces the context with a clean one and opens its blank page. */
|
|
@@ -353,9 +369,12 @@ export declare class PlaywrightSurface {
|
|
|
353
369
|
* Checks that every frame selector along the expression matches exactly one
|
|
354
370
|
* element, each counted inside the frame before it, the way `project` walks
|
|
355
371
|
* the same chain; a frame-scoped `count` is 0 until that frame's document
|
|
356
|
-
* has loaded, which is what makes `FRAME_NOT_FOUND` worth retrying.
|
|
372
|
+
* has loaded, which is what makes `FRAME_NOT_FOUND` worth retrying. Returns
|
|
373
|
+
* the innermost frame's content box in the top-level viewport, where its
|
|
374
|
+
* document's viewport starts: undefined for an expression in the main
|
|
375
|
+
* document, null for a frame with no box.
|
|
357
376
|
*/
|
|
358
|
-
private
|
|
377
|
+
private frameBox;
|
|
359
378
|
/**
|
|
360
379
|
* Reserves one artifact path under the attempt directory: a per-attempt
|
|
361
380
|
* counter keeps names unique, the label keeps them readable.
|
|
@@ -366,11 +385,7 @@ export declare class PlaywrightSurface {
|
|
|
366
385
|
};
|
|
367
386
|
/** Redacted at the source: secure fields in every frame are masked, as in observation pixels. */
|
|
368
387
|
screenshot(label: string | undefined, operation: OperationContext): Promise<string>;
|
|
369
|
-
/** Starts
|
|
370
|
-
startTrace(operation: OperationContext): Promise<void>;
|
|
371
|
-
/** Returns every trace segment finalized by the current attempt. */
|
|
372
|
-
stopTrace(operation: OperationContext): Promise<string | readonly string[]>;
|
|
373
|
-
/** Starts the attempt's video before a trace chooses its screencast dimensions. */
|
|
388
|
+
/** Starts the attempt's video. */
|
|
374
389
|
startVideo(operation: OperationContext): Promise<void>;
|
|
375
390
|
/** Finishes the attempt's recording and returns its finalized segments. */
|
|
376
391
|
stopVideo(operation: OperationContext): Promise<readonly VideoSegment[]>;
|