@e2e-dev/web 0.15.5-nightly-20261104030231 → 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/attempt-session.d.ts +8 -0
- package/dist/attempt-session.js +21 -0
- package/dist/browser-connection.d.ts +2 -3
- package/dist/browser-connection.js +18 -5
- package/dist/browser.d.ts +7 -4
- package/dist/browser.js +65 -53
- 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/frame-document.d.ts +20 -0
- package/dist/frame-document.js +40 -0
- package/dist/in-page/read-semantics.js +3 -0
- package/dist/install.d.ts +2 -2
- package/dist/install.js +5 -5
- package/dist/locators.d.ts +9 -2
- package/dist/locators.js +31 -23
- package/dist/observation-capture.js +3 -3
- package/dist/observation.d.ts +12 -8
- package/dist/observation.js +40 -43
- package/dist/observe.d.ts +3 -2
- package/dist/observe.js +6 -5
- package/dist/read-node.d.ts +5 -0
- package/dist/refs.d.ts +17 -8
- package/dist/refs.js +20 -10
- package/dist/route-options.d.ts +7 -2
- package/dist/route-options.js +33 -9
- package/dist/support.d.ts +16 -3
- package/dist/support.js +31 -0
- package/dist/surface.d.ts +30 -8
- package/dist/surface.js +213 -100
- package/package.json +3 -3
package/dist/evaluation.d.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
ok: false;
|
|
6
|
-
message: string;
|
|
7
|
-
};
|
|
8
|
-
/** The `__name` helper tsx's `keepNames` output calls, declared beside test code serialized into the page. */
|
|
9
|
-
export declare const KEEP_NAMES_HELPER = "const __name = (target, value) => Object.defineProperty(target, 'name', { value, configurable: true });";
|
|
1
|
+
/** Separates exceptions thrown by trusted page code from failures of the evaluation transport. */
|
|
2
|
+
import type { JsonValue } from 'e2e';
|
|
3
|
+
/** The `__name` helper tsx's `keepNames` output calls, declared beside test code serialized into the page; it takes `defineProperty` before that code runs. */
|
|
4
|
+
export declare const KEEP_NAMES_HELPER = "const __name = ((define) => (target, value) => define(target, 'name', { value, configurable: true }))(({}).constructor.defineProperty);";
|
|
10
5
|
/** A function expression for page code, including ordinary and async method shorthand. Never runs it here. */
|
|
11
6
|
export declare function pageFunctionSource(fn: (...args: never[]) => unknown): string;
|
|
7
|
+
/**
|
|
8
|
+
* Builds, in the page, the encoder of an evaluation result into JSON text.
|
|
9
|
+
* Called before the caller's code runs, it takes the intrinsics it needs from
|
|
10
|
+
* literals, so code that deletes or replaces `Array`, `Object`, or `JSON`
|
|
11
|
+
* cannot reach it. Only JSON passes: `null`, booleans, finite numbers,
|
|
12
|
+
* strings, arrays, and plain objects. Anything else throws `{ invalid }` with
|
|
13
|
+
* the reason `validateJsonValue` gives. Embedded into the page as source, so
|
|
14
|
+
* it captures nothing from this module.
|
|
15
|
+
*/
|
|
16
|
+
export declare function pageValueEncoder(): (root: unknown) => string | undefined;
|
|
12
17
|
/**
|
|
13
18
|
* Compiles the page-side error boundary without evaluating the caller's source in this process.
|
|
14
19
|
* The source is one expression: a function it evaluates to is called with the argument, any
|
|
15
|
-
* other value is the result.
|
|
20
|
+
* other value is the result. The boundary resolves to JSON text, a primitive the caller takes
|
|
21
|
+
* back without Playwright serializing it in the page, so a page that deleted or replaced a
|
|
22
|
+
* global the serializer needs still returns its result; `decodeEvaluation` reads it.
|
|
16
23
|
*
|
|
17
24
|
* A function serialized with `toString()` carries whatever the loader compiled
|
|
18
25
|
* it to. e2e before 0.17 loaded tests through tsx, whose esbuild `keepNames`
|
|
@@ -20,5 +27,6 @@ export declare function pageFunctionSource(fn: (...args: never[]) => unknown): s
|
|
|
20
27
|
* helper; the page has no such helper, so the same one is declared next to
|
|
21
28
|
* the inlined source for as long as this engine supports those runners.
|
|
22
29
|
*/
|
|
23
|
-
export declare function compileEvaluation(source: string, hasArgument: boolean): (arg: unknown) => Promise<
|
|
24
|
-
|
|
30
|
+
export declare function compileEvaluation(source: string, hasArgument: boolean): (arg: unknown) => Promise<string>;
|
|
31
|
+
/** The result of the boundary `compileEvaluation` compiled, from the JSON text it resolved to; throws what the page threw. */
|
|
32
|
+
export declare function decodeEvaluation(text: string): JsonValue | undefined;
|
package/dist/evaluation.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
/** Separates exceptions thrown by trusted page code from failures of the evaluation transport. */
|
|
2
1
|
import { TestError } from 'e2e/engine';
|
|
3
2
|
import { message } from './support.js';
|
|
4
|
-
/** The `__name` helper tsx's `keepNames` output calls, declared beside test code serialized into the page. */
|
|
5
|
-
export const KEEP_NAMES_HELPER = "const __name = (target, value) =>
|
|
3
|
+
/** The `__name` helper tsx's `keepNames` output calls, declared beside test code serialized into the page; it takes `defineProperty` before that code runs. */
|
|
4
|
+
export const KEEP_NAMES_HELPER = "const __name = ((define) => (target, value) => define(target, 'name', { value, configurable: true }))(({}).constructor.defineProperty);";
|
|
6
5
|
/** A function expression for page code, including ordinary and async method shorthand. Never runs it here. */
|
|
7
6
|
export function pageFunctionSource(fn) {
|
|
8
7
|
const source = fn.toString().trim();
|
|
@@ -34,10 +33,82 @@ export function pageFunctionSource(fn) {
|
|
|
34
33
|
}
|
|
35
34
|
}
|
|
36
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Builds, in the page, the encoder of an evaluation result into JSON text.
|
|
38
|
+
* Called before the caller's code runs, it takes the intrinsics it needs from
|
|
39
|
+
* literals, so code that deletes or replaces `Array`, `Object`, or `JSON`
|
|
40
|
+
* cannot reach it. Only JSON passes: `null`, booleans, finite numbers,
|
|
41
|
+
* strings, arrays, and plain objects. Anything else throws `{ invalid }` with
|
|
42
|
+
* the reason `validateJsonValue` gives. Embedded into the page as source, so
|
|
43
|
+
* it captures nothing from this module.
|
|
44
|
+
*/
|
|
45
|
+
export function pageValueEncoder() {
|
|
46
|
+
const { getPrototypeOf, keys } = ({}).constructor;
|
|
47
|
+
const objectPrototype = getPrototypeOf({});
|
|
48
|
+
const { isArray } = [].constructor;
|
|
49
|
+
const charCodeAt = ''.charCodeAt;
|
|
50
|
+
const hex = '0123456789abcdef';
|
|
51
|
+
const invalid = (reason) => ({ invalid: reason });
|
|
52
|
+
const quote = (text) => {
|
|
53
|
+
let out = '"';
|
|
54
|
+
for (let i = 0; i < text.length; i++) {
|
|
55
|
+
const code = charCodeAt.call(text, i);
|
|
56
|
+
const escape = code === 34 || code === 92 || code < 32 || code === 0x2028 || code === 0x2029 || (code >= 0xd800 && code <= 0xdfff);
|
|
57
|
+
out += escape ? `\\u${hex[code >> 12]}${hex[(code >> 8) & 15]}${hex[(code >> 4) & 15]}${hex[code & 15]}` : text[i];
|
|
58
|
+
}
|
|
59
|
+
return `${out}"`;
|
|
60
|
+
};
|
|
61
|
+
const ancestors = [];
|
|
62
|
+
const encode = (value) => {
|
|
63
|
+
if (value === null)
|
|
64
|
+
return 'null';
|
|
65
|
+
switch (typeof value) {
|
|
66
|
+
case 'string':
|
|
67
|
+
return quote(value);
|
|
68
|
+
case 'boolean':
|
|
69
|
+
return value ? 'true' : 'false';
|
|
70
|
+
case 'number':
|
|
71
|
+
if (value !== value || value === Infinity || value === -Infinity)
|
|
72
|
+
throw invalid('contains a non-finite number');
|
|
73
|
+
return `${value}`;
|
|
74
|
+
case 'object':
|
|
75
|
+
break;
|
|
76
|
+
default:
|
|
77
|
+
throw invalid(`must be JSON-safe, found ${typeof value}`);
|
|
78
|
+
}
|
|
79
|
+
const array = isArray(value);
|
|
80
|
+
const prototype = getPrototypeOf(value);
|
|
81
|
+
if (!array && prototype !== objectPrototype && prototype !== null)
|
|
82
|
+
throw invalid('must be JSON-safe');
|
|
83
|
+
for (let i = 0; i < ancestors.length; i++) {
|
|
84
|
+
if (ancestors[i] === value)
|
|
85
|
+
throw invalid('contains a cycle');
|
|
86
|
+
}
|
|
87
|
+
ancestors[ancestors.length] = value;
|
|
88
|
+
let text = '';
|
|
89
|
+
if (array) {
|
|
90
|
+
for (let i = 0; i < value.length; i++)
|
|
91
|
+
text += `${i === 0 ? '' : ','}${encode(value[i])}`;
|
|
92
|
+
text = `[${text}]`;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const record = value;
|
|
96
|
+
const names = keys(record);
|
|
97
|
+
for (let i = 0; i < names.length; i++)
|
|
98
|
+
text += `${i === 0 ? '' : ','}${quote(names[i])}:${encode(record[names[i]])}`;
|
|
99
|
+
text = `{${text}}`;
|
|
100
|
+
}
|
|
101
|
+
ancestors.length -= 1;
|
|
102
|
+
return text;
|
|
103
|
+
};
|
|
104
|
+
return (root) => (root === undefined ? undefined : encode(root));
|
|
105
|
+
}
|
|
37
106
|
/**
|
|
38
107
|
* Compiles the page-side error boundary without evaluating the caller's source in this process.
|
|
39
108
|
* The source is one expression: a function it evaluates to is called with the argument, any
|
|
40
|
-
* other value is the result.
|
|
109
|
+
* other value is the result. The boundary resolves to JSON text, a primitive the caller takes
|
|
110
|
+
* back without Playwright serializing it in the page, so a page that deleted or replaced a
|
|
111
|
+
* global the serializer needs still returns its result; `decodeEvaluation` reads it.
|
|
41
112
|
*
|
|
42
113
|
* A function serialized with `toString()` carries whatever the loader compiled
|
|
43
114
|
* it to. e2e before 0.17 loaded tests through tsx, whose esbuild `keepNames`
|
|
@@ -48,23 +119,49 @@ export function pageFunctionSource(fn) {
|
|
|
48
119
|
export function compileEvaluation(source, hasArgument) {
|
|
49
120
|
try {
|
|
50
121
|
return new Function('arg', `
|
|
51
|
-
|
|
52
|
-
|
|
122
|
+
${KEEP_NAMES_HELPER}
|
|
123
|
+
return (async (encode, run) => {
|
|
124
|
+
const report = (ok, key, text) => \`{"ok":\${ok}\${text === undefined ? '' : \`,"\${key}":\${text}\`}}\`;
|
|
125
|
+
let value;
|
|
53
126
|
try {
|
|
54
|
-
|
|
127
|
+
value = await run();
|
|
55
128
|
} catch (cause) {
|
|
56
129
|
let message;
|
|
57
130
|
try {
|
|
58
|
-
message = typeof cause?.message === 'string' ? cause.message :
|
|
131
|
+
message = typeof cause?.message === 'string' ? cause.message : \`\${cause}\`;
|
|
59
132
|
} catch {
|
|
60
133
|
message = 'Page evaluation threw an unprintable value';
|
|
61
134
|
}
|
|
62
|
-
return
|
|
135
|
+
return report(false, 'message', encode(message));
|
|
63
136
|
}
|
|
64
|
-
|
|
137
|
+
try {
|
|
138
|
+
return report(true, 'value', encode(value));
|
|
139
|
+
} catch (cause) {
|
|
140
|
+
if (cause !== null && typeof cause === 'object' && typeof cause.invalid === 'string') {
|
|
141
|
+
return report(false, 'invalid', encode(cause.invalid));
|
|
142
|
+
}
|
|
143
|
+
return '{"ok":false,"message":"evaluate result cannot be read"}';
|
|
144
|
+
}
|
|
145
|
+
})((${pageValueEncoder.toString()})(), async () => ((result) => typeof result === 'function' ? result(${hasArgument ? 'arg' : ''}) : result)((${source}\n)));
|
|
65
146
|
`);
|
|
66
147
|
}
|
|
67
148
|
catch (cause) {
|
|
68
149
|
throw new TestError('EVALUATE_FAILED', message(cause), { cause });
|
|
69
150
|
}
|
|
70
151
|
}
|
|
152
|
+
/** The result of the boundary `compileEvaluation` compiled, from the JSON text it resolved to; throws what the page threw. */
|
|
153
|
+
export function decodeEvaluation(text) {
|
|
154
|
+
let report;
|
|
155
|
+
try {
|
|
156
|
+
// oxlint-disable-next-line e2e/no-unchecked-json -- the text is what compileEvaluation's own boundary wrote
|
|
157
|
+
report = JSON.parse(text);
|
|
158
|
+
}
|
|
159
|
+
catch (cause) {
|
|
160
|
+
throw new TestError('EVALUATE_FAILED', 'page evaluation returned an unreadable result', { cause });
|
|
161
|
+
}
|
|
162
|
+
if (report.ok)
|
|
163
|
+
return report.value;
|
|
164
|
+
if ('invalid' in report)
|
|
165
|
+
throw new TestError('INVALID_ARGUMENT', `evaluate result ${report.invalid}`);
|
|
166
|
+
throw new TestError('EVALUATE_FAILED', report.message);
|
|
167
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Which frames hold a document script can read. */
|
|
2
|
+
import type { ElementHandle, Frame } from 'playwright-core';
|
|
3
|
+
/**
|
|
4
|
+
* True when Playwright can evaluate in the frame without waiting on a
|
|
5
|
+
* navigation.
|
|
6
|
+
*
|
|
7
|
+
* A child frame that has not committed a navigation (its URL is empty, as
|
|
8
|
+
* while its first request is held) shows its initial empty document.
|
|
9
|
+
* Chromium creates no script context for that document until script in its
|
|
10
|
+
* parent touches it, and Playwright waits for the navigation to commit
|
|
11
|
+
* instead. Reading `contentDocument` from the parent is that touch, so the
|
|
12
|
+
* frame is readable from then on. A parent that cannot read it (a sandboxed
|
|
13
|
+
* frame) means no script could have written into it either: it shows
|
|
14
|
+
* nothing. When the check itself fails, the frame counts as readable, so the
|
|
15
|
+
* caller's own read of it decides. `owner` is the frame's element when the
|
|
16
|
+
* caller already holds it.
|
|
17
|
+
*/
|
|
18
|
+
export declare function hasReadableDocument(frame: Frame, owner?: ElementHandle): Promise<boolean>;
|
|
19
|
+
/** The page's frames whose documents script can read; see `hasReadableDocument`. */
|
|
20
|
+
export declare function readableFrames(frames: readonly Frame[]): Promise<Frame[]>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Which frames hold a document script can read. */
|
|
2
|
+
/**
|
|
3
|
+
* True when Playwright can evaluate in the frame without waiting on a
|
|
4
|
+
* navigation.
|
|
5
|
+
*
|
|
6
|
+
* A child frame that has not committed a navigation (its URL is empty, as
|
|
7
|
+
* while its first request is held) shows its initial empty document.
|
|
8
|
+
* Chromium creates no script context for that document until script in its
|
|
9
|
+
* parent touches it, and Playwright waits for the navigation to commit
|
|
10
|
+
* instead. Reading `contentDocument` from the parent is that touch, so the
|
|
11
|
+
* frame is readable from then on. A parent that cannot read it (a sandboxed
|
|
12
|
+
* frame) means no script could have written into it either: it shows
|
|
13
|
+
* nothing. When the check itself fails, the frame counts as readable, so the
|
|
14
|
+
* caller's own read of it decides. `owner` is the frame's element when the
|
|
15
|
+
* caller already holds it.
|
|
16
|
+
*/
|
|
17
|
+
export async function hasReadableDocument(frame, owner) {
|
|
18
|
+
if (frame.url() !== '' || frame.parentFrame() === null)
|
|
19
|
+
return true;
|
|
20
|
+
if (owner !== undefined)
|
|
21
|
+
return parentReadsDocument(owner);
|
|
22
|
+
const fetched = await frame.frameElement().catch(() => null);
|
|
23
|
+
if (fetched === null)
|
|
24
|
+
return true;
|
|
25
|
+
try {
|
|
26
|
+
return await parentReadsDocument(fetched);
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await fetched.dispose().catch(() => undefined);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** The page's frames whose documents script can read; see `hasReadableDocument`. */
|
|
33
|
+
export async function readableFrames(frames) {
|
|
34
|
+
const readable = await Promise.all(frames.map((frame) => hasReadableDocument(frame)));
|
|
35
|
+
return frames.filter((_, index) => readable[index]);
|
|
36
|
+
}
|
|
37
|
+
/** Reads the frame's document from its parent, which gives it a script context; true when that read fails. */
|
|
38
|
+
function parentReadsDocument(owner) {
|
|
39
|
+
return owner.evaluate((element) => 'contentDocument' in element && element.contentDocument !== null).catch(() => true);
|
|
40
|
+
}
|
|
@@ -1460,6 +1460,8 @@ export const readSemanticsFunction = (element, options) => {
|
|
|
1460
1460
|
// no rect, as Playwright's `boundingBox()` answers null; a box with no
|
|
1461
1461
|
// size, or one `visibility: hidden` keeps, is still a rect.
|
|
1462
1462
|
const rect = el.getClientRects().length === 0 ? null : el.getBoundingClientRect();
|
|
1463
|
+
const borderLeft = Number.parseFloat(style?.borderLeftWidth ?? '') || 0;
|
|
1464
|
+
const borderTop = Number.parseFloat(style?.borderTopWidth ?? '') || 0;
|
|
1463
1465
|
const role = isDocumentRoot ? 'document' : roleOf(el, tag);
|
|
1464
1466
|
let level = null;
|
|
1465
1467
|
if (role === 'heading') {
|
|
@@ -1494,6 +1496,7 @@ export const readSemanticsFunction = (element, options) => {
|
|
|
1494
1496
|
attributes,
|
|
1495
1497
|
testId: el.getAttribute(options.testIdAttribute),
|
|
1496
1498
|
rect: rect === null ? null : { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
1499
|
+
origin: rect === null || (borderLeft === 0 && borderTop === 0) ? null : { x: rect.x + borderLeft, y: rect.y + borderTop },
|
|
1497
1500
|
};
|
|
1498
1501
|
};
|
|
1499
1502
|
// The conditional return type resolves per call site; inside the body the
|
package/dist/install.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export declare function runPlaywrightCli(args: readonly string[]): Promise<numbe
|
|
|
13
13
|
* disk, a `warn` when the first run will download it, or none when `env`
|
|
14
14
|
* points at another browser cache than this process loaded.
|
|
15
15
|
*/
|
|
16
|
-
export declare function browserCheck(name: BrowserName, env: NodeJS.ProcessEnv, headed: boolean, isInstalled?: (name: BrowserName) => boolean | undefined): EngineDoctorCheck[]
|
|
16
|
+
export declare function browserCheck(name: BrowserName, env: NodeJS.ProcessEnv, headed: boolean, isInstalled?: (name: BrowserName) => boolean | undefined | Promise<boolean | undefined>): Promise<EngineDoctorCheck[]>;
|
|
17
17
|
/** What an installer receives besides the browser names. */
|
|
18
18
|
export interface InstallContext {
|
|
19
19
|
/** Receives every line the installer prints: the download URL, progress, warnings. */
|
|
@@ -36,7 +36,7 @@ export interface EnsureBrowsersOptions {
|
|
|
36
36
|
* Injected detection for tests; defaults to an executable existence check.
|
|
37
37
|
* Undefined means "cannot tell here": the installer runs and decides.
|
|
38
38
|
*/
|
|
39
|
-
readonly isInstalled?: (name: BrowserName) => boolean | undefined
|
|
39
|
+
readonly isInstalled?: (name: BrowserName) => boolean | undefined | Promise<boolean | undefined>;
|
|
40
40
|
/** Injected installer for tests; defaults to spawning the Playwright CLI. */
|
|
41
41
|
readonly install?: (names: readonly BrowserName[], context: InstallContext) => Promise<void>;
|
|
42
42
|
/** Aborts a download in progress; the run's interrupt owns it. */
|
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;
|
package/dist/locators.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export type PostStep = {
|
|
|
17
17
|
readonly kind: 'filter';
|
|
18
18
|
readonly options: PwFilterOptions;
|
|
19
19
|
};
|
|
20
|
+
/** What a projection reads besides the expression. */
|
|
21
|
+
export interface ProjectionOptions {
|
|
22
|
+
/** The attribute a `testId` query reads. */
|
|
23
|
+
readonly testIdAttribute: string;
|
|
24
|
+
/** True when a frame along the expression's frame chain is hidden, so every node in it is. */
|
|
25
|
+
readonly frameHidden: boolean;
|
|
26
|
+
}
|
|
20
27
|
export interface ProjectedLocator {
|
|
21
28
|
/**
|
|
22
29
|
* The Playwright locator to resolve. For a display-value query this is the
|
|
@@ -61,8 +68,8 @@ export interface ProjectedLocator {
|
|
|
61
68
|
*/
|
|
62
69
|
readonly visible: boolean;
|
|
63
70
|
}
|
|
64
|
-
/** Projects an expression onto the page
|
|
65
|
-
export declare function projectExpression(page: Page, expression: LocatorExpression,
|
|
71
|
+
/** Projects an expression onto the page. */
|
|
72
|
+
export declare function projectExpression(page: Page, expression: LocatorExpression, options: ProjectionOptions): ProjectedLocator;
|
|
66
73
|
/**
|
|
67
74
|
* Applies post steps, innermost first, to an ordered list of matches. `nth`
|
|
68
75
|
* past the end selects nothing, matching Playwright's `nth` on a locator with
|
package/dist/locators.js
CHANGED
|
@@ -152,11 +152,19 @@ function queryToPw(scope, query, testIdAttribute) {
|
|
|
152
152
|
* never selects or retains a node that state calls hidden, and the surface's
|
|
153
153
|
* hold of a terminal query to the batch-read state agrees with it.
|
|
154
154
|
*/
|
|
155
|
-
function visibleQueryToPw(scope, query,
|
|
156
|
-
return narrowedToVisible(queryToPw(scope, query, testIdAttribute), query);
|
|
155
|
+
function visibleQueryToPw(scope, query, options) {
|
|
156
|
+
return narrowedToVisible(queryToPw(scope, query, options.testIdAttribute), query, options.frameHidden);
|
|
157
157
|
}
|
|
158
|
-
/**
|
|
159
|
-
|
|
158
|
+
/**
|
|
159
|
+
* Matches no element. Inside a hidden frame every node reads as hidden, which
|
|
160
|
+
* the frame's own document cannot see, so a query that never matches a
|
|
161
|
+
* hidden node, a `visible` or a role query, narrows to it there.
|
|
162
|
+
*/
|
|
163
|
+
const NOTHING_SELECTOR = 'css=:not(*)';
|
|
164
|
+
/** The visibility narrowing above, on a locator already built for `query`; identity unless the query keeps shown nodes alone. */
|
|
165
|
+
function narrowedToVisible(located, query, frameHidden) {
|
|
166
|
+
if (frameHidden && (query.visible === true || query.kind === 'role'))
|
|
167
|
+
return located.locator(NOTHING_SELECTOR);
|
|
160
168
|
if (query.visible !== true)
|
|
161
169
|
return located;
|
|
162
170
|
return located.locator(SHOWN_SELECTOR);
|
|
@@ -192,20 +200,20 @@ const DISPLAY_VALUE_COMPOSITION_MESSAGE = 'displayValue queries cannot scope chi
|
|
|
192
200
|
* unsupported: a display-value query as the scope of a child query, and as a
|
|
193
201
|
* `has` filter.
|
|
194
202
|
*/
|
|
195
|
-
function project(scope, expression,
|
|
203
|
+
function project(scope, expression, options) {
|
|
196
204
|
switch (expression.kind) {
|
|
197
205
|
case 'union': {
|
|
198
|
-
const left = requireComposable(project(scope, expression.left,
|
|
199
|
-
const right = requireComposable(project(scope, expression.right,
|
|
206
|
+
const left = requireComposable(project(scope, expression.left, options));
|
|
207
|
+
const right = requireComposable(project(scope, expression.right, options));
|
|
200
208
|
return plain(left.or(right));
|
|
201
209
|
}
|
|
202
210
|
case 'query': {
|
|
203
211
|
const inner = expression.scope === undefined
|
|
204
212
|
? scope
|
|
205
|
-
: requireComposable(project(scope, expression.scope,
|
|
213
|
+
: requireComposable(project(scope, expression.scope, options));
|
|
206
214
|
const { query } = expression;
|
|
207
215
|
const exactLabel = query.kind === 'label' && query.value.kind === 'string' && query.value.exact ? query.value : null;
|
|
208
|
-
const locator = visibleQueryToPw(inner, query,
|
|
216
|
+
const locator = visibleQueryToPw(inner, query, options);
|
|
209
217
|
return {
|
|
210
218
|
locator,
|
|
211
219
|
displayValue: query.kind === 'displayValue' ? query.value : null,
|
|
@@ -216,30 +224,30 @@ function project(scope, expression, testIdAttribute) {
|
|
|
216
224
|
};
|
|
217
225
|
}
|
|
218
226
|
case 'filter': {
|
|
219
|
-
const source = project(scope, expression.source,
|
|
220
|
-
const
|
|
227
|
+
const source = project(scope, expression.source, options);
|
|
228
|
+
const filter = {};
|
|
221
229
|
if (expression.hasText !== undefined)
|
|
222
|
-
|
|
230
|
+
filter.hasText = patternToPw(expression.hasText);
|
|
223
231
|
if (expression.hasNotText !== undefined)
|
|
224
|
-
|
|
232
|
+
filter.hasNotText = patternToPw(expression.hasNotText);
|
|
225
233
|
if (expression.has !== undefined) {
|
|
226
|
-
|
|
234
|
+
filter.has = requireComposable(project(scope, expression.has, options));
|
|
227
235
|
}
|
|
228
236
|
if (expression.hasNot !== undefined) {
|
|
229
|
-
|
|
237
|
+
filter.hasNot = requireComposable(project(scope, expression.hasNot, options));
|
|
230
238
|
}
|
|
231
239
|
if (source.steps.length > 0) {
|
|
232
|
-
return { ...source, steps: [...source.steps, { kind: 'filter', options }] };
|
|
240
|
+
return { ...source, steps: [...source.steps, { kind: 'filter', options: filter }] };
|
|
233
241
|
}
|
|
234
242
|
return {
|
|
235
243
|
...source,
|
|
236
|
-
locator: source.locator.filter(
|
|
237
|
-
composable: source.composable === null ? null : source.composable.filter(
|
|
244
|
+
locator: source.locator.filter(filter),
|
|
245
|
+
composable: source.composable === null ? null : source.composable.filter(filter),
|
|
238
246
|
steps: [],
|
|
239
247
|
};
|
|
240
248
|
}
|
|
241
249
|
case 'index': {
|
|
242
|
-
const source = project(scope, expression.source,
|
|
250
|
+
const source = project(scope, expression.source, options);
|
|
243
251
|
if (source.displayValue !== null || source.name !== null) {
|
|
244
252
|
return {
|
|
245
253
|
...source,
|
|
@@ -252,12 +260,12 @@ function project(scope, expression, testIdAttribute) {
|
|
|
252
260
|
case 'selector':
|
|
253
261
|
return plain(scope.locator(expression.selector));
|
|
254
262
|
case 'frame':
|
|
255
|
-
return project(scope.frameLocator(expression.selector), expression.source,
|
|
263
|
+
return project(scope.frameLocator(expression.selector), expression.source, options);
|
|
256
264
|
}
|
|
257
265
|
}
|
|
258
|
-
/** Projects an expression onto the page
|
|
259
|
-
export function projectExpression(page, expression,
|
|
260
|
-
return project(page, expression,
|
|
266
|
+
/** Projects an expression onto the page. */
|
|
267
|
+
export function projectExpression(page, expression, options) {
|
|
268
|
+
return project(page, expression, options);
|
|
261
269
|
}
|
|
262
270
|
/**
|
|
263
271
|
* Returns the locator of a projection that must participate in Playwright's
|
|
@@ -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 {};
|