@e2e-dev/web 0.13.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 +2 -1
- package/dist/actions.js +45 -28
- package/dist/app-log.d.ts +20 -0
- package/dist/app-log.js +127 -0
- package/dist/attempt-session.d.ts +2 -17
- package/dist/attempt-session.js +2 -77
- package/dist/browser.js +10 -10
- package/dist/dialogs.js +5 -0
- package/dist/engine.js +0 -2
- package/dist/evaluation.d.ts +2 -0
- package/dist/evaluation.js +31 -0
- package/dist/in-page/read-semantics.js +140 -15
- package/dist/init-scripts.js +2 -2
- 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.js +2 -2
- package/dist/provider-video.d.ts +0 -2
- package/dist/provider-video.js +0 -2
- package/dist/surface.d.ts +23 -11
- package/dist/surface.js +105 -34
- 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 +3 -3
|
@@ -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
|
|
58
|
-
* a bounded attribute projection, hrefs reduced to
|
|
59
|
-
* document named by its title, and no value on a
|
|
60
|
-
* value attribute is an app token its checked
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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.
|
|
1242
|
-
text =
|
|
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
|
-
|
|
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)
|
package/dist/init-scripts.js
CHANGED
|
@@ -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
|
|
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 {};
|
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
|
}
|
package/dist/operation-budget.js
CHANGED
|
@@ -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
|
|
15
|
-
*
|
|
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;
|
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/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)
|
|
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
|
|
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
|
|
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
|
|
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[]>;
|