@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/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, element) => {
40
+ commit: (id, target) => {
41
41
  if (accepting && !operation.signal.aborted)
42
- generation.set(id, { kind: 'element', element });
42
+ generation.set(id, target);
43
43
  else
44
- void element.dispose().catch(() => undefined);
44
+ target.list.dispose();
45
45
  },
46
46
  }, page, {
47
47
  framePath: [], budget: MAX_OBSERVED_NODES,
@@ -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 { ElementHandle, Frame } from 'playwright-core';
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 element handle under its id in the generation being built. */
25
- commit(id: string, element: ElementHandle<Element>): void;
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 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.
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 placeRectInFrame(rect: Rect, frame: Rect): Rect | undefined;
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 {};
@@ -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 stage = (id, element) => {
41
- if (scope.own(element))
42
- staged.push([id, element]);
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, element] of staged) {
48
- deps.commit(id, element);
49
- scope.release(element);
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 elements = await collectElementHandles(scope, elementsHandle, nodes.length);
103
- elements.forEach((element, index) => stage(ids[index], element));
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
- const frame = await scope.read(() => elements[index].contentFrame().catch(() => null));
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, elements[index]))))
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(() => elements[index].evaluate(frameContentInset).catch(() => undefined));
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
- ...(placed === undefined ? {} : { rect: placed }),
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 top-level
269
- * viewport's space: shifted by the frame's box and clipped to it; undefined
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 placeRectInFrame(rect, frame) {
273
- return clipRect(offsetRect(rect, frame), frame);
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
  };
@@ -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. Handle-backed refs from `observe` form one generation per
8
- * observation: the whole generation is swapped atomically and the superseded
9
- * one disposed in a sweep, so locator-ref eviction can never destroy a handle
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, ActionTarget>;
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 observation generation. */
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. Handle-backed refs from `observe` form one generation per
8
- * observation: the whole generation is swapped atomically and the superseded
9
- * one disposed in a sweep, so locator-ref eviction can never destroy a handle
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 target = this.located.get(ref.id) ?? this.observation.get(ref.id);
49
- if (target === undefined) {
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 target;
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 observation generation. */
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 === 'element')
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 a live element handle captured during one agent observation. Handles are
17
- * always elements: the in-page observation walk records `Element` nodes only.
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
- /** Runs one navigation the engine itself makes, so the app log leaves it to the step that asked for it. */
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 keeps a live
428
- * element handle valid until the next observation replaces the generation.
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