@e2e-dev/web 0.11.1 → 0.12.0
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 +15 -9
- package/dist/.build.json +1 -1
- package/dist/actions.d.ts +1 -2
- package/dist/actions.js +54 -6
- package/dist/attempt-session.d.ts +1 -2
- package/dist/attempt-session.js +0 -1
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +3 -0
- package/dist/browser-connection.d.ts +7 -4
- package/dist/browser-connection.js +7 -2
- package/dist/browser.d.ts +43 -7
- package/dist/browser.js +93 -36
- package/dist/capture-scope.d.ts +1 -2
- package/dist/capture-scope.js +0 -1
- package/dist/cdp-recovery.d.ts +1 -2
- package/dist/cdp-recovery.js +0 -1
- package/dist/cdp-selectors.d.ts +1 -2
- package/dist/cdp-selectors.js +0 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +42 -0
- package/dist/closed-shadow.d.ts +0 -1
- package/dist/closed-shadow.js +0 -1
- package/dist/dialogs.d.ts +1 -2
- package/dist/dialogs.js +0 -1
- package/dist/downloads.d.ts +1 -2
- package/dist/downloads.js +0 -1
- package/dist/engine.d.ts +1 -2
- package/dist/engine.js +74 -17
- package/dist/evaluation.d.ts +2 -1
- package/dist/evaluation.js +3 -2
- package/dist/in-page/read-semantics.d.ts +4 -6
- package/dist/in-page/read-semantics.js +184 -54
- package/dist/index.d.ts +2 -2
- package/dist/index.js +0 -1
- package/dist/init-scripts.d.ts +30 -0
- package/dist/init-scripts.js +105 -0
- package/dist/install.d.ts +8 -2
- package/dist/install.js +35 -9
- package/dist/label-selector.d.ts +0 -1
- package/dist/label-selector.js +0 -1
- package/dist/locators.d.ts +1 -2
- package/dist/locators.js +30 -19
- package/dist/observation-capture.d.ts +1 -2
- package/dist/observation-capture.js +0 -1
- package/dist/observation.d.ts +1 -2
- package/dist/observation.js +3 -4
- package/dist/observe.d.ts +1 -2
- package/dist/observe.js +0 -1
- package/dist/operation-budget.d.ts +0 -1
- package/dist/operation-budget.js +0 -1
- package/dist/protected-app.d.ts +20 -5
- package/dist/protected-app.js +36 -6
- package/dist/provider-video.d.ts +1 -2
- package/dist/provider-video.js +0 -1
- package/dist/provider.d.ts +0 -1
- package/dist/provider.js +0 -1
- package/dist/read-node.d.ts +16 -44
- package/dist/read-node.js +7 -22
- package/dist/read-selector.d.ts +51 -0
- package/dist/read-selector.js +69 -0
- package/dist/refs.d.ts +0 -1
- package/dist/refs.js +0 -1
- package/dist/route-options.d.ts +44 -0
- package/dist/route-options.js +125 -0
- package/dist/route-pattern.d.ts +0 -1
- package/dist/route-pattern.js +0 -1
- package/dist/selector-engines.d.ts +0 -1
- package/dist/selector-engines.js +4 -1
- package/dist/shown-selector.d.ts +20 -0
- package/dist/shown-selector.js +27 -0
- package/dist/support.d.ts +4 -3
- package/dist/support.js +1 -2
- package/dist/surface.d.ts +40 -4
- package/dist/surface.js +62 -28
- package/dist/video.d.ts +1 -2
- package/dist/video.js +0 -1
- package/package.json +9 -5
- package/dist/actions.d.ts.map +0 -1
- package/dist/actions.js.map +0 -1
- package/dist/attempt-session.d.ts.map +0 -1
- package/dist/attempt-session.js.map +0 -1
- package/dist/browser-connection.d.ts.map +0 -1
- package/dist/browser-connection.js.map +0 -1
- package/dist/browser.d.ts.map +0 -1
- package/dist/browser.js.map +0 -1
- package/dist/capture-scope.d.ts.map +0 -1
- package/dist/capture-scope.js.map +0 -1
- package/dist/cdp-recovery.d.ts.map +0 -1
- package/dist/cdp-recovery.js.map +0 -1
- package/dist/cdp-selectors.d.ts.map +0 -1
- package/dist/cdp-selectors.js.map +0 -1
- package/dist/closed-shadow.d.ts.map +0 -1
- package/dist/closed-shadow.js.map +0 -1
- package/dist/dialogs.d.ts.map +0 -1
- package/dist/dialogs.js.map +0 -1
- package/dist/downloads.d.ts.map +0 -1
- package/dist/downloads.js.map +0 -1
- package/dist/engine.d.ts.map +0 -1
- package/dist/engine.js.map +0 -1
- package/dist/evaluation.d.ts.map +0 -1
- package/dist/evaluation.js.map +0 -1
- package/dist/in-page/read-semantics.d.ts.map +0 -1
- package/dist/in-page/read-semantics.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/install.d.ts.map +0 -1
- package/dist/install.js.map +0 -1
- package/dist/label-selector.d.ts.map +0 -1
- package/dist/label-selector.js.map +0 -1
- package/dist/locators.d.ts.map +0 -1
- package/dist/locators.js.map +0 -1
- package/dist/observation-capture.d.ts.map +0 -1
- package/dist/observation-capture.js.map +0 -1
- package/dist/observation.d.ts.map +0 -1
- package/dist/observation.js.map +0 -1
- package/dist/observe.d.ts.map +0 -1
- package/dist/observe.js.map +0 -1
- package/dist/operation-budget.d.ts.map +0 -1
- package/dist/operation-budget.js.map +0 -1
- package/dist/protected-app.d.ts.map +0 -1
- package/dist/protected-app.js.map +0 -1
- package/dist/provider-video.d.ts.map +0 -1
- package/dist/provider-video.js.map +0 -1
- package/dist/provider.d.ts.map +0 -1
- package/dist/provider.js.map +0 -1
- package/dist/read-node.d.ts.map +0 -1
- package/dist/read-node.js.map +0 -1
- package/dist/refs.d.ts.map +0 -1
- package/dist/refs.js.map +0 -1
- package/dist/route-pattern.d.ts.map +0 -1
- package/dist/route-pattern.js.map +0 -1
- package/dist/selector-engines.d.ts.map +0 -1
- package/dist/selector-engines.js.map +0 -1
- package/dist/support.d.ts.map +0 -1
- package/dist/support.js.map +0 -1
- package/dist/surface.d.ts.map +0 -1
- package/dist/surface.js.map +0 -1
- package/dist/video.d.ts.map +0 -1
- package/dist/video.js.map +0 -1
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The selector engine that reads a locate's matches in the task that finds
|
|
3
|
+
* them. Appended to a projected locator (`<locator> >> e2e-read=<json>`), it
|
|
4
|
+
* maps each match to itself and records the reader's semantics for it on the
|
|
5
|
+
* element, so a page that replaces the match a frame later can never detach
|
|
6
|
+
* it between the query and the read: both happen before the page runs again.
|
|
7
|
+
* A later protocol call takes those reads back from the very elements the
|
|
8
|
+
* query returned, as values or as handles to pin.
|
|
9
|
+
*/
|
|
10
|
+
import type { RawNodeData } from './read-node.ts';
|
|
11
|
+
/** Playwright selector engine name; `e2e-read=<json>` reads every match under one locate's token. */
|
|
12
|
+
export declare const READ_SELECTOR_ENGINE = "e2e-read";
|
|
13
|
+
/** What the selector body carries into the page: the locate's token and the reader's options. */
|
|
14
|
+
interface ReadSelectorBody {
|
|
15
|
+
readonly token: string;
|
|
16
|
+
readonly testIdAttribute: string;
|
|
17
|
+
readonly secureFieldSelector: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The selector one locate appends to its projected locator, under a fresh
|
|
21
|
+
* token so concurrent locates of one element each take back their own read.
|
|
22
|
+
* The body is JSON, kept whole by Playwright's selector parser like the
|
|
23
|
+
* `e2e-label` body.
|
|
24
|
+
*/
|
|
25
|
+
export declare function readSelector(options: Omit<ReadSelectorBody, 'token'>): {
|
|
26
|
+
readonly selector: string;
|
|
27
|
+
readonly token: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The engine behind `e2e-read=<json>`: each query root is a match of the
|
|
31
|
+
* locator before it, returned as is after the reader's node read of it is
|
|
32
|
+
* recorded under the body's token. The reader is embedded as source because
|
|
33
|
+
* a selector engine is one self-contained page function. It runs in the main
|
|
34
|
+
* world (not as a content script) so the take function, which Playwright
|
|
35
|
+
* evaluates there, finds what it recorded.
|
|
36
|
+
*/
|
|
37
|
+
export declare const READ_SELECTOR_ENGINE_SOURCE: string;
|
|
38
|
+
/**
|
|
39
|
+
* Takes back, in order, the reads the engine recorded under `token` and
|
|
40
|
+
* deletes them; `null` where none was recorded, which is a match the engine
|
|
41
|
+
* never saw: a `*` capture earlier in the selector returns an element the
|
|
42
|
+
* chain only passed through. `locator.evaluateAll` calls it with the matched
|
|
43
|
+
* elements. A caller pinning handles evaluates it on the first handle with
|
|
44
|
+
* every handle in `elements`, so it runs in the frame the handles belong to;
|
|
45
|
+
* `page.evaluate` would reject handles taken inside an iframe.
|
|
46
|
+
*/
|
|
47
|
+
export declare const takeReadsFunction: (subject: Element | Element[], arg: {
|
|
48
|
+
readonly token: string;
|
|
49
|
+
readonly elements?: Element[];
|
|
50
|
+
}) => (RawNodeData | null)[];
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The selector engine that reads a locate's matches in the task that finds
|
|
3
|
+
* them. Appended to a projected locator (`<locator> >> e2e-read=<json>`), it
|
|
4
|
+
* maps each match to itself and records the reader's semantics for it on the
|
|
5
|
+
* element, so a page that replaces the match a frame later can never detach
|
|
6
|
+
* it between the query and the read: both happen before the page runs again.
|
|
7
|
+
* A later protocol call takes those reads back from the very elements the
|
|
8
|
+
* query returned, as values or as handles to pin.
|
|
9
|
+
*/
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import { readSemanticsFunction } from './in-page/read-semantics.js';
|
|
12
|
+
/** Playwright selector engine name; `e2e-read=<json>` reads every match under one locate's token. */
|
|
13
|
+
export const READ_SELECTOR_ENGINE = 'e2e-read';
|
|
14
|
+
/**
|
|
15
|
+
* Where the engine records reads on an element: a `Map` from locate token to
|
|
16
|
+
* read under a registry symbol, like the observation's ref stamp: no
|
|
17
|
+
* attribute, nothing a key listing shows, not removable by application code,
|
|
18
|
+
* and gone with the element. The take function below repeats the literal, since a page function
|
|
19
|
+
* cannot import it.
|
|
20
|
+
*/
|
|
21
|
+
const READS_KEY = 'e2e.locate.reads';
|
|
22
|
+
/**
|
|
23
|
+
* The selector one locate appends to its projected locator, under a fresh
|
|
24
|
+
* token so concurrent locates of one element each take back their own read.
|
|
25
|
+
* The body is JSON, kept whole by Playwright's selector parser like the
|
|
26
|
+
* `e2e-label` body.
|
|
27
|
+
*/
|
|
28
|
+
export function readSelector(options) {
|
|
29
|
+
const token = randomUUID();
|
|
30
|
+
return { selector: `${READ_SELECTOR_ENGINE}=${JSON.stringify({ ...options, token })}`, token };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The engine behind `e2e-read=<json>`: each query root is a match of the
|
|
34
|
+
* locator before it, returned as is after the reader's node read of it is
|
|
35
|
+
* recorded under the body's token. The reader is embedded as source because
|
|
36
|
+
* a selector engine is one self-contained page function. It runs in the main
|
|
37
|
+
* world (not as a content script) so the take function, which Playwright
|
|
38
|
+
* evaluates there, finds what it recorded.
|
|
39
|
+
*/
|
|
40
|
+
export const READ_SELECTOR_ENGINE_SOURCE = `() => {
|
|
41
|
+
const read = (${readSemanticsFunction.toString()});
|
|
42
|
+
const key = Symbol.for(${JSON.stringify(READS_KEY)});
|
|
43
|
+
const queryAll = (root, selector) => {
|
|
44
|
+
if (root.nodeType !== 1) return [];
|
|
45
|
+
const body = JSON.parse(selector);
|
|
46
|
+
const options = { testIdAttribute: body.testIdAttribute, secureFieldSelector: body.secureFieldSelector, mode: { kind: 'node' } };
|
|
47
|
+
if (!Object.prototype.hasOwnProperty.call(root, key)) {
|
|
48
|
+
Object.defineProperty(root, key, { value: new Map(), enumerable: false, configurable: false, writable: false });
|
|
49
|
+
}
|
|
50
|
+
root[key].set(body.token, read(root, options));
|
|
51
|
+
return [root];
|
|
52
|
+
};
|
|
53
|
+
return { queryAll, query: (root, selector) => queryAll(root, selector)[0] ?? null };
|
|
54
|
+
}`;
|
|
55
|
+
/**
|
|
56
|
+
* Takes back, in order, the reads the engine recorded under `token` and
|
|
57
|
+
* deletes them; `null` where none was recorded, which is a match the engine
|
|
58
|
+
* never saw: a `*` capture earlier in the selector returns an element the
|
|
59
|
+
* chain only passed through. `locator.evaluateAll` calls it with the matched
|
|
60
|
+
* elements. A caller pinning handles evaluates it on the first handle with
|
|
61
|
+
* every handle in `elements`, so it runs in the frame the handles belong to;
|
|
62
|
+
* `page.evaluate` would reject handles taken inside an iframe.
|
|
63
|
+
*/
|
|
64
|
+
export const takeReadsFunction = (subject, arg) => (Array.isArray(subject) ? subject : (arg.elements ?? [])).map((element) => {
|
|
65
|
+
const reads = element[Symbol.for('e2e.locate.reads')];
|
|
66
|
+
const read = reads?.get(arg.token) ?? null;
|
|
67
|
+
reads?.delete(arg.token);
|
|
68
|
+
return read;
|
|
69
|
+
});
|
package/dist/refs.d.ts
CHANGED
package/dist/refs.js
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument validation for a route handler's decision. Every key is checked
|
|
3
|
+
* before the decision is taken: an option this engine does not implement
|
|
4
|
+
* (Playwright's `abort(errorCode)`, `fulfill({ response })`, a misspelled
|
|
5
|
+
* key) is `INVALID_ARGUMENT` rather than a request answered as if the option
|
|
6
|
+
* were absent. A continue URL goes through the harness's navigation rule,
|
|
7
|
+
* the same `resolveUrl` `goto` uses.
|
|
8
|
+
*/
|
|
9
|
+
import { type JsonValue } from 'e2e/engine';
|
|
10
|
+
/** What `route.fulfill` hands Playwright once the response is validated. */
|
|
11
|
+
export interface FulfillDecision {
|
|
12
|
+
readonly status: number;
|
|
13
|
+
readonly headers: Record<string, string>;
|
|
14
|
+
readonly contentType?: string;
|
|
15
|
+
readonly json?: JsonValue;
|
|
16
|
+
readonly body?: string;
|
|
17
|
+
/** Absolute; Playwright reads it and derives `Content-Type` from its extension when none is given. */
|
|
18
|
+
readonly path?: string;
|
|
19
|
+
}
|
|
20
|
+
/** What `route.continue` hands Playwright once the overrides are validated; `headers` before the site's are merged in. */
|
|
21
|
+
export interface ContinueDecision {
|
|
22
|
+
readonly url?: string;
|
|
23
|
+
readonly method?: string;
|
|
24
|
+
readonly headers?: Record<string, string>;
|
|
25
|
+
readonly postData?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validates `route.fulfill(response)`, synchronously so the decision is
|
|
29
|
+
* taken the moment `fulfill` is called. `path` resolves with `resolvePath`
|
|
30
|
+
* (the project root, never `process.cwd()`) and must name a file now, so a
|
|
31
|
+
* missing fixture fails the step with its own message, and `json` follows
|
|
32
|
+
* the JSON-value rules `evaluate` does. A `content-type` header becomes
|
|
33
|
+
* `contentType` when none is given, since Playwright overwrites the header
|
|
34
|
+
* with the type it derives for `json` and `path`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseFulfill(response: unknown, resolvePath: (file: string) => string): FulfillDecision;
|
|
37
|
+
/**
|
|
38
|
+
* Validates `route.continue(overrides)`. A `url` resolves against the base
|
|
39
|
+
* URL through `resolveUrl`, which denies what navigation denies, and keeps
|
|
40
|
+
* the request's scheme, as the browser requires of a rewritten request.
|
|
41
|
+
*/
|
|
42
|
+
export declare function parseContinue(overrides: unknown, requestUrl: string, resolveUrl: (url: string) => string): ContinueDecision;
|
|
43
|
+
/** Rejects any argument to a decision that takes none (`abort`, `fallback`). */
|
|
44
|
+
export declare function requireNoArguments(api: string, args: readonly unknown[]): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument validation for a route handler's decision. Every key is checked
|
|
3
|
+
* before the decision is taken: an option this engine does not implement
|
|
4
|
+
* (Playwright's `abort(errorCode)`, `fulfill({ response })`, a misspelled
|
|
5
|
+
* key) is `INVALID_ARGUMENT` rather than a request answered as if the option
|
|
6
|
+
* were absent. A continue URL goes through the harness's navigation rule,
|
|
7
|
+
* the same `resolveUrl` `goto` uses.
|
|
8
|
+
*/
|
|
9
|
+
import { statSync } from 'node:fs';
|
|
10
|
+
import { rejectUnknownOptions, TestError, validateJsonValue } from 'e2e/engine';
|
|
11
|
+
import { headerProblem } from './protected-app.js';
|
|
12
|
+
const FULFILL_KEYS = ['status', 'headers', 'contentType', 'json', 'body', 'path'];
|
|
13
|
+
const CONTINUE_KEYS = ['url', 'method', 'headers', 'postData'];
|
|
14
|
+
/**
|
|
15
|
+
* Validates `route.fulfill(response)`, synchronously so the decision is
|
|
16
|
+
* taken the moment `fulfill` is called. `path` resolves with `resolvePath`
|
|
17
|
+
* (the project root, never `process.cwd()`) and must name a file now, so a
|
|
18
|
+
* missing fixture fails the step with its own message, and `json` follows
|
|
19
|
+
* the JSON-value rules `evaluate` does. A `content-type` header becomes
|
|
20
|
+
* `contentType` when none is given, since Playwright overwrites the header
|
|
21
|
+
* with the type it derives for `json` and `path`.
|
|
22
|
+
*/
|
|
23
|
+
export function parseFulfill(response, resolvePath) {
|
|
24
|
+
const fields = requireOptions('route.fulfill', response, FULFILL_KEYS, false);
|
|
25
|
+
const sources = ['json', 'body', 'path'].filter((key) => fields[key] !== undefined);
|
|
26
|
+
if (sources.length > 1) {
|
|
27
|
+
throw new TestError('INVALID_ARGUMENT', `route.fulfill takes one of json, body, or path; got ${sources.join(' and ')}`);
|
|
28
|
+
}
|
|
29
|
+
const { status, headers, contentType, json, body, path } = fields;
|
|
30
|
+
if (status !== undefined && !(Number.isInteger(status) && status >= 100 && status <= 599)) {
|
|
31
|
+
throw new TestError('INVALID_ARGUMENT', 'route.fulfill status must be an integer from 100 to 599');
|
|
32
|
+
}
|
|
33
|
+
if (json !== undefined)
|
|
34
|
+
validateJsonValue(json, 'route.fulfill json');
|
|
35
|
+
const file = path === undefined ? undefined : resolvePath(requireString('route.fulfill', 'path', path, true));
|
|
36
|
+
if (file !== undefined)
|
|
37
|
+
requireFile(file);
|
|
38
|
+
const replied = requireHeaders('route.fulfill', headers) ?? {};
|
|
39
|
+
const type = contentType === undefined
|
|
40
|
+
? Object.entries(replied).find(([name]) => name.toLowerCase() === 'content-type')?.[1]
|
|
41
|
+
: requireString('route.fulfill', 'contentType', contentType, true);
|
|
42
|
+
return {
|
|
43
|
+
status: status ?? 200,
|
|
44
|
+
headers: replied,
|
|
45
|
+
...(type === undefined ? {} : { contentType: type }),
|
|
46
|
+
...(json === undefined ? {} : { json: json }),
|
|
47
|
+
...(body === undefined ? {} : { body: requireString('route.fulfill', 'body', body, false) }),
|
|
48
|
+
...(file === undefined ? {} : { path: file }),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validates `route.continue(overrides)`. A `url` resolves against the base
|
|
53
|
+
* URL through `resolveUrl`, which denies what navigation denies, and keeps
|
|
54
|
+
* the request's scheme, as the browser requires of a rewritten request.
|
|
55
|
+
*/
|
|
56
|
+
export function parseContinue(overrides, requestUrl, resolveUrl) {
|
|
57
|
+
const fields = requireOptions('route.continue', overrides, CONTINUE_KEYS, true);
|
|
58
|
+
const { url, method, headers, postData } = fields;
|
|
59
|
+
let target;
|
|
60
|
+
if (url !== undefined) {
|
|
61
|
+
target = resolveUrl(requireString('route.continue', 'url', url, true));
|
|
62
|
+
const from = new URL(requestUrl).protocol;
|
|
63
|
+
const to = new URL(target).protocol;
|
|
64
|
+
if (from !== to) {
|
|
65
|
+
throw new TestError('INVALID_ARGUMENT', `route.continue url must keep the request's ${from} scheme; got ${to}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const replaced = requireHeaders('route.continue', headers);
|
|
69
|
+
return {
|
|
70
|
+
...(target === undefined ? {} : { url: target }),
|
|
71
|
+
...(method === undefined ? {} : { method: requireString('route.continue', 'method', method, true) }),
|
|
72
|
+
...(replaced === undefined ? {} : { headers: replaced }),
|
|
73
|
+
...(postData === undefined ? {} : { postData: requireString('route.continue', 'postData', postData, false) }),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Rejects any argument to a decision that takes none (`abort`, `fallback`). */
|
|
77
|
+
export function requireNoArguments(api, args) {
|
|
78
|
+
if (args.some((arg) => arg !== undefined)) {
|
|
79
|
+
throw new TestError('INVALID_ARGUMENT', `${api}() takes no arguments`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** The options object with every key checked against `allowed`; `undefined` is `{}` when `optional`. */
|
|
83
|
+
function requireOptions(api, value, allowed, optional) {
|
|
84
|
+
if (value === undefined) {
|
|
85
|
+
if (optional)
|
|
86
|
+
return {};
|
|
87
|
+
throw new TestError('INVALID_ARGUMENT', `${api} takes an options object`);
|
|
88
|
+
}
|
|
89
|
+
rejectUnknownOptions(api, value, allowed);
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
/** `value` as a string, else `INVALID_ARGUMENT` naming the option. */
|
|
93
|
+
function requireString(api, key, value, nonempty) {
|
|
94
|
+
if (typeof value !== 'string' || (nonempty && value.length === 0)) {
|
|
95
|
+
throw new TestError('INVALID_ARGUMENT', `${api} ${key} must be a ${nonempty ? 'nonempty ' : ''}string`);
|
|
96
|
+
}
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
/** A header map the browser can send, or `undefined` when none was given. */
|
|
100
|
+
function requireHeaders(api, value) {
|
|
101
|
+
if (value === undefined)
|
|
102
|
+
return undefined;
|
|
103
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
104
|
+
throw new TestError('INVALID_ARGUMENT', `${api} headers must map names to string values`);
|
|
105
|
+
}
|
|
106
|
+
for (const [name, entry] of Object.entries(value)) {
|
|
107
|
+
const problem = headerProblem(name, entry);
|
|
108
|
+
if (problem !== undefined)
|
|
109
|
+
throw new TestError('INVALID_ARGUMENT', `${api} ${problem}`);
|
|
110
|
+
}
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
/** Rejects a path that does not name a regular file; synchronous so the check lands before the decision. */
|
|
114
|
+
function requireFile(file) {
|
|
115
|
+
let isFile = false;
|
|
116
|
+
try {
|
|
117
|
+
isFile = statSync(file, { throwIfNoEntry: false })?.isFile() === true;
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// A directory on the path the runner may not search (`EACCES`): not a file the route can serve.
|
|
121
|
+
}
|
|
122
|
+
if (!isFile) {
|
|
123
|
+
throw new TestError('INVALID_ARGUMENT', `route.fulfill path is not a readable file: ${file}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
package/dist/route-pattern.d.ts
CHANGED
|
@@ -10,4 +10,3 @@ export declare function compileRoutePattern(pattern: string): RegExp;
|
|
|
10
10
|
export declare function routePatternMatches(pattern: TextPattern, url: string): boolean;
|
|
11
11
|
/** Structural equality for route patterns, used by unroute. */
|
|
12
12
|
export declare function routePatternsEqual(a: TextPattern, b: TextPattern): boolean;
|
|
13
|
-
//# sourceMappingURL=route-pattern.d.ts.map
|
package/dist/route-pattern.js
CHANGED
package/dist/selector-engines.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/** Every selector engine the web engine registers, in a launched browser and in a CDP context alike. */
|
|
2
2
|
import { CLOSED_SHADOW_SELECTOR_ENGINES } from './closed-shadow.js';
|
|
3
3
|
import { EXACT_LABEL_SELECTOR_ENGINE, EXACT_LABEL_SELECTOR_ENGINE_SOURCE } from './label-selector.js';
|
|
4
|
+
import { READ_SELECTOR_ENGINE, READ_SELECTOR_ENGINE_SOURCE } from './read-selector.js';
|
|
5
|
+
import { SHOWN_SELECTOR_ENGINE, SHOWN_SELECTOR_ENGINE_SOURCE } from './shown-selector.js';
|
|
4
6
|
export const SELECTOR_ENGINES = [
|
|
5
7
|
...CLOSED_SHADOW_SELECTOR_ENGINES,
|
|
6
8
|
{ name: EXACT_LABEL_SELECTOR_ENGINE, source: EXACT_LABEL_SELECTOR_ENGINE_SOURCE },
|
|
9
|
+
{ name: READ_SELECTOR_ENGINE, source: READ_SELECTOR_ENGINE_SOURCE },
|
|
10
|
+
{ name: SHOWN_SELECTOR_ENGINE, source: SHOWN_SELECTOR_ENGINE_SOURCE },
|
|
7
11
|
];
|
|
8
|
-
//# sourceMappingURL=selector-engines.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The selector engine a `visible` query narrows through inside Playwright's
|
|
3
|
+
* own chain, before an index, a filter, or an enclosing scope runs. It keeps
|
|
4
|
+
* the query root when the reader's `hidden` state is false, so a node the
|
|
5
|
+
* chain retains is one `toBeVisible()` accepts by construction. Playwright's
|
|
6
|
+
* `filter({ visible: true })` could not stand in for it: it reads text under
|
|
7
|
+
* a `display: contents` element with `visibility: hidden` as visible, which
|
|
8
|
+
* paints nothing.
|
|
9
|
+
*/
|
|
10
|
+
/** Playwright selector engine name; `e2e-shown=` resolves to the query root when it reads as shown. */
|
|
11
|
+
export declare const SHOWN_SELECTOR_ENGINE = "e2e-shown";
|
|
12
|
+
/** The selector step that keeps a match only when it reads as shown. */
|
|
13
|
+
export declare const SHOWN_SELECTOR = "e2e-shown=";
|
|
14
|
+
/**
|
|
15
|
+
* The engine behind `e2e-shown=`: the query root itself when the reader's
|
|
16
|
+
* `hidden` mode answers false, else nothing. The reader is embedded as source
|
|
17
|
+
* because a selector engine, like the reader, is one self-contained page
|
|
18
|
+
* function.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SHOWN_SELECTOR_ENGINE_SOURCE: string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The selector engine a `visible` query narrows through inside Playwright's
|
|
3
|
+
* own chain, before an index, a filter, or an enclosing scope runs. It keeps
|
|
4
|
+
* the query root when the reader's `hidden` state is false, so a node the
|
|
5
|
+
* chain retains is one `toBeVisible()` accepts by construction. Playwright's
|
|
6
|
+
* `filter({ visible: true })` could not stand in for it: it reads text under
|
|
7
|
+
* a `display: contents` element with `visibility: hidden` as visible, which
|
|
8
|
+
* paints nothing.
|
|
9
|
+
*/
|
|
10
|
+
import { readSemanticsFunction } from './in-page/read-semantics.js';
|
|
11
|
+
import { SECURE_FIELD_SELECTOR } from './read-node.js';
|
|
12
|
+
/** Playwright selector engine name; `e2e-shown=` resolves to the query root when it reads as shown. */
|
|
13
|
+
export const SHOWN_SELECTOR_ENGINE = 'e2e-shown';
|
|
14
|
+
/** The selector step that keeps a match only when it reads as shown. */
|
|
15
|
+
export const SHOWN_SELECTOR = `${SHOWN_SELECTOR_ENGINE}=`;
|
|
16
|
+
/**
|
|
17
|
+
* The engine behind `e2e-shown=`: the query root itself when the reader's
|
|
18
|
+
* `hidden` mode answers false, else nothing. The reader is embedded as source
|
|
19
|
+
* because a selector engine, like the reader, is one self-contained page
|
|
20
|
+
* function.
|
|
21
|
+
*/
|
|
22
|
+
export const SHOWN_SELECTOR_ENGINE_SOURCE = `() => {
|
|
23
|
+
const read = (${readSemanticsFunction.toString()});
|
|
24
|
+
const options = { testIdAttribute: '', secureFieldSelector: ${JSON.stringify(SECURE_FIELD_SELECTOR)}, mode: { kind: 'hidden' } };
|
|
25
|
+
const queryAll = (root) => (read(root, options) ? [] : [root]);
|
|
26
|
+
return { queryAll, query: (root) => queryAll(root)[0] ?? null };
|
|
27
|
+
}`;
|
package/dist/support.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Shared error translation, filename, and swipe helpers for the Playwright engine. */
|
|
2
|
-
import type { ElementHandle, Locator as PwLocator, Mouse, Page } from 'playwright';
|
|
2
|
+
import type { ElementHandle, 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
4
|
export declare const DEFAULT_VIEWPORT: {
|
|
5
5
|
readonly width: 1280;
|
|
@@ -29,7 +29,7 @@ export type ActionTarget = {
|
|
|
29
29
|
* evaluate, page ownership, drag) have explicit helpers below so every fork is
|
|
30
30
|
* visible at its call site instead of hidden behind a uniform interface.
|
|
31
31
|
*/
|
|
32
|
-
type Actionable = Pick<PwLocator, 'click' | 'dblclick' | 'fill' | 'press' | '
|
|
32
|
+
type Actionable = Pick<PwLocator, 'click' | 'dblclick' | 'fill' | 'press' | 'hover' | 'scrollIntoViewIfNeeded' | 'selectOption' | 'setInputFiles'>;
|
|
33
33
|
/** Narrows one action target to the shared Playwright action surface. */
|
|
34
34
|
export declare function asActionable(target: ActionTarget): Actionable;
|
|
35
35
|
/**
|
|
@@ -119,6 +119,8 @@ export declare function isTestErrorCode(cause: unknown, code: string): cause is
|
|
|
119
119
|
export declare const POST_DISPATCH_PATTERN: RegExp;
|
|
120
120
|
/** Translates an unexpected Playwright error at the contract boundary. */
|
|
121
121
|
export declare function translatePwError(cause: unknown, operation: string): Error;
|
|
122
|
+
/** Whether a Playwright failure describes a read that lost its document to a navigation. */
|
|
123
|
+
export declare function isNavigationRace(cause: unknown): boolean;
|
|
122
124
|
/**
|
|
123
125
|
* Like translatePwError, but a read that lost its document to a navigation
|
|
124
126
|
* becomes retryable NODE_STALE so the runner re-reads the new document instead
|
|
@@ -144,4 +146,3 @@ export declare function nearestPixel(point: ViewportPoint): ViewportPoint;
|
|
|
144
146
|
export declare function performPointDrag(mouse: Mouse, start: ViewportPoint, end: ViewportPoint): Promise<void>;
|
|
145
147
|
export declare function performElementSwipe(target: ActionTarget, direction: ScrollDirection, momentum: Momentum, timeout: number): Promise<void>;
|
|
146
148
|
export {};
|
|
147
|
-
//# sourceMappingURL=support.d.ts.map
|
package/dist/support.js
CHANGED
|
@@ -274,7 +274,7 @@ const STALE_PATTERN = /detached|not attached|resolved to hidden|no element|not f
|
|
|
274
274
|
*/
|
|
275
275
|
const NAVIGATION_RACE_PATTERN = /execution context was destroyed|because of a navigation|navigating and changing the content|frame was detached|frame got detached|node is detached from document/i;
|
|
276
276
|
/** Whether a Playwright failure describes a read that lost its document to a navigation. */
|
|
277
|
-
function isNavigationRace(cause) {
|
|
277
|
+
export function isNavigationRace(cause) {
|
|
278
278
|
return NAVIGATION_RACE_PATTERN.test(message(cause));
|
|
279
279
|
}
|
|
280
280
|
/**
|
|
@@ -363,4 +363,3 @@ function wheelDelta(direction, distance) {
|
|
|
363
363
|
return [-distance, 0];
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
|
-
//# sourceMappingURL=support.js.map
|
package/dist/surface.d.ts
CHANGED
|
@@ -6,11 +6,12 @@
|
|
|
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';
|
|
9
|
+
import type { BrowserContext, Page, Route } from 'playwright-core';
|
|
10
10
|
import { type EngineAttemptContext, type EngineCleanupContext, 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';
|
|
13
13
|
import { DialogRouter } from './dialogs.ts';
|
|
14
|
+
import { type WebInitScript } from './init-scripts.ts';
|
|
14
15
|
import { ErrorLatch } from './support.ts';
|
|
15
16
|
type RoutePredicate = (url: URL) => boolean;
|
|
16
17
|
type RouteHandler = (route: Route) => Promise<void>;
|
|
@@ -41,7 +42,7 @@ export interface WebConnectOptions {
|
|
|
41
42
|
* same browser after a transport drop. Called once before the next
|
|
42
43
|
* operation, within its budget. The original browser and page must survive.
|
|
43
44
|
* Dispatched operations are never retried. The host owns browser cleanup.
|
|
44
|
-
* Context replacement, headers, basicAuth, and
|
|
45
|
+
* Context replacement, headers, basicAuth, userAgent, locale, and timezoneId are unavailable in this mode.
|
|
45
46
|
*/
|
|
46
47
|
readonly reconnectEndpoint?: (signal: AbortSignal) => string | Promise<string>;
|
|
47
48
|
}
|
|
@@ -131,6 +132,29 @@ export interface WebOptions {
|
|
|
131
132
|
* (`connect.reconnectEndpoint`, or a provider with `scope: 'attempt'`).
|
|
132
133
|
*/
|
|
133
134
|
readonly userAgent?: string;
|
|
135
|
+
/**
|
|
136
|
+
* The locale every attempt's context runs in, a BCP 47 tag such as
|
|
137
|
+
* `de-DE`, as Playwright's own `locale` context option sets it: what
|
|
138
|
+
* `navigator.language`, `Intl` formatting, and the `Accept-Language` header
|
|
139
|
+
* report. Defaults to the browser's own. An `accept-language` entry in
|
|
140
|
+
* `headers` beside it is `INVALID_CONFIG`, and so is a persistent context
|
|
141
|
+
* (`connect.reconnectEndpoint`, or a provider with `scope: 'attempt'`).
|
|
142
|
+
*/
|
|
143
|
+
readonly locale?: string;
|
|
144
|
+
/**
|
|
145
|
+
* The IANA time zone every attempt's context runs in, such as
|
|
146
|
+
* `Europe/Berlin`, as Playwright's own `timezoneId` context option sets it:
|
|
147
|
+
* what `Date` and `Intl` resolve local time against. Defaults to the
|
|
148
|
+
* machine's. A persistent context (`connect.reconnectEndpoint`, or a
|
|
149
|
+
* provider with `scope: 'attempt'`) is `INVALID_CONFIG`.
|
|
150
|
+
*/
|
|
151
|
+
readonly timezoneId?: string;
|
|
152
|
+
/**
|
|
153
|
+
* Scripts every document runs before the page's own, in every tab and
|
|
154
|
+
* frame: JavaScript source, a `{ path }` relative to the project root, or a
|
|
155
|
+
* function, which cannot close over test variables.
|
|
156
|
+
*/
|
|
157
|
+
readonly initScripts?: readonly WebInitScript[];
|
|
134
158
|
}
|
|
135
159
|
export declare class PlaywrightSurface {
|
|
136
160
|
/**
|
|
@@ -153,7 +177,13 @@ export declare class PlaywrightSurface {
|
|
|
153
177
|
private readonly basicAuth;
|
|
154
178
|
private readonly testIdAttribute;
|
|
155
179
|
private readonly userAgent;
|
|
180
|
+
private readonly locale;
|
|
181
|
+
private readonly timezoneId;
|
|
182
|
+
private readonly configuredInitScripts;
|
|
183
|
+
/** The attempt's init scripts, configured then added, applied to each context the attempt opens. */
|
|
184
|
+
private initScripts;
|
|
156
185
|
private app;
|
|
186
|
+
private projectRoot;
|
|
157
187
|
private headed;
|
|
158
188
|
private artifactsDir;
|
|
159
189
|
private artifactCounter;
|
|
@@ -170,7 +200,8 @@ export declare class PlaywrightSurface {
|
|
|
170
200
|
* leases the run's browsers from a provider. A CDP attach uses the
|
|
171
201
|
* remote's browser, so only a local launch needs the browser here. The
|
|
172
202
|
* download narrates through `info.log` and is bounded by the run's
|
|
173
|
-
* interrupt alone, never by a launch budget.
|
|
203
|
+
* interrupt alone, never by a launch budget. Init scripts are read first
|
|
204
|
+
* so a missing file fails the run before any worker starts.
|
|
174
205
|
*/
|
|
175
206
|
prepare(info: EnginePrepareInfo): Promise<EnginePrepareResult | void>;
|
|
176
207
|
/** Releases the browsers `prepare` leased; a local launch or a `connect` has nothing to release. */
|
|
@@ -235,8 +266,14 @@ export declare class PlaywrightSurface {
|
|
|
235
266
|
dispose(context: EngineCleanupContext): Promise<void>;
|
|
236
267
|
/** Returns the active attempt, including its connection generation and references. */
|
|
237
268
|
private requireSession;
|
|
269
|
+
/** Adds one attempt-scoped init script to the current context, for every document it creates from now on. */
|
|
270
|
+
addInitScript(source: string): Promise<void>;
|
|
238
271
|
/** Registers one attempt-scoped route on the current context. */
|
|
239
272
|
route(predicate: RoutePredicate, handler: RouteHandler): Promise<void>;
|
|
273
|
+
/** The configured headers a request to `url` carries, lowercased; `undefined` off the app's site. */
|
|
274
|
+
siteHeaders(url: string): Readonly<Record<string, string>> | undefined;
|
|
275
|
+
/** Resolves a file a test names against the project root, as config paths do, never `process.cwd()`. */
|
|
276
|
+
projectPath(file: string): string;
|
|
240
277
|
/** Removes one registered route from the attempt and the current context. */
|
|
241
278
|
unroute(predicate: RoutePredicate, handler: RouteHandler): Promise<void>;
|
|
242
279
|
requirePage(): Page;
|
|
@@ -354,4 +391,3 @@ export declare class PlaywrightSurface {
|
|
|
354
391
|
private captureObservation;
|
|
355
392
|
}
|
|
356
393
|
export {};
|
|
357
|
-
//# sourceMappingURL=surface.d.ts.map
|