@e2e-dev/web 0.15.5-nightly-20261105030216 → 0.15.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/.build.json +1 -1
- package/dist/actions.d.ts +1 -1
- package/dist/actions.js +17 -6
- package/dist/browser-connection.d.ts +2 -3
- package/dist/browser-connection.js +18 -5
- package/dist/browser.js +35 -25
- package/dist/element-list.d.ts +28 -0
- package/dist/element-list.js +73 -0
- package/dist/evaluation.d.ts +20 -12
- package/dist/evaluation.js +107 -10
- package/dist/in-page/read-semantics.js +3 -0
- package/dist/install.d.ts +2 -2
- package/dist/install.js +5 -5
- package/dist/observation-capture.js +3 -3
- package/dist/observation.d.ts +12 -8
- package/dist/observation.js +37 -44
- package/dist/read-node.d.ts +5 -0
- package/dist/refs.d.ts +17 -8
- package/dist/refs.js +20 -10
- package/dist/support.d.ts +16 -3
- package/dist/support.js +31 -0
- package/dist/surface.d.ts +21 -4
- package/dist/surface.js +195 -97
- package/package.json +3 -3
package/dist/install.js
CHANGED
|
@@ -18,11 +18,11 @@ function headlessShellInstalled(browsersPath, revision) {
|
|
|
18
18
|
* Whether the build the run launches (headless shell for headless chromium, else the full build)
|
|
19
19
|
* is on disk, or undefined when `env` points at another cache than this process loaded.
|
|
20
20
|
*/
|
|
21
|
-
function isBrowserInstalled(name, env, headed) {
|
|
21
|
+
async function isBrowserInstalled(name, env, headed) {
|
|
22
22
|
if (env['PLAYWRIGHT_BROWSERS_PATH'] !== process.env['PLAYWRIGHT_BROWSERS_PATH'])
|
|
23
23
|
return undefined;
|
|
24
24
|
try {
|
|
25
|
-
const executable = browserType(name).executablePath();
|
|
25
|
+
const executable = (await browserType(name)).executablePath();
|
|
26
26
|
if (headed || name !== 'chromium')
|
|
27
27
|
return existsSync(executable);
|
|
28
28
|
return headlessShellAt(executable);
|
|
@@ -79,8 +79,8 @@ export function runPlaywrightCli(args) {
|
|
|
79
79
|
* disk, a `warn` when the first run will download it, or none when `env`
|
|
80
80
|
* points at another browser cache than this process loaded.
|
|
81
81
|
*/
|
|
82
|
-
export function browserCheck(name, env, headed, isInstalled = (browser) => isBrowserInstalled(browser, env, headed)) {
|
|
83
|
-
const installed = isInstalled(name);
|
|
82
|
+
export async function browserCheck(name, env, headed, isInstalled = (browser) => isBrowserInstalled(browser, env, headed)) {
|
|
83
|
+
const installed = await isInstalled(name);
|
|
84
84
|
if (installed === undefined)
|
|
85
85
|
return [];
|
|
86
86
|
if (installed)
|
|
@@ -115,7 +115,7 @@ export async function ensureBrowsersInstalled(names, options = {}) {
|
|
|
115
115
|
const env = options.env ?? process.env;
|
|
116
116
|
const headed = options.headed ?? false;
|
|
117
117
|
const isInstalled = options.isInstalled ?? ((name) => isBrowserInstalled(name, env, headed));
|
|
118
|
-
const verdicts = new Map([...new Set(names)].map((name) => [name, isInstalled(name)]));
|
|
118
|
+
const verdicts = new Map(await Promise.all([...new Set(names)].map(async (name) => [name, await isInstalled(name)])));
|
|
119
119
|
const missing = [...verdicts.keys()].filter((name) => verdicts.get(name) !== true);
|
|
120
120
|
if (missing.length === 0)
|
|
121
121
|
return;
|
|
@@ -37,11 +37,11 @@ export async function captureObservation(page, refs, settings, operation, option
|
|
|
37
37
|
captureDocument({
|
|
38
38
|
...settings,
|
|
39
39
|
reserveIds: (count) => refs.reserveIds(count),
|
|
40
|
-
commit: (id,
|
|
40
|
+
commit: (id, target) => {
|
|
41
41
|
if (accepting && !operation.signal.aborted)
|
|
42
|
-
generation.set(id,
|
|
42
|
+
generation.set(id, target);
|
|
43
43
|
else
|
|
44
|
-
|
|
44
|
+
target.list.dispose();
|
|
45
45
|
},
|
|
46
46
|
}, page, {
|
|
47
47
|
framePath: [], budget: MAX_OBSERVED_NODES,
|
package/dist/observation.d.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Semantic tree capture for one observation: the in-page walk, nested-document
|
|
3
3
|
* stitching, and element-handle bookkeeping the surface keeps per generation.
|
|
4
4
|
*/
|
|
5
|
-
import type {
|
|
6
|
-
import { type NodeRef, type SemanticNode } from 'e2e/engine';
|
|
5
|
+
import type { Frame } from 'playwright-core';
|
|
6
|
+
import { type NodeRef, type SemanticNode, type ViewportPoint } from 'e2e/engine';
|
|
7
7
|
import { type RawNodeData } from './read-node.ts';
|
|
8
|
+
import type { ListedTarget } from './refs.ts';
|
|
8
9
|
/**
|
|
9
10
|
* The id of every observation's root: the main document's element under one
|
|
10
11
|
* id the engine mints, stable across observations and navigations, so the
|
|
@@ -21,8 +22,8 @@ export interface CaptureDeps {
|
|
|
21
22
|
readonly site: string | undefined;
|
|
22
23
|
/** Reserves a disjoint range before the reader can stamp any element, including on timeout. */
|
|
23
24
|
reserveIds(count: number): number;
|
|
24
|
-
/** Publishes one
|
|
25
|
-
commit(id: string,
|
|
25
|
+
/** Publishes one observed node under its id in the generation being built. */
|
|
26
|
+
commit(id: string, target: ListedTarget): void;
|
|
26
27
|
}
|
|
27
28
|
export interface CaptureOptions {
|
|
28
29
|
/** Enclosing frame selectors, outermost first; empty for the main document. */
|
|
@@ -69,10 +70,13 @@ export declare function frameContentInset(element: Element): Rect;
|
|
|
69
70
|
/** A frame element's content box, from its border box and `frameContentInset`. */
|
|
70
71
|
export declare function contentBox(border: Rect, inset: Rect): Rect;
|
|
71
72
|
/**
|
|
72
|
-
* A box a child document measured against its own viewport, in the
|
|
73
|
-
* viewport's space: shifted by the frame's box and clipped
|
|
74
|
-
* when nothing of it is inside the frame.
|
|
73
|
+
* A node box a child document measured against its own viewport, in the
|
|
74
|
+
* top-level viewport's space: `rect` shifted by the frame's box and clipped
|
|
75
|
+
* to it, empty when nothing of it is inside the frame. `origin` is shifted
|
|
76
|
+
* but never clipped, and a clip that moves the box's corner keeps the
|
|
77
|
+
* unclipped corner as `origin`, so an offset into the node lands where it
|
|
78
|
+
* would at the top level.
|
|
75
79
|
*/
|
|
76
|
-
export declare function
|
|
80
|
+
export declare function placeBoxInFrame(rect: Rect, origin: ViewportPoint | undefined, frame: Rect): Pick<SemanticNode, 'rect' | 'origin'>;
|
|
77
81
|
export declare function toSemanticNode(ref: NodeRef, raw: RawNodeData, children?: readonly SemanticNode[], framePath?: readonly string[]): SemanticNode;
|
|
78
82
|
export {};
|
package/dist/observation.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { EngineError, sameSite, OBSERVED_NAME_LIMIT, OBSERVED_TEXT_LIMIT, } from 'e2e/engine';
|
|
6
6
|
import { readDocumentSemanticsFunction, SECURE_FIELD_SELECTOR, } from './read-node.js';
|
|
7
7
|
import { CaptureScope } from './capture-scope.js';
|
|
8
|
+
import { ElementList } from './element-list.js';
|
|
8
9
|
import { hasReadableDocument } from './frame-document.js';
|
|
9
10
|
/**
|
|
10
11
|
* The id of every observation's root: the main document's element under one
|
|
@@ -37,16 +38,22 @@ export async function captureDocument(deps, host, options) {
|
|
|
37
38
|
// A child document's handles are published only once it captured whole; a
|
|
38
39
|
// frame that fails midway leaves no reachable ids behind.
|
|
39
40
|
const staged = [];
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
const accepted = new Map();
|
|
42
|
+
const stage = (id, target) => {
|
|
43
|
+
let owned = accepted.get(target.list);
|
|
44
|
+
if (owned === undefined) {
|
|
45
|
+
owned = scope.own(target.list.handle);
|
|
46
|
+
accepted.set(target.list, owned);
|
|
47
|
+
}
|
|
48
|
+
if (owned)
|
|
49
|
+
staged.push([id, target]);
|
|
43
50
|
};
|
|
44
51
|
return scope.run(async () => {
|
|
45
52
|
const captured = await captureInto(deps, scope, stage, host, framePath, budget, signal);
|
|
46
53
|
scope.check();
|
|
47
|
-
for (const [id,
|
|
48
|
-
deps.commit(id,
|
|
49
|
-
scope.release(
|
|
54
|
+
for (const [id, target] of staged) {
|
|
55
|
+
deps.commit(id, target);
|
|
56
|
+
scope.release(target.list.handle);
|
|
50
57
|
}
|
|
51
58
|
return captured;
|
|
52
59
|
});
|
|
@@ -99,8 +106,8 @@ async function captureInto(deps, scope, stage, host, framePath, budget, signal)
|
|
|
99
106
|
if (framePath.length === 0 && ids.length > 0)
|
|
100
107
|
ids[0] = ROOT_NODE_ID;
|
|
101
108
|
const elementsHandle = await scope.read(() => captured.getProperty('elements'), (handle) => scope.own(handle));
|
|
102
|
-
const
|
|
103
|
-
|
|
109
|
+
const list = new ElementList(elementsHandle);
|
|
110
|
+
ids.forEach((id, index) => stage(id, { kind: 'listed', list, index }));
|
|
104
111
|
let nodeCount = nodes.length;
|
|
105
112
|
let truncated = walkTruncated === true;
|
|
106
113
|
const frameChildren = new Map();
|
|
@@ -110,7 +117,10 @@ async function captureInto(deps, scope, stage, host, framePath, budget, signal)
|
|
|
110
117
|
continue;
|
|
111
118
|
// A frame with no document (detached, never loaded) shows nothing, so
|
|
112
119
|
// nothing is missing from the tree.
|
|
113
|
-
|
|
120
|
+
// Owned by the scope and released with it: the frame's element is read
|
|
121
|
+
// here, and an action reaches it later through the list like any node.
|
|
122
|
+
const element = await scope.read(() => list.fetch(index), (handle) => scope.own(handle));
|
|
123
|
+
const frame = await scope.read(() => element.contentFrame().catch(() => null));
|
|
114
124
|
if (frame === null)
|
|
115
125
|
continue;
|
|
116
126
|
// Only frames on the app's site enter observations. Third-party
|
|
@@ -120,7 +130,7 @@ async function captureInto(deps, scope, stage, host, framePath, budget, signal)
|
|
|
120
130
|
if (!isOnSiteFrame(frame.url(), deps.site))
|
|
121
131
|
continue;
|
|
122
132
|
// A frame whose navigation has not committed shows nothing yet.
|
|
123
|
-
if (!(await scope.read(() => hasReadableDocument(frame,
|
|
133
|
+
if (!(await scope.read(() => hasReadableDocument(frame, element))))
|
|
124
134
|
continue;
|
|
125
135
|
// From here the frame's document is content the model should see: any
|
|
126
136
|
// reason it is not read (depth, budget, deadline, a failed or timed-out
|
|
@@ -134,7 +144,7 @@ async function captureInto(deps, scope, stage, host, framePath, budget, signal)
|
|
|
134
144
|
truncated = true;
|
|
135
145
|
continue;
|
|
136
146
|
}
|
|
137
|
-
const inset = await scope.read(() =>
|
|
147
|
+
const inset = await scope.read(() => element.evaluate(frameContentInset).catch(() => undefined));
|
|
138
148
|
if (inset === undefined) {
|
|
139
149
|
truncated = true;
|
|
140
150
|
continue;
|
|
@@ -174,31 +184,6 @@ function isOnSiteFrame(url, site) {
|
|
|
174
184
|
const protocol = URL.canParse(url) ? new URL(url).protocol : undefined;
|
|
175
185
|
return protocol !== 'data:' && sameSite(url, site);
|
|
176
186
|
}
|
|
177
|
-
/**
|
|
178
|
-
* Reads one element handle per observed node from the in-page element array.
|
|
179
|
-
* `asElement` types handles as `ElementHandle<Node>`, but the observation walk
|
|
180
|
-
* records `Element` nodes only, so the narrowing is safe by construction.
|
|
181
|
-
*/
|
|
182
|
-
async function collectElementHandles(scope, elementsHandle, count) {
|
|
183
|
-
const properties = await scope.read(() => elementsHandle.getProperties(), (handles) => {
|
|
184
|
-
for (const handle of handles.values())
|
|
185
|
-
scope.own(handle);
|
|
186
|
-
});
|
|
187
|
-
const elements = [];
|
|
188
|
-
for (let index = 0; index < count; index += 1) {
|
|
189
|
-
const property = properties.get(String(index));
|
|
190
|
-
const element = (property?.asElement() ?? null);
|
|
191
|
-
if (element === null) {
|
|
192
|
-
// The in-page array outlived its document (a navigation committed while
|
|
193
|
-
// the handles were being read back). The capture is repeatable.
|
|
194
|
-
throw new EngineError('NODE_STALE', `observation node ${index} lost its element`, {
|
|
195
|
-
retryable: true,
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
elements.push(element);
|
|
199
|
-
}
|
|
200
|
-
return elements;
|
|
201
|
-
}
|
|
202
187
|
/**
|
|
203
188
|
* Rebuilds the observation tree from the depth-first node list. Descendants
|
|
204
189
|
* always follow their parent, so children are complete before a parent is
|
|
@@ -256,21 +241,28 @@ export function contentBox(border, inset) {
|
|
|
256
241
|
}
|
|
257
242
|
/** 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. */
|
|
258
243
|
function placeInFrame(node, frame) {
|
|
259
|
-
const { rect, ...rest } = node;
|
|
260
|
-
const placed = rect === undefined ? undefined : placeRectInFrame(rect, frame);
|
|
244
|
+
const { rect, origin, ...rest } = node;
|
|
261
245
|
return {
|
|
262
246
|
...rest,
|
|
263
|
-
...(
|
|
247
|
+
...(rect === undefined ? {} : placeBoxInFrame(rect, origin, frame)),
|
|
264
248
|
...(node.children === undefined ? {} : { children: node.children.map((child) => placeInFrame(child, frame)) }),
|
|
265
249
|
};
|
|
266
250
|
}
|
|
267
251
|
/**
|
|
268
|
-
* A box a child document measured against its own viewport, in the
|
|
269
|
-
* viewport's space: shifted by the frame's box and clipped
|
|
270
|
-
* when nothing of it is inside the frame.
|
|
252
|
+
* A node box a child document measured against its own viewport, in the
|
|
253
|
+
* top-level viewport's space: `rect` shifted by the frame's box and clipped
|
|
254
|
+
* to it, empty when nothing of it is inside the frame. `origin` is shifted
|
|
255
|
+
* but never clipped, and a clip that moves the box's corner keeps the
|
|
256
|
+
* unclipped corner as `origin`, so an offset into the node lands where it
|
|
257
|
+
* would at the top level.
|
|
271
258
|
*/
|
|
272
|
-
export function
|
|
273
|
-
|
|
259
|
+
export function placeBoxInFrame(rect, origin, frame) {
|
|
260
|
+
const placed = clipRect(offsetRect(rect, frame), frame);
|
|
261
|
+
if (placed === undefined)
|
|
262
|
+
return {};
|
|
263
|
+
const corner = origin ?? rect;
|
|
264
|
+
const shifted = { x: corner.x + frame.x, y: corner.y + frame.y };
|
|
265
|
+
return shifted.x === placed.x && shifted.y === placed.y ? { rect: placed } : { rect: placed, origin: shifted };
|
|
274
266
|
}
|
|
275
267
|
function offsetRect(rect, by) {
|
|
276
268
|
return { x: rect.x + by.x, y: rect.y + by.y, width: rect.width, height: rect.height };
|
|
@@ -313,6 +305,7 @@ export function toSemanticNode(ref, raw, children = [], framePath = []) {
|
|
|
313
305
|
...(raw.level !== null ? { level: raw.level } : {}),
|
|
314
306
|
attributes: raw.attributes,
|
|
315
307
|
...(raw.rect !== null ? { rect: raw.rect } : {}),
|
|
308
|
+
...(raw.origin !== null ? { origin: raw.origin } : {}),
|
|
316
309
|
...(framePath.length > 0 ? { framePath } : {}),
|
|
317
310
|
...(children.length > 0 ? { children } : {}),
|
|
318
311
|
};
|
package/dist/read-node.d.ts
CHANGED
|
@@ -55,6 +55,11 @@ export interface RawNodeData {
|
|
|
55
55
|
width: number;
|
|
56
56
|
height: number;
|
|
57
57
|
} | null;
|
|
58
|
+
/** The element's padding-box corner in the same viewport, when a border puts it past `rect`'s; null otherwise. */
|
|
59
|
+
origin: {
|
|
60
|
+
x: number;
|
|
61
|
+
y: number;
|
|
62
|
+
} | null;
|
|
58
63
|
}
|
|
59
64
|
/** One observed node plus its position in the flattened depth-first tree. */
|
|
60
65
|
export interface RawObservedNode extends RawNodeData {
|
package/dist/refs.d.ts
CHANGED
|
@@ -4,17 +4,24 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Two populations with different lifetimes live here. Locator-backed refs
|
|
6
6
|
* from `locate` hold no live handles and are pruned oldest-first past a
|
|
7
|
-
* bound.
|
|
8
|
-
* observation: the whole generation is swapped atomically and
|
|
9
|
-
*
|
|
7
|
+
* bound. List-backed refs from `observe` form one generation per
|
|
8
|
+
* observation: the whole generation is swapped atomically and its element
|
|
9
|
+
* lists disposed in a sweep, so locator-ref eviction can never destroy a list
|
|
10
10
|
* an in-flight observation still references.
|
|
11
11
|
*/
|
|
12
12
|
import { type EngineSnapshot, type NodeRef } from 'e2e/engine';
|
|
13
|
+
import type { ElementList } from './element-list.ts';
|
|
13
14
|
import type { ActionTarget } from './support.ts';
|
|
15
|
+
/** An observed node: one position in the element list of the document it was read from. */
|
|
16
|
+
export interface ListedTarget {
|
|
17
|
+
readonly kind: 'listed';
|
|
18
|
+
readonly list: ElementList;
|
|
19
|
+
readonly index: number;
|
|
20
|
+
}
|
|
14
21
|
/** A complete observation and the private handles that back its semantic refs. */
|
|
15
22
|
export interface CapturedObservation {
|
|
16
23
|
readonly snapshot: EngineSnapshot;
|
|
17
|
-
readonly generation: Map<string,
|
|
24
|
+
readonly generation: Map<string, ListedTarget>;
|
|
18
25
|
}
|
|
19
26
|
export declare class RefRegistry {
|
|
20
27
|
private counter;
|
|
@@ -28,15 +35,17 @@ export declare class RefRegistry {
|
|
|
28
35
|
storeLocated(target: ActionTarget): string;
|
|
29
36
|
/**
|
|
30
37
|
* Ids are the engine's; revisions are the harness's. The adapter already
|
|
31
|
-
* rejected a ref from a superseded resolution, so lookup is by id alone.
|
|
38
|
+
* rejected a ref from a superseded resolution, so lookup is by id alone. An
|
|
39
|
+
* observed node's element handle is made here, the first time an action
|
|
40
|
+
* reaches for it.
|
|
32
41
|
*/
|
|
33
|
-
lookup(ref: NodeRef): ActionTarget
|
|
42
|
+
lookup(ref: NodeRef): Promise<ActionTarget>;
|
|
34
43
|
/** Starts a capture, preventing older in-flight observations from publishing afterward. */
|
|
35
44
|
beginCapture(): number;
|
|
36
45
|
/** Publishes only the current capture; pixel-only recovery also retires located refs. */
|
|
37
46
|
publish(capture: number, { snapshot, generation }: CapturedObservation): void;
|
|
38
47
|
/** Releases every ref of the attempt: located and observed alike. */
|
|
39
48
|
clear(): void;
|
|
40
|
-
/** Disposes every element handle in one
|
|
41
|
-
static dispose(generation: ReadonlyMap<string, ActionTarget>): void;
|
|
49
|
+
/** Disposes every element handle in one generation, each list once however many nodes it backs. */
|
|
50
|
+
static dispose(generation: ReadonlyMap<string, ActionTarget | ListedTarget>): void;
|
|
42
51
|
}
|
package/dist/refs.js
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Two populations with different lifetimes live here. Locator-backed refs
|
|
6
6
|
* from `locate` hold no live handles and are pruned oldest-first past a
|
|
7
|
-
* bound.
|
|
8
|
-
* observation: the whole generation is swapped atomically and
|
|
9
|
-
*
|
|
7
|
+
* bound. List-backed refs from `observe` form one generation per
|
|
8
|
+
* observation: the whole generation is swapped atomically and its element
|
|
9
|
+
* lists disposed in a sweep, so locator-ref eviction can never destroy a list
|
|
10
10
|
* an in-flight observation still references.
|
|
11
11
|
*/
|
|
12
12
|
import { EngineError } from 'e2e/engine';
|
|
@@ -42,14 +42,19 @@ export class RefRegistry {
|
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
44
|
* Ids are the engine's; revisions are the harness's. The adapter already
|
|
45
|
-
* rejected a ref from a superseded resolution, so lookup is by id alone.
|
|
45
|
+
* rejected a ref from a superseded resolution, so lookup is by id alone. An
|
|
46
|
+
* observed node's element handle is made here, the first time an action
|
|
47
|
+
* reaches for it.
|
|
46
48
|
*/
|
|
47
|
-
lookup(ref) {
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
49
|
+
async lookup(ref) {
|
|
50
|
+
const located = this.located.get(ref.id);
|
|
51
|
+
if (located !== undefined)
|
|
52
|
+
return located;
|
|
53
|
+
const observed = this.observation.get(ref.id);
|
|
54
|
+
if (observed === undefined) {
|
|
50
55
|
throw new EngineError('NODE_STALE', `node reference ${ref.id} is stale`, { retryable: true });
|
|
51
56
|
}
|
|
52
|
-
return
|
|
57
|
+
return { kind: 'element', element: await observed.list.at(observed.index) };
|
|
53
58
|
}
|
|
54
59
|
/** Starts a capture, preventing older in-flight observations from publishing afterward. */
|
|
55
60
|
beginCapture() {
|
|
@@ -74,11 +79,16 @@ export class RefRegistry {
|
|
|
74
79
|
RefRegistry.dispose(this.located);
|
|
75
80
|
this.located.clear();
|
|
76
81
|
}
|
|
77
|
-
/** Disposes every element handle in one
|
|
82
|
+
/** Disposes every element handle in one generation, each list once however many nodes it backs. */
|
|
78
83
|
static dispose(generation) {
|
|
84
|
+
const lists = new Set();
|
|
79
85
|
for (const target of generation.values()) {
|
|
80
|
-
if (target.kind === '
|
|
86
|
+
if (target.kind === 'listed')
|
|
87
|
+
lists.add(target.list);
|
|
88
|
+
else if (target.kind === 'element')
|
|
81
89
|
void target.element.dispose().catch(() => undefined);
|
|
82
90
|
}
|
|
91
|
+
for (const list of lists)
|
|
92
|
+
list.dispose();
|
|
83
93
|
}
|
|
84
94
|
}
|
package/dist/support.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Shared error translation, filename, and swipe helpers for the Playwright engine. */
|
|
2
|
-
import type { ElementHandle, Locator as PwLocator, Mouse, Page } from 'playwright-core';
|
|
2
|
+
import type { ElementHandle, FrameLocator, Locator as PwLocator, Mouse, Page } from 'playwright-core';
|
|
3
3
|
import { EngineError, type EngineCleanupContext, type Momentum, type ScrollDirection, type ViewportPoint, type ViewportSize } from 'e2e/engine';
|
|
4
|
+
import { TestError } from 'e2e/engine';
|
|
4
5
|
export declare const DEFAULT_VIEWPORT: {
|
|
5
6
|
readonly width: 1280;
|
|
6
7
|
readonly height: 720;
|
|
@@ -13,8 +14,9 @@ export declare const DEFAULT_VIEWPORT: {
|
|
|
13
14
|
export declare function currentViewport(page: Page): Promise<ViewportSize>;
|
|
14
15
|
/**
|
|
15
16
|
* A resolved node is addressed either by a deterministic locator expression or
|
|
16
|
-
* by
|
|
17
|
-
* always elements: the in-page observation walk records `Element`
|
|
17
|
+
* by an element handle made from one agent observation's element list.
|
|
18
|
+
* Handles are always elements: the in-page observation walk records `Element`
|
|
19
|
+
* nodes only.
|
|
18
20
|
*/
|
|
19
21
|
export type ActionTarget = {
|
|
20
22
|
readonly kind: 'locator';
|
|
@@ -127,6 +129,17 @@ export declare const POST_DISPATCH_PATTERN: RegExp;
|
|
|
127
129
|
export declare function translatePwError(cause: unknown, operation: string): Error;
|
|
128
130
|
/** Whether a Playwright failure describes a read that lost its document to a navigation. */
|
|
129
131
|
export declare function isNavigationRace(cause: unknown): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* The failure of an operation whose in-page code the page's own scripts broke,
|
|
134
|
+
* as `EVALUATE_FAILED`, or undefined when something else failed. A page that
|
|
135
|
+
* deletes or replaces a JavaScript built-in (`Map`, `Object`) breaks every
|
|
136
|
+
* script Playwright runs in its main world, e2e's locate reads included. Told
|
|
137
|
+
* apart by asking the frame in both worlds: Playwright's utility world still
|
|
138
|
+
* reads the document before and after a main-world query that runs no e2e
|
|
139
|
+
* code fails, so the frame lives throughout and e2e's own code is not the
|
|
140
|
+
* cause.
|
|
141
|
+
*/
|
|
142
|
+
export declare function pageScriptFailure(scope: Page | FrameLocator, cause: unknown, operation: string, timeoutMs: number): Promise<TestError | undefined>;
|
|
130
143
|
/**
|
|
131
144
|
* Like translatePwError, but a read that lost its document to a navigation
|
|
132
145
|
* becomes retryable NODE_STALE so the runner re-reads the new document instead
|
package/dist/support.js
CHANGED
|
@@ -283,6 +283,37 @@ const NAVIGATION_RACE_PATTERN = /execution context was destroyed|because of a na
|
|
|
283
283
|
export function isNavigationRace(cause) {
|
|
284
284
|
return NAVIGATION_RACE_PATTERN.test(message(cause));
|
|
285
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* The failure of an operation whose in-page code the page's own scripts broke,
|
|
288
|
+
* as `EVALUATE_FAILED`, or undefined when something else failed. A page that
|
|
289
|
+
* deletes or replaces a JavaScript built-in (`Map`, `Object`) breaks every
|
|
290
|
+
* script Playwright runs in its main world, e2e's locate reads included. Told
|
|
291
|
+
* apart by asking the frame in both worlds: Playwright's utility world still
|
|
292
|
+
* reads the document before and after a main-world query that runs no e2e
|
|
293
|
+
* code fails, so the frame lives throughout and e2e's own code is not the
|
|
294
|
+
* cause.
|
|
295
|
+
*/
|
|
296
|
+
export async function pageScriptFailure(scope, cause, operation, timeoutMs) {
|
|
297
|
+
if (isClassified(cause) || isPwTimeout(cause) || isNavigationRace(cause))
|
|
298
|
+
return undefined;
|
|
299
|
+
const root = scope.locator(':root');
|
|
300
|
+
/** Whether Playwright's utility world still reads the frame's document. */
|
|
301
|
+
const alive = () => root.count().then(() => true, () => false);
|
|
302
|
+
if (!(await alive()))
|
|
303
|
+
return undefined;
|
|
304
|
+
try {
|
|
305
|
+
await root.evaluate(() => 0, undefined, { timeout: Math.max(timeoutMs, 1) });
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
catch (probe) {
|
|
309
|
+
if (isPwTimeout(probe) || isNavigationRace(probe))
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
// A crash or a closed target between the two probes fails this read too.
|
|
313
|
+
if (!(await alive()))
|
|
314
|
+
return undefined;
|
|
315
|
+
return new TestError('EVALUATE_FAILED', `${operation} failed: the page deleted or replaced a JavaScript built-in that in-page automation code needs: ${message(cause)}`, { cause });
|
|
316
|
+
}
|
|
286
317
|
/**
|
|
287
318
|
* Like translatePwError, but a read that lost its document to a navigation
|
|
288
319
|
* becomes retryable NODE_STALE so the runner re-reads the new document instead
|
package/dist/surface.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `actions.ts` and tree capture in `observation.ts`. `AttemptSession` owns
|
|
7
7
|
* the attempt's live binding, recovery, references, and recordings.
|
|
8
8
|
*/
|
|
9
|
-
import type { BrowserContext, Page, Route } from 'playwright-core';
|
|
9
|
+
import type { BrowserContext, Page, Request, Route } from 'playwright-core';
|
|
10
10
|
import { type EngineAttemptContext, type EngineCleanupContext, type EngineDoctorCheck, type EngineDoctorInfo, type EngineInitInfo, type EngineObserveOptions, type EnginePrepareInfo, type EnginePrepareResult, type EngineFinishInfo, type EngineSnapshot, type EngineState, type LocatorAction, type LocatorExpression, type NodeRef, type OperationContext, type PointerAction, type Secret, type SemanticNode, type VideoSegment, type ViewportPoint, type ViewportSize } from 'e2e/engine';
|
|
11
11
|
import { type BrowserName } from './browser-connection.ts';
|
|
12
12
|
import { type BrowserProvider, type LeaseDownloads } from './provider.ts';
|
|
@@ -206,6 +206,8 @@ export declare class PlaywrightSurface {
|
|
|
206
206
|
private artifactsDir;
|
|
207
207
|
/** Navigations the engine has in flight, which the app log leaves to the step that started them. */
|
|
208
208
|
private ownNavigations;
|
|
209
|
+
/** One entry per navigation in flight, set to the URL of a document request a test route aborted during it. */
|
|
210
|
+
private readonly navigationAborts;
|
|
209
211
|
/** What the attempt tells its trace beside the app log; absent between attempts. */
|
|
210
212
|
private trace;
|
|
211
213
|
private artifactCounter;
|
|
@@ -344,8 +346,18 @@ export declare class PlaywrightSurface {
|
|
|
344
346
|
back(operation: OperationContext): Promise<void>;
|
|
345
347
|
/** Sets the viewport for the agent's `resize` verb, as `browser.setViewport` does, and shows the screen after it. */
|
|
346
348
|
resize(size: ViewportSize, operation: OperationContext): Promise<void>;
|
|
347
|
-
/**
|
|
349
|
+
/**
|
|
350
|
+
* Runs one navigation the engine itself makes, so the app log leaves it to
|
|
351
|
+
* the step that asked for it. A navigation that fails after a test route
|
|
352
|
+
* aborted its document request failed because the test aborted it: an
|
|
353
|
+
* action failure, not an engine fault.
|
|
354
|
+
*/
|
|
348
355
|
navigating<T>(work: () => Promise<T>): Promise<T>;
|
|
356
|
+
/**
|
|
357
|
+
* Records a request a test route is about to abort, when it loads the
|
|
358
|
+
* document of the attempt's page. Never throws: the abort must follow.
|
|
359
|
+
*/
|
|
360
|
+
noteRouteAbort(request: Request): void;
|
|
349
361
|
/** Resolves with what an action resolved with once the trace has the screen it left (`TraceFeed.screenAfterAction`); an action that failed leaves none. */
|
|
350
362
|
private acted;
|
|
351
363
|
/** Restarts the document while retaining this attempt's context and storage. */
|
|
@@ -362,6 +374,10 @@ export declare class PlaywrightSurface {
|
|
|
362
374
|
* reported before either of those, so a position is among shown matches.
|
|
363
375
|
*/
|
|
364
376
|
locate(expression: LocatorExpression, operation: OperationContext): Promise<readonly SemanticNode[]>;
|
|
377
|
+
/** The body of `locate`, inside its guard. */
|
|
378
|
+
private locateNow;
|
|
379
|
+
/** The page or frame an expression's frame selectors lead to, unresolved. */
|
|
380
|
+
private frameScope;
|
|
365
381
|
/**
|
|
366
382
|
* One action on a located or observed node. A `swipe` on the observation
|
|
367
383
|
* root is the viewport swipe: a wheel gesture sized by the viewport, with no
|
|
@@ -424,8 +440,9 @@ export declare class PlaywrightSurface {
|
|
|
424
440
|
restoreState(state: EngineState, operation: OperationContext): Promise<void>;
|
|
425
441
|
/**
|
|
426
442
|
* Captures one atomic semantic observation. Secure fields are masked in the
|
|
427
|
-
* page before the tree leaves the engine, and every node
|
|
428
|
-
* element
|
|
443
|
+
* page before the tree leaves the engine, and every node stays reachable
|
|
444
|
+
* through its document's element list until the next observation replaces
|
|
445
|
+
* the generation; an action makes the node's handle on first use.
|
|
429
446
|
*
|
|
430
447
|
* With `options.pixels`, masked viewport pixels are captured alongside the
|
|
431
448
|
* tree rather than after it, so the image and the node geometry describe the
|