@e2e-dev/web 0.13.0 → 0.14.0-nightly-20261009094406

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.
@@ -54,11 +54,11 @@ export const readSemanticsFunction = (element, options) => {
54
54
  const PLACEHOLDER_NAMED_INPUT_TYPES = ['text', 'password', 'number', 'search', 'tel', 'email', 'url'];
55
55
  /**
56
56
  * How the active mode projects one node, expressed as data so `describe`
57
- * stays branch-free. Tree mode is the model-bound projection: bounded text,
58
- * a bounded attribute projection, hrefs reduced to origin+path, the root
59
- * document named by its title, and no value on a checkbox or radio, whose
60
- * value attribute is an app token its checked state already says more
61
- * than. Node mode is the full locator-read surface.
57
+ * stays branch-free. Tree mode is the model-bound projection: bounded line
58
+ * text (`lineTextOf`), a bounded attribute projection, hrefs reduced to
59
+ * origin+path, the root document named by its title, and no value on a
60
+ * checkbox or radio, whose value attribute is an app token its checked
61
+ * state already says more than. Node mode is the full locator-read surface.
62
62
  */
63
63
  const projection = options.mode.kind === 'tree'
64
64
  ? {
@@ -66,7 +66,7 @@ export const readSemanticsFunction = (element, options) => {
66
66
  textLimit: options.mode.textLimit,
67
67
  nameLimit: options.mode.nameLimit,
68
68
  redactHref: true,
69
- directTextOnly: true,
69
+ lineText: true,
70
70
  documentRoot: true,
71
71
  checkableValue: false,
72
72
  }
@@ -75,7 +75,7 @@ export const readSemanticsFunction = (element, options) => {
75
75
  textLimit: null,
76
76
  nameLimit: null,
77
77
  redactHref: false,
78
- directTextOnly: false,
78
+ lineText: false,
79
79
  documentRoot: false,
80
80
  checkableValue: true,
81
81
  };
@@ -111,7 +111,15 @@ export const readSemanticsFunction = (element, options) => {
111
111
  * HTML-AAM scopes it to sectioning content and to the ARIA roles that stand
112
112
  * for it, so a `<div role="article">` scopes it like an `<article>`.
113
113
  */
114
- const isPageLevel = (el) => el.closest(SECTIONING_SCOPE) === null;
114
+ const isPageLevel = (el) => {
115
+ for (let candidate = el; candidate !== null;) {
116
+ if (candidate.closest(SECTIONING_SCOPE) !== null)
117
+ return false;
118
+ const root = candidate.getRootNode();
119
+ candidate = root instanceof ShadowRoot ? root.host : null;
120
+ }
121
+ return true;
122
+ };
115
123
  /**
116
124
  * The root of a contenteditable region: editable itself, under a parent that
117
125
  * is not. A rich-text editor (ProseMirror, TipTap, Lexical, Slate) renders
@@ -368,7 +376,7 @@ export const readSemanticsFunction = (element, options) => {
368
376
  /** An element's `::after` contribution, once per read. */
369
377
  const afterContentOf = memoized((el) => generatedContentOf(el, '::after'));
370
378
  /** True for a subtree the name computation drops: aria-hidden, or hidden by style as innerText leaves it out. */
371
- const isNameHidden = (el, style) => el.getAttribute('aria-hidden') === 'true' ||
379
+ const isNameHidden = (el, style) => (el.getAttribute('aria-hidden') ?? '').toLowerCase() === 'true' ||
372
380
  (style !== undefined && (style.display === 'none' || style.visibility === 'hidden'));
373
381
  /** `alt` of an element HTML-AAM names by it: an `<img>` or an `<input type="image">`. */
374
382
  const altOf = (el) => {
@@ -399,7 +407,7 @@ export const readSemanticsFunction = (element, options) => {
399
407
  * under an `aria-hidden` ancestor, which excludes it from the tree as
400
408
  * surely as its own attribute would.
401
409
  */
402
- const isReferenceHidden = (el) => isHidden(el) || el.closest('[aria-hidden="true"]') !== null;
410
+ const isReferenceHidden = (el) => isHidden(el) || el.closest('[aria-hidden="true" i]') !== null;
403
411
  /**
404
412
  * The element an id names for `el`, looked up in `el`'s own tree: an IDREF
405
413
  * resolves in the tree scope it is written in, so a shadow tree's ids are
@@ -682,6 +690,121 @@ export const readSemanticsFunction = (element, options) => {
682
690
  }
683
691
  return out.replace(/\s+/g, ' ').trim();
684
692
  });
693
+ /**
694
+ * Elements whose content is not text in a line: a form control's options
695
+ * or value, a picture, a frame, a drawn surface. Like a line break, each
696
+ * separates the words around it.
697
+ */
698
+ const LINE_BREAKING_TAGS = [...SKIP_TAGS, ...OPAQUE_TAGS, 'iframe', 'img', 'input', 'select', 'textarea', 'br'];
699
+ /**
700
+ * True when an element's text flows in its parent's line: a visible
701
+ * inline-level box (`inline`, `inline-block`, `ruby`), or `display:
702
+ * contents`, whose children lay out as the parent's own. Visible as the
703
+ * walk reads it (`isHidden`): a box with no size shows no words. A slot
704
+ * has no box and shows the nodes assigned to it, so only its visibility
705
+ * counts: a hidden one hides the text they inherit it with, and a child
706
+ * that shows itself again is listed on its own.
707
+ */
708
+ const flowsInLine = memoized((el) => {
709
+ if (LINE_BREAKING_TAGS.indexOf(el.tagName.toLowerCase()) !== -1 || isEditingHost(el))
710
+ return false;
711
+ const style = styleOf(el);
712
+ if (style === undefined || hidesSubtree(el, style))
713
+ return false;
714
+ if (el instanceof HTMLSlotElement ? style.visibility !== 'visible' : isHidden(el, style))
715
+ return false;
716
+ return isInlineLevel(style);
717
+ });
718
+ /** True for a box laid out in its parent's line, or for `display: contents`, which lays out none of its own. */
719
+ const isInlineLevel = (style) => style.display.startsWith('inline') || style.display.startsWith('ruby') || style.display === 'contents';
720
+ /**
721
+ * The nodes an element renders in its place, as the walk reaches them: a
722
+ * shadow host's shadow tree (a closed one through the record), a slot's
723
+ * assigned nodes or else its fallback content, any other element's children.
724
+ */
725
+ const renderedChildrenOf = (el) => {
726
+ const shadow = shadowRootOf(el);
727
+ if (shadow !== null)
728
+ return Array.from(shadow.childNodes);
729
+ if (el instanceof HTMLSlotElement) {
730
+ const assigned = el.assignedNodes();
731
+ if (assigned.length > 0)
732
+ return assigned;
733
+ }
734
+ return Array.from(el.childNodes);
735
+ };
736
+ /**
737
+ * An element's rendered text with each inline descendant's text in place.
738
+ * Anything else in the line reads as a space, except a hidden inline
739
+ * element (a `<wbr>`, a zero-size span), which shows nothing.
740
+ */
741
+ const lineRunOf = (el) => {
742
+ let out = '';
743
+ for (const child of renderedChildrenOf(el)) {
744
+ if (child.nodeType === 3)
745
+ out += child.nodeValue ?? '';
746
+ if (child.nodeType !== 1)
747
+ continue;
748
+ const inner = child;
749
+ if (flowsInLine(inner)) {
750
+ out += lineRunOf(inner);
751
+ continue;
752
+ }
753
+ const style = styleOf(inner);
754
+ if (style === undefined || hidesSubtree(inner, style))
755
+ continue;
756
+ const breaking = LINE_BREAKING_TAGS.indexOf(inner.tagName.toLowerCase()) !== -1;
757
+ if (breaking || !isInlineLevel(style) || !isHidden(inner, style))
758
+ out += ' ';
759
+ }
760
+ return out;
761
+ };
762
+ /**
763
+ * The text an element owns, read as a person reads its line: its own text
764
+ * with every inline descendant's text in place, so `<p>Read our <a>privacy
765
+ * policy</a> for details.</p>` reads as the whole sentence while the link
766
+ * stays a node of its own. Block-level descendants are nodes of their own
767
+ * and only separate words. An element with no text of its own owns none: a
768
+ * wrapper of elements leaves them to say what they say (`ownsLine`).
769
+ */
770
+ const lineTextOf = memoized((el) => ownsLine(el) ? lineRunOf(el).replace(/\s+/g, ' ').trim() : '');
771
+ /**
772
+ * True when an element reads its own line: it has text of its own, or it
773
+ * offers an action inside an ancestor's line, where its words are what a
774
+ * person aims at.
775
+ */
776
+ const ownsLine = (el) => directTextOf(el) !== '' || (offersAction(el) && isReadInLine(el));
777
+ /**
778
+ * True when an element with no role still says it can be acted on: it is
779
+ * focusable through `tabindex`, it has a click handler set as an attribute
780
+ * or a property (`onclick`), or it is an `<a>` with no `href`, which an app
781
+ * wires up by script. Such an element stays listed inside a line, so it
782
+ * keeps an id to act on. A handler added with `addEventListener` leaves
783
+ * nothing the page can read.
784
+ */
785
+ const offersAction = memoized((el) => {
786
+ const tabindex = el.getAttribute('tabindex');
787
+ if (tabindex !== null && /^\s*[-+]?\d/.test(tabindex))
788
+ return true;
789
+ if (el.tagName.toLowerCase() === 'a' || el.hasAttribute('onclick'))
790
+ return true;
791
+ return el instanceof HTMLElement && el.onclick !== null;
792
+ });
793
+ /**
794
+ * True when an element's text is already read in an ancestor's line
795
+ * (`lineTextOf`), so the element has nothing to add on text alone. A line
796
+ * longer than the projection's text bound is cut, so its inline
797
+ * descendants stay listed and nothing past the cut is lost.
798
+ */
799
+ const isReadInLine = memoized((el) => {
800
+ const parent = el.assignedSlot ?? parentOrHostOf(el);
801
+ if (parent === null || !flowsInLine(el))
802
+ return false;
803
+ if (!ownsLine(parent))
804
+ return isReadInLine(parent);
805
+ const fits = projection.textLimit === null || lineTextOf(parent).length <= projection.textLimit;
806
+ return fits && !isHidden(parent);
807
+ });
685
808
  const accessibleName = memoized((el) => {
686
809
  // accname reads a labelledby reference (2B) before the element's own aria-label (2C).
687
810
  const referenced = referencedNamesOf(el, nameWalk([], true));
@@ -905,7 +1028,7 @@ export const readSemanticsFunction = (element, options) => {
905
1028
  * edges. The walk goes on through both and only the element itself stays
906
1029
  * unlisted.
907
1030
  */
908
- const hidesSubtree = (el, style) => el.getAttribute('aria-hidden') === 'true' ||
1031
+ const hidesSubtree = (el, style) => (el.getAttribute('aria-hidden') ?? '').toLowerCase() === 'true' ||
909
1032
  isInert(el, style) ||
910
1033
  style === undefined ||
911
1034
  style.display === 'none' ||
@@ -1194,7 +1317,7 @@ export const readSemanticsFunction = (element, options) => {
1194
1317
  if (selectedState === null) {
1195
1318
  const ariaSelected = el.getAttribute('aria-selected');
1196
1319
  if (ariaSelected !== null)
1197
- selectedState = ariaSelected === 'true';
1320
+ selectedState = ariaSelected.toLowerCase() === 'true';
1198
1321
  }
1199
1322
  const disabled = isDisabled(el);
1200
1323
  const ariaExpanded = el.getAttribute('aria-expanded');
@@ -1238,8 +1361,8 @@ export const readSemanticsFunction = (element, options) => {
1238
1361
  let text;
1239
1362
  if (secure)
1240
1363
  text = '';
1241
- else if (projection.directTextOnly)
1242
- text = directTextOf(el);
1364
+ else if (projection.lineText)
1365
+ text = lineTextOf(el);
1243
1366
  else
1244
1367
  text = textOf(el);
1245
1368
  if (projection.textLimit !== null)
@@ -1334,7 +1457,9 @@ export const readSemanticsFunction = (element, options) => {
1334
1457
  return true;
1335
1458
  if (accessibleName(el) !== null)
1336
1459
  return true;
1337
- return directTextOf(el) !== '';
1460
+ if (offersAction(el) && isReadInLine(el))
1461
+ return lineTextOf(el) !== '';
1462
+ return directTextOf(el) !== '' && !isReadInLine(el);
1338
1463
  };
1339
1464
  const walk = (el, parent) => {
1340
1465
  if (truncated)
@@ -7,7 +7,7 @@
7
7
  import { readFile } from 'node:fs/promises';
8
8
  import path from 'node:path';
9
9
  import { ConfigurationError, TestError, validateJsonValue } from 'e2e/engine';
10
- import { KEEP_NAMES_HELPER } from './evaluation.js';
10
+ import { KEEP_NAMES_HELPER, pageFunctionSource } from './evaluation.js';
11
11
  import { message } from './support.js';
12
12
  /** Refuses, at config load, a `web({ initScripts })` value that is not a list of scripts. */
13
13
  export function validateInitScripts(scripts) {
@@ -93,7 +93,7 @@ async function initScriptSource(script, arg, resolvePath) {
93
93
  if (typeof script === 'string')
94
94
  return script;
95
95
  if (typeof script === 'function') {
96
- return `(() => {\n${KEEP_NAMES_HELPER}\n(${script.toString()}\n)(${arg === undefined ? '' : `JSON.parse(${JSON.stringify(JSON.stringify(arg))})`});\n})();`;
96
+ return `(() => {\n${KEEP_NAMES_HELPER}\n(${pageFunctionSource(script)}\n)(${arg === undefined ? '' : `JSON.parse(${JSON.stringify(JSON.stringify(arg))})`});\n})();`;
97
97
  }
98
98
  const file = resolvePath(script.path);
99
99
  try {
@@ -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 {};
@@ -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 {};
@@ -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. Shifted by
209
- // the boundary element's box and clipped to it, every box in the tree is
210
- // in the top-level viewport's CSS pixels, the space the screenshot and a
211
- // point tap share, and a child that overflows its frame cannot claim a
212
- // point over the page around it. The shift is the frame's border box; a
213
- // bordered iframe is off by its border width, which is within a tap
214
- // target. A frame under a CSS transform is not unwound: its boxes are
215
- // where the untransformed frame would put them.
216
- if (embedded !== undefined && raw.rect !== null)
217
- childLists[index].unshift(placeInFrame(embedded, raw.rect));
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 : clipRect(offsetRect(rect, frame), frame);
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
  }
@@ -11,8 +11,8 @@ export function connectionAbort(signal, label) {
11
11
  * fires: its error names what blocked an action, or that the input was
12
12
  * already dispatched, and this lead lets that answer arrive before the
13
13
  * deadline does. The deadline is for the calls Playwright never answers: an
14
- * evaluate, which takes no timeout, and every call while a trace snapshots a
15
- * page whose renderer is stuck in a script. A budget shorter than twice the
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
16
  * lead splits in half.
17
17
  */
18
18
  const PLAYWRIGHT_TIMEOUT_LEAD_MS = 250;
@@ -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;
@@ -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/surface.d.ts CHANGED
@@ -107,8 +107,7 @@ export interface WebOptions {
107
107
  * page, deterministic and agent-driven alike. Injecting headers routes
108
108
  * every request of the attempt, which turns the browser's HTTP cache off
109
109
  * and blocks service workers (a worker's requests bypass routing, so a
110
- * page it controlled would reach the gate bare), and a Playwright trace
111
- * records request headers.
110
+ * page it controlled would reach the gate bare).
112
111
  */
113
112
  readonly headers?: Readonly<Record<string, string>>;
114
113
  /**
@@ -187,6 +186,10 @@ export declare class PlaywrightSurface {
187
186
  private projectRoot;
188
187
  private headed;
189
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;
190
193
  private artifactCounter;
191
194
  /**
192
195
  * Attempt-scoped network routes. Registered on the
@@ -261,8 +264,8 @@ export declare class PlaywrightSurface {
261
264
  * Releases attempt resources, the worker's shared browser process, and a
262
265
  * lease the worker acquired for itself: three independent tasks, in that
263
266
  * order, every one attempted whatever the earlier ones did, and the first
264
- * failure reported once all ran. A billed browser outlives a failed trace
265
- * flush otherwise.
267
+ * failure reported once all ran. A billed browser outlives a failed
268
+ * recording flush otherwise.
266
269
  */
267
270
  dispose(context: EngineCleanupContext): Promise<void>;
268
271
  /** Returns the active attempt, including its connection generation and references. */
@@ -277,6 +280,12 @@ export declare class PlaywrightSurface {
277
280
  projectPath(file: string): string;
278
281
  /** Removes one registered route from the attempt and the current context. */
279
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;
280
289
  requirePage(): Page;
281
290
  requireContext(): BrowserContext;
282
291
  /**
@@ -308,6 +317,10 @@ export declare class PlaywrightSurface {
308
317
  /** Opens one URL the harness resolved; the attempt's page is created on first use. */
309
318
  open(url: string, operation: OperationContext): Promise<void>;
310
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;
311
324
  /** Restarts the document while retaining this attempt's context and storage. */
312
325
  restart(operation: OperationContext): Promise<void>;
313
326
  /** Replaces the context with a clean one and opens its blank page. */
@@ -356,9 +369,12 @@ export declare class PlaywrightSurface {
356
369
  * Checks that every frame selector along the expression matches exactly one
357
370
  * element, each counted inside the frame before it, the way `project` walks
358
371
  * the same chain; a frame-scoped `count` is 0 until that frame's document
359
- * 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.
360
376
  */
361
- private validateFrames;
377
+ private frameBox;
362
378
  /**
363
379
  * Reserves one artifact path under the attempt directory: a per-attempt
364
380
  * counter keeps names unique, the label keeps them readable.
@@ -369,11 +385,7 @@ export declare class PlaywrightSurface {
369
385
  };
370
386
  /** Redacted at the source: secure fields in every frame are masked, as in observation pixels. */
371
387
  screenshot(label: string | undefined, operation: OperationContext): Promise<string>;
372
- /** Starts tracing the current attempt. */
373
- startTrace(operation: OperationContext): Promise<void>;
374
- /** Returns every trace segment finalized by the current attempt. */
375
- stopTrace(operation: OperationContext): Promise<string | readonly string[]>;
376
- /** Starts the attempt's video before a trace chooses its screencast dimensions. */
388
+ /** Starts the attempt's video. */
377
389
  startVideo(operation: OperationContext): Promise<void>;
378
390
  /** Finishes the attempt's recording and returns its finalized segments. */
379
391
  stopVideo(operation: OperationContext): Promise<readonly VideoSegment[]>;