@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
package/README.md
CHANGED
|
@@ -11,14 +11,13 @@ seam with no privilege either way.
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm install --save-dev e2e @e2e-dev/web
|
|
14
|
+
npm install --save-dev e2e @e2e-dev/web
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
keeps its
|
|
20
|
-
|
|
21
|
-
(npm's `ERESOLVE`), so upgrade `playwright` within the range.
|
|
17
|
+
This package depends on `playwright-core` pinned to an exact version, so the
|
|
18
|
+
engine always runs the Playwright it was tested against. An app that depends
|
|
19
|
+
on Playwright itself keeps its own copy; the two share a browser cache only
|
|
20
|
+
when their versions match.
|
|
22
21
|
|
|
23
22
|
```ts title="e2e.config.ts"
|
|
24
23
|
import type { E2EConfig } from 'e2e';
|
|
@@ -41,7 +40,10 @@ target, so a test can `requires: ['browser']`. Browser options:
|
|
|
41
40
|
`BrowserProvider` that leases hosted browsers, see below), `viewport`
|
|
42
41
|
(`{ width, height }`; default 1280x720), `testIdAttribute` (the attribute
|
|
43
42
|
`getByTestId` and a node's `testId` read; default `data-testid`), `userAgent`
|
|
44
|
-
(the `User-Agent` every attempt sends and `navigator.userAgent` reports),
|
|
43
|
+
(the `User-Agent` every attempt sends and `navigator.userAgent` reports),
|
|
44
|
+
`locale` and `timezoneId` (the language and time zone every attempt runs in,
|
|
45
|
+
such as `de-DE` and `Europe/Berlin`), `initScripts` (scripts every document
|
|
46
|
+
runs before the page's own), and
|
|
45
47
|
`connect` — attach to a remote browser over CDP instead of launching a local
|
|
46
48
|
one.
|
|
47
49
|
|
|
@@ -71,7 +73,7 @@ engine verifies the default context ID and active page target ID, clears stale
|
|
|
71
73
|
references, and never repeats a dispatched operation. A missing target fails
|
|
72
74
|
the attempt. The host owns deleting the remote browser after cleanup.
|
|
73
75
|
|
|
74
|
-
This mode does not support `headers`, `basicAuth`, `userAgent`, context reset, or session
|
|
76
|
+
This mode does not support `headers`, `basicAuth`, `userAgent`, `locale`, `timezoneId`, context reset, or session
|
|
75
77
|
state capture and restore. Recording resumes after reconnect, but a segment
|
|
76
78
|
lost during the disconnect remains unavailable. See the
|
|
77
79
|
[Playwright reference](../../docs/reference/web.mdx#recovering-a-cdp-transport)
|
|
@@ -131,9 +133,13 @@ In CI, install browsers as their own step instead, so the cost is visible and
|
|
|
131
133
|
cacheable:
|
|
132
134
|
|
|
133
135
|
```bash
|
|
134
|
-
npx
|
|
136
|
+
npx @e2e-dev/web install chromium --with-deps
|
|
135
137
|
```
|
|
136
138
|
|
|
139
|
+
It downloads the browsers for the Playwright version this package pins, the
|
|
140
|
+
ones the engine launches. Under pnpm, run the bin by name:
|
|
141
|
+
`pnpm exec e2e-web install chromium --with-deps`.
|
|
142
|
+
|
|
137
143
|
`--with-deps` also installs the system libraries a slim container image lacks.
|
|
138
144
|
|
|
139
145
|
## Documentation
|
package/dist/.build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.
|
|
1
|
+
{"version":"0.12.0"}
|
package/dist/actions.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Locator action dispatch for the Playwright engine. */
|
|
2
|
-
import type { Page } from 'playwright';
|
|
2
|
+
import type { Page } from 'playwright-core';
|
|
3
3
|
import { type LocatorAction, type NodeRef, type PointerAction, type ViewportPoint } from 'e2e/engine';
|
|
4
4
|
import { type ActionTarget } from './support.ts';
|
|
5
5
|
/**
|
|
@@ -20,4 +20,3 @@ export declare function dispatchPointerAction(page: Page, at: ViewportPoint, act
|
|
|
20
20
|
* committed, or an engine fault.
|
|
21
21
|
*/
|
|
22
22
|
export declare function classifyActionError(rawCause: unknown, action: LocatorAction): Error;
|
|
23
|
-
//# sourceMappingURL=actions.d.ts.map
|
package/dist/actions.js
CHANGED
|
@@ -1,12 +1,63 @@
|
|
|
1
1
|
/** Locator action dispatch for the Playwright engine. */
|
|
2
2
|
import { EngineError } from 'e2e/engine';
|
|
3
|
-
import { asActionable, isClassified, isPwTimeout, message, nearestPixel, performElementSwipe, performPointDrag, performPointerDrag, performViewportSwipe, POST_DISPATCH_PATTERN, } from './support.js';
|
|
3
|
+
import { asActionable, isClassified, isNavigationRace, isPwTimeout, message, nearestPixel, performElementSwipe, performPointDrag, performPointerDrag, performViewportSwipe, POST_DISPATCH_PATTERN, } from './support.js';
|
|
4
4
|
/** How long a long press holds the button when the action names no duration. */
|
|
5
5
|
const DEFAULT_LONG_PRESS_MS = 500;
|
|
6
6
|
/** The keys a click holds, as Playwright's `modifiers` takes them; the contract spells them the same. */
|
|
7
7
|
function heldKeys(action) {
|
|
8
8
|
return action.modifiers === undefined ? {} : { modifiers: [...action.modifiers] };
|
|
9
9
|
}
|
|
10
|
+
/** Playwright's words for an element handle whose element left the DOM. */
|
|
11
|
+
const DETACHED_PATTERN = /element (is |was )?(detached|not attached)/i;
|
|
12
|
+
/**
|
|
13
|
+
* Sets a checkbox, switch, or radio to `checked` with one click, as
|
|
14
|
+
* Playwright's `check` does, apart from the read after the click: a control
|
|
15
|
+
* that is gone by then (an app that swaps a picked radio for its selected
|
|
16
|
+
* view, or navigates on change) took the click, so the action is done, where
|
|
17
|
+
* Playwright reports it detached as if the click never happened. Whatever
|
|
18
|
+
* took its place is not read: the next observation or assertion shows it,
|
|
19
|
+
* as it does after a tap. The reads and the click share one element, so the state before and after the click
|
|
20
|
+
* is one control's.
|
|
21
|
+
*/
|
|
22
|
+
async function setChecked(target, checked, timeout) {
|
|
23
|
+
const deadline = Date.now() + timeout;
|
|
24
|
+
// Playwright reads a timeout of 0 as no timeout at all.
|
|
25
|
+
const remaining = () => Math.max(1, deadline - Date.now());
|
|
26
|
+
const element = target.kind === 'element' ? target.element : await target.locator.elementHandle({ timeout });
|
|
27
|
+
const verb = checked ? 'check' : 'uncheck';
|
|
28
|
+
try {
|
|
29
|
+
if ((await element.isChecked()) === checked)
|
|
30
|
+
return;
|
|
31
|
+
if (!checked && (await isRadio(element))) {
|
|
32
|
+
throw new EngineError('NOT_ACTIONABLE', 'uncheck cannot clear a radio button; select another radio in its group', {
|
|
33
|
+
retryable: false,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
await element.click({ timeout: remaining() });
|
|
37
|
+
let after;
|
|
38
|
+
try {
|
|
39
|
+
after = await element.isChecked();
|
|
40
|
+
}
|
|
41
|
+
catch (cause) {
|
|
42
|
+
if (DETACHED_PATTERN.test(message(cause)) || isNavigationRace(cause))
|
|
43
|
+
return;
|
|
44
|
+
throw cause;
|
|
45
|
+
}
|
|
46
|
+
if (after !== checked) {
|
|
47
|
+
throw new EngineError('NOT_ACTIONABLE', `${verb} clicked the control but its checked state did not change`, {
|
|
48
|
+
retryable: false,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (target.kind === 'locator')
|
|
54
|
+
void element.dispose().catch(() => undefined);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Whether a checkable element is a radio, which a click can select but never clear. */
|
|
58
|
+
function isRadio(element) {
|
|
59
|
+
return element.evaluate((node) => (node instanceof HTMLInputElement && node.type === 'radio') || node.getAttribute('role') === 'radio');
|
|
60
|
+
}
|
|
10
61
|
/**
|
|
11
62
|
* Dispatches one deterministic locator action onto a Playwright target with the
|
|
12
63
|
* platform's own actionability checks. `lookup` resolves the second ref of a
|
|
@@ -39,10 +90,8 @@ export async function dispatchLocatorAction(target, action, timeout, lookup) {
|
|
|
39
90
|
await locator.press(action.key, { timeout });
|
|
40
91
|
return;
|
|
41
92
|
case 'check':
|
|
42
|
-
await locator.check({ timeout });
|
|
43
|
-
return;
|
|
44
93
|
case 'uncheck':
|
|
45
|
-
await
|
|
94
|
+
await setChecked(target, action.kind === 'check', timeout);
|
|
46
95
|
return;
|
|
47
96
|
case 'focus':
|
|
48
97
|
// ElementHandle.focus takes no timeout: the element is already resolved.
|
|
@@ -177,7 +226,7 @@ export function classifyActionError(rawCause, action) {
|
|
|
177
226
|
if (/strict mode violation/i.test(text)) {
|
|
178
227
|
return new EngineError('ENGINE_FAILURE', text, { retryable: false, cause });
|
|
179
228
|
}
|
|
180
|
-
if (
|
|
229
|
+
if (DETACHED_PATTERN.test(text)) {
|
|
181
230
|
return new EngineError('NODE_STALE', text, { retryable: true, cause });
|
|
182
231
|
}
|
|
183
232
|
if (/Timeout .*exceeded/i.test(text) || isPwTimeout(rawCause)) {
|
|
@@ -191,4 +240,3 @@ export function classifyActionError(rawCause, action) {
|
|
|
191
240
|
}
|
|
192
241
|
return new EngineError('ENGINE_FAILURE', text, { retryable: false, cause });
|
|
193
242
|
}
|
|
194
|
-
//# sourceMappingURL=actions.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Browser, BrowserContext, BrowserContextOptions, Page } from 'playwright';
|
|
1
|
+
import type { Browser, BrowserContext, BrowserContextOptions, Page } from 'playwright-core';
|
|
2
2
|
import { type EngineCleanupContext, type OperationContext, type VideoSegment, type ViewportSize } from 'e2e/engine';
|
|
3
3
|
import { type CdpEndpointResolver, type SessionBinding } from './cdp-recovery.ts';
|
|
4
4
|
import { RefRegistry } from './refs.ts';
|
|
@@ -109,4 +109,3 @@ export declare class AttemptSession {
|
|
|
109
109
|
private finalize;
|
|
110
110
|
}
|
|
111
111
|
export {};
|
|
112
|
-
//# sourceMappingURL=attempt-session.d.ts.map
|
package/dist/attempt-session.js
CHANGED
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/** Shares one browser process connection across a worker's attempts. */
|
|
2
|
-
import type { Browser } from 'playwright';
|
|
3
|
-
import { chromium } from 'playwright';
|
|
4
|
-
|
|
2
|
+
import type { Browser } from 'playwright-core';
|
|
3
|
+
import { chromium } from 'playwright-core';
|
|
4
|
+
/** Every browser the engine launches, in the order the CLI lists them. */
|
|
5
|
+
export declare const BROWSER_NAMES: readonly ['chromium', 'firefox', 'webkit'];
|
|
6
|
+
export type BrowserName = (typeof BROWSER_NAMES)[number];
|
|
7
|
+
/** Whether `name` is a browser the engine launches. */
|
|
8
|
+
export declare function isBrowserName(name: string): name is BrowserName;
|
|
5
9
|
/**
|
|
6
10
|
* Launching a browser process costs hundreds of milliseconds; per-attempt
|
|
7
11
|
* isolation lives in browser contexts, so every attempt a worker runs can
|
|
@@ -31,4 +35,3 @@ export declare function browserType(name: BrowserName): typeof chromium;
|
|
|
31
35
|
* detaches the CDP session without killing the remote process the host owns.
|
|
32
36
|
*/
|
|
33
37
|
export declare function connectCdp(endpoint: string, timeoutMs: number): Promise<Browser>;
|
|
34
|
-
//# sourceMappingURL=browser-connection.d.ts.map
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
/** Shares one browser process connection across a worker's attempts. */
|
|
2
|
-
import { chromium, firefox, selectors, webkit } from 'playwright';
|
|
2
|
+
import { chromium, firefox, selectors, webkit } from 'playwright-core';
|
|
3
3
|
import { SELECTOR_ENGINES } from './selector-engines.js';
|
|
4
|
+
/** Every browser the engine launches, in the order the CLI lists them. */
|
|
5
|
+
export const BROWSER_NAMES = ['chromium', 'firefox', 'webkit'];
|
|
6
|
+
/** Whether `name` is a browser the engine launches. */
|
|
7
|
+
export function isBrowserName(name) {
|
|
8
|
+
return BROWSER_NAMES.includes(name);
|
|
9
|
+
}
|
|
4
10
|
/**
|
|
5
11
|
* Playwright's default SIGINT handler closes the browser and exits the
|
|
6
12
|
* process, and its SIGTERM handler closes the browser. A terminal Ctrl-C
|
|
@@ -94,4 +100,3 @@ function registerSelectorEngines() {
|
|
|
94
100
|
selectorEngines ??= Promise.all(SELECTOR_ENGINES.map(({ name, source }) => selectors.register(name, new Function(`return ${source};`)()))).then(() => undefined);
|
|
95
101
|
return selectorEngines;
|
|
96
102
|
}
|
|
97
|
-
//# sourceMappingURL=browser-connection.js.map
|
package/dist/browser.d.ts
CHANGED
|
@@ -15,20 +15,44 @@ import type { ActionOptions, Expectable, JsonValue, Locator, Screen, TextMatch }
|
|
|
15
15
|
import { type EngineFixtureContext } from 'e2e/engine';
|
|
16
16
|
import type { DialogHandler } from './dialogs.ts';
|
|
17
17
|
import type { PlaywrightSurface } from './surface.ts';
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* One of `json`, `body`, or `path`, or none for an empty body. `path` names a
|
|
20
|
+
* file relative to the project root; its `Content-Type` follows its
|
|
21
|
+
* extension unless `contentType` or a `content-type` header sets one.
|
|
22
|
+
*/
|
|
19
23
|
export type RouteFulfillResponse = {
|
|
20
24
|
status?: number;
|
|
21
25
|
headers?: Record<string, string>;
|
|
26
|
+
/** Sets the `Content-Type` response header. */
|
|
27
|
+
contentType?: string;
|
|
22
28
|
} & ({
|
|
23
29
|
json: JsonValue;
|
|
24
30
|
body?: never;
|
|
31
|
+
path?: never;
|
|
25
32
|
} | {
|
|
26
33
|
body: string;
|
|
27
34
|
json?: never;
|
|
35
|
+
path?: never;
|
|
36
|
+
} | {
|
|
37
|
+
path: string;
|
|
38
|
+
json?: never;
|
|
39
|
+
body?: never;
|
|
28
40
|
} | {
|
|
29
41
|
body?: never;
|
|
30
42
|
json?: never;
|
|
43
|
+
path?: never;
|
|
31
44
|
});
|
|
45
|
+
/** What `continue` changes about the request before it goes to the network. */
|
|
46
|
+
export interface RouteContinueOverrides {
|
|
47
|
+
/** Resolved against the base URL like `goto`; must keep the request's scheme. */
|
|
48
|
+
url?: string;
|
|
49
|
+
/** Request method. */
|
|
50
|
+
method?: string;
|
|
51
|
+
/** Replaces every request header; the configured `headers` for the app's site are added on top. */
|
|
52
|
+
headers?: Record<string, string>;
|
|
53
|
+
/** Request body. */
|
|
54
|
+
postData?: string;
|
|
55
|
+
}
|
|
32
56
|
export interface WebRoute {
|
|
33
57
|
/** The intercepted request. */
|
|
34
58
|
readonly request: {
|
|
@@ -39,8 +63,10 @@ export interface WebRoute {
|
|
|
39
63
|
};
|
|
40
64
|
/** Fulfills the intercepted request once. */
|
|
41
65
|
fulfill(response: RouteFulfillResponse): Promise<void>;
|
|
42
|
-
/**
|
|
43
|
-
continue(): Promise<void>;
|
|
66
|
+
/** Sends the intercepted request to the network once, skipping every other route. */
|
|
67
|
+
continue(overrides?: RouteContinueOverrides): Promise<void>;
|
|
68
|
+
/** Hands the intercepted request to the route registered before this one, or the network when none matches. */
|
|
69
|
+
fallback(): Promise<void>;
|
|
44
70
|
/** Aborts the intercepted request once. */
|
|
45
71
|
abort(): Promise<void>;
|
|
46
72
|
}
|
|
@@ -51,9 +77,9 @@ export interface WebResponse {
|
|
|
51
77
|
readonly status: number;
|
|
52
78
|
/** Response headers, lower-cased names. */
|
|
53
79
|
readonly headers: Readonly<Record<string, string>>;
|
|
54
|
-
/**
|
|
80
|
+
/** Waits for the body and parses it as JSON; rejects with `ACTION_FAILED` when the body could not be read. */
|
|
55
81
|
json<T = unknown>(): Promise<T>;
|
|
56
|
-
/**
|
|
82
|
+
/** Waits for the body and reads it as text; rejects with `ACTION_FAILED` when the body could not be read. */
|
|
57
83
|
text(): Promise<string>;
|
|
58
84
|
}
|
|
59
85
|
export interface CookieFields {
|
|
@@ -83,8 +109,9 @@ export type Cookie = CookieFields & ({
|
|
|
83
109
|
export interface BrowserExpectation {
|
|
84
110
|
/** Inverts the matcher. */
|
|
85
111
|
readonly not: BrowserExpectation;
|
|
86
|
-
/** Waits for the current URL to match. */
|
|
112
|
+
/** Waits for the current URL to match; `ignoreCase` compares a string case-insensitively and adds the `i` flag to a RegExp, `false` removes it. */
|
|
87
113
|
toHaveURL(expected: string | RegExp, options?: {
|
|
114
|
+
ignoreCase?: boolean;
|
|
88
115
|
timeout?: number;
|
|
89
116
|
}): Promise<void>;
|
|
90
117
|
/** Waits for the current title to match. */
|
|
@@ -146,6 +173,16 @@ export interface Browser extends Expectable<BrowserExpectation> {
|
|
|
146
173
|
evaluate<T extends JsonValue>(fn: string | (() => T | Promise<T>)): Promise<T>;
|
|
147
174
|
/** Evaluates trusted test code with one required JSON-safe argument. */
|
|
148
175
|
evaluate<T extends JsonValue, Arg extends JsonValue>(fn: string | ((arg: Arg) => T | Promise<T>), arg: Arg): Promise<T>;
|
|
176
|
+
/**
|
|
177
|
+
* Adds a script every document runs before the page's own, from the next
|
|
178
|
+
* navigation on: JavaScript source, a `{ path }` relative to the project
|
|
179
|
+
* root, or a function.
|
|
180
|
+
*/
|
|
181
|
+
addInitScript(script: string | {
|
|
182
|
+
path: string;
|
|
183
|
+
} | (() => unknown)): Promise<void>;
|
|
184
|
+
/** Adds an init script function called with one JSON argument. */
|
|
185
|
+
addInitScript<Arg extends JsonValue>(script: (arg: Arg) => unknown, arg: Arg): Promise<void>;
|
|
149
186
|
/** Adds an attempt-scoped network route. */
|
|
150
187
|
route(pattern: string | RegExp, handler: (route: WebRoute) => void | Promise<void>): Promise<void>;
|
|
151
188
|
/** Removes matching attempt-scoped routes. */
|
|
@@ -197,4 +234,3 @@ export interface Browser extends Expectable<BrowserExpectation> {
|
|
|
197
234
|
}
|
|
198
235
|
/** Builds the `browser` fixture for one attempt over the shared surface. */
|
|
199
236
|
export declare function createBrowserFixture(surface: PlaywrightSurface, context: EngineFixtureContext): Browser;
|
|
200
|
-
//# sourceMappingURL=browser.d.ts.map
|
package/dist/browser.js
CHANGED
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
* never re-implemented here: `context.app.resolveUrl` is the one place that
|
|
12
12
|
* says which URLs a test may open.
|
|
13
13
|
*/
|
|
14
|
-
import { Deadline, describePattern, EngineError, matchesText, pollCondition, TestError, toTextPattern, urlMatches, validateJsonValue, } from 'e2e/engine';
|
|
14
|
+
import { ConfigurationError, Deadline, describePattern, EngineError, matchesText, pollCondition, raceAbort, rejectUnknownOptions, TestError, toTextPattern, urlMatches, validateJsonValue, withTimeout, } from 'e2e/engine';
|
|
15
15
|
import { saveDownloadsTo, saveFromBrowser, saveLocally } from './downloads.js';
|
|
16
16
|
import { isTestErrorCode, message as causeMessage, translatePwError } from './support.js';
|
|
17
17
|
import { compileEvaluation } from './evaluation.js';
|
|
18
|
+
import { initScriptLabel, testInitScriptSource } from './init-scripts.js';
|
|
19
|
+
import { lowercaseNames } from './protected-app.js';
|
|
20
|
+
import { parseContinue, parseFulfill, requireNoArguments } from './route-options.js';
|
|
18
21
|
import { routePatternMatches, routePatternsEqual } from './route-pattern.js';
|
|
19
22
|
/**
|
|
20
23
|
* Reads the body once the response is known and keeps the outcome: a body
|
|
@@ -85,7 +88,10 @@ export function createBrowserFixture(surface, context) {
|
|
|
85
88
|
url: currentUrl,
|
|
86
89
|
title: currentTitle,
|
|
87
90
|
// The same poll as `expect(browser).toHaveURL`, exposed as a wait.
|
|
88
|
-
waitForURL: (url, options) =>
|
|
91
|
+
waitForURL: (url, options) => {
|
|
92
|
+
rejectUnknownOptions('browser.waitForURL', options, ['timeout']);
|
|
93
|
+
return expectation.toHaveURL(url, options);
|
|
94
|
+
},
|
|
89
95
|
locator: (selector) => context.locator({ kind: 'selector', selector: requireSelector('browser.locator', selector) }),
|
|
90
96
|
frameLocator: (selector) => frameScreen(context, [requireSelector('browser.frameLocator', selector)]),
|
|
91
97
|
async evaluate(fn, arg) {
|
|
@@ -100,6 +106,10 @@ export function createBrowserFixture(surface, context) {
|
|
|
100
106
|
validateJsonValue(result.value, 'evaluate result');
|
|
101
107
|
return result.value;
|
|
102
108
|
},
|
|
109
|
+
async addInitScript(script, ...rest) {
|
|
110
|
+
const source = await testInitScriptSource(script, rest.length === 0 ? undefined : { arg: rest[0] }, (file) => surface.projectPath(file));
|
|
111
|
+
await surface.guard(context.operation(), 'addInitScript', () => surface.addInitScript(source));
|
|
112
|
+
},
|
|
103
113
|
route(pattern, handler) {
|
|
104
114
|
const wirePattern = toTextPattern(pattern);
|
|
105
115
|
const predicate = (url) => routePatternMatches(wirePattern, url.href);
|
|
@@ -114,39 +124,62 @@ export function createBrowserFixture(surface, context) {
|
|
|
114
124
|
}
|
|
115
125
|
decided = true;
|
|
116
126
|
};
|
|
117
|
-
|
|
127
|
+
// Validation runs before `decide`, so a rejected option leaves the
|
|
128
|
+
// request to the abort below. A Playwright call that fails after the
|
|
129
|
+
// decision aborts too, rather than leaving the page's request pending.
|
|
130
|
+
const decideWith = async (name, act) => {
|
|
131
|
+
decide(name);
|
|
132
|
+
try {
|
|
133
|
+
await act();
|
|
134
|
+
}
|
|
135
|
+
catch (cause) {
|
|
136
|
+
await route.abort().catch(() => undefined);
|
|
137
|
+
throw cause;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const request = route.request();
|
|
141
|
+
const postData = request.postData();
|
|
118
142
|
const publicRoute = {
|
|
119
143
|
request: {
|
|
120
|
-
url:
|
|
121
|
-
method:
|
|
122
|
-
headers:
|
|
144
|
+
url: request.url(),
|
|
145
|
+
method: request.method(),
|
|
146
|
+
headers: request.headers(),
|
|
123
147
|
...(postData === null ? {} : { postData }),
|
|
124
148
|
},
|
|
125
149
|
fulfill: async (response) => {
|
|
126
|
-
|
|
127
|
-
await route.fulfill({
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
? { body: response.body }
|
|
134
|
-
: { body: '' }),
|
|
135
|
-
});
|
|
150
|
+
const decision = parseFulfill(response, (file) => surface.projectPath(file));
|
|
151
|
+
await decideWith('fulfill', () => route.fulfill({
|
|
152
|
+
...decision,
|
|
153
|
+
...(decision.json === undefined && decision.path === undefined && decision.body === undefined
|
|
154
|
+
? { body: '' }
|
|
155
|
+
: {}),
|
|
156
|
+
}));
|
|
136
157
|
},
|
|
137
|
-
continue: async () => {
|
|
138
|
-
|
|
139
|
-
|
|
158
|
+
continue: async (overrides) => {
|
|
159
|
+
const { headers, ...decision } = parseContinue(overrides, request.url(), context.app.resolveUrl);
|
|
160
|
+
// `continue` skips every route registered before this one, the
|
|
161
|
+
// site-header route included, so it merges those headers itself.
|
|
162
|
+
const site = surface.siteHeaders(decision.url ?? request.url());
|
|
163
|
+
await decideWith('continue', () => route.continue({
|
|
164
|
+
...decision,
|
|
165
|
+
...(headers === undefined && site === undefined
|
|
166
|
+
? {}
|
|
167
|
+
: { headers: { ...lowercaseNames(headers ?? request.headers()), ...site } }),
|
|
168
|
+
}));
|
|
140
169
|
},
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
await route.
|
|
170
|
+
fallback: async (...args) => {
|
|
171
|
+
requireNoArguments('route.fallback', args);
|
|
172
|
+
await decideWith('fallback', () => route.fallback());
|
|
173
|
+
},
|
|
174
|
+
abort: async (...args) => {
|
|
175
|
+
requireNoArguments('route.abort', args);
|
|
176
|
+
await decideWith('abort', () => route.abort());
|
|
144
177
|
},
|
|
145
178
|
};
|
|
146
179
|
try {
|
|
147
180
|
await handler(publicRoute);
|
|
148
181
|
if (!decided) {
|
|
149
|
-
throw new TestError('ACTION_FAILED', 'route handler returned without calling fulfill, continue, or abort');
|
|
182
|
+
throw new TestError('ACTION_FAILED', 'route handler returned without calling fulfill, continue, fallback, or abort');
|
|
150
183
|
}
|
|
151
184
|
}
|
|
152
185
|
catch (cause) {
|
|
@@ -186,13 +219,22 @@ export function createBrowserFixture(surface, context) {
|
|
|
186
219
|
const operation = context.operation(options?.timeout);
|
|
187
220
|
return surface.guard(operation, 'waitForResponse', async (currentOperation) => {
|
|
188
221
|
const response = await surface.requirePage().waitForResponse((candidate) => routePatternMatches(wirePattern, candidate.url()), { timeout: currentOperation.timeoutMs });
|
|
189
|
-
|
|
222
|
+
// Started now, while the browser still holds the body, and awaited
|
|
223
|
+
// only by `text` and `json` on a budget of their own: the timeout
|
|
224
|
+
// bounds the match, never a body still streaming in behind headers
|
|
225
|
+
// that already arrived.
|
|
226
|
+
const body = readResponseBody(response);
|
|
227
|
+
const text = async () => {
|
|
228
|
+
const { signal, timeoutMs } = context.operation();
|
|
229
|
+
const read = await raceAbort(withTimeout(body, timeoutMs, () => new TestError('ACTION_FAILED', `waitForResponse: response body did not finish within ${timeoutMs}ms`)), signal, 'waitForResponse body');
|
|
230
|
+
return read();
|
|
231
|
+
};
|
|
190
232
|
return {
|
|
191
233
|
url: response.url(),
|
|
192
234
|
status: response.status(),
|
|
193
235
|
headers: response.headers(),
|
|
194
|
-
json: async () => JSON.parse(await
|
|
195
|
-
text
|
|
236
|
+
json: async () => JSON.parse(await text()),
|
|
237
|
+
text,
|
|
196
238
|
};
|
|
197
239
|
});
|
|
198
240
|
},
|
|
@@ -212,17 +254,24 @@ export function createBrowserFixture(surface, context) {
|
|
|
212
254
|
setCookies(cookies) {
|
|
213
255
|
const scheme = new URL(baseHref()).protocol === 'https:' ? 'https' : 'http';
|
|
214
256
|
// The harness's URL rule decides which cookie targets are admitted; a
|
|
215
|
-
// domain cookie is checked as the origin it would be sent to.
|
|
216
|
-
for
|
|
217
|
-
|
|
218
|
-
|
|
257
|
+
// domain cookie is checked as the origin it would be sent to. The rule
|
|
258
|
+
// admits `about:blank` for navigation, which holds no cookie. A `url`
|
|
259
|
+
// cookie is set on the URL the rule resolved, so a relative one lands
|
|
260
|
+
// on the base URL the way `goto` would.
|
|
261
|
+
const targets = cookies.map((cookie) => {
|
|
262
|
+
const target = context.app.resolveUrl(cookie.url === undefined ? `${scheme}://${cookie.domain.replace(/^\./, '')}` : cookie.url);
|
|
263
|
+
if (!/^https?:/.test(target)) {
|
|
264
|
+
throw new ConfigurationError('POLICY_DENIED', `cookie URL must be http(s): ${target}`);
|
|
265
|
+
}
|
|
266
|
+
return cookie.url === undefined
|
|
267
|
+
? { domain: cookie.domain, path: cookie.path ?? '/' }
|
|
268
|
+
: { url: target };
|
|
269
|
+
});
|
|
219
270
|
return surface.guard(context.operation(), 'setCookies', async () => {
|
|
220
|
-
await surface.requireContext().addCookies(cookies.map((cookie) => ({
|
|
271
|
+
await surface.requireContext().addCookies(cookies.map((cookie, index) => ({
|
|
221
272
|
name: cookie.name,
|
|
222
273
|
value: cookie.value,
|
|
223
|
-
...
|
|
224
|
-
? { domain: cookie.domain, path: cookie.path ?? '/' }
|
|
225
|
-
: { url: cookie.url }),
|
|
274
|
+
...targets[index],
|
|
226
275
|
...(cookie.expires !== undefined ? { expires: cookie.expires } : {}),
|
|
227
276
|
...(cookie.httpOnly !== undefined ? { httpOnly: cookie.httpOnly } : {}),
|
|
228
277
|
...(cookie.secure !== undefined ? { secure: cookie.secure } : {}),
|
|
@@ -310,6 +359,7 @@ export function createBrowserFixture(surface, context) {
|
|
|
310
359
|
title: action,
|
|
311
360
|
waitForURL: { ...navigationCall, verifies: true, label: (url) => String(url) },
|
|
312
361
|
evaluate: action,
|
|
362
|
+
addInitScript: { ...action, label: (script) => initScriptLabel(script) },
|
|
313
363
|
route: { ...action, label: (pattern) => String(pattern) },
|
|
314
364
|
unroute: { ...action, label: (pattern) => String(pattern) },
|
|
315
365
|
waitForResponse: { ...action, timeout: (_pattern, options) => options?.timeout, label: (pattern) => String(pattern) },
|
|
@@ -333,20 +383,28 @@ function createBrowserExpectation(deps, negated = false) {
|
|
|
333
383
|
onTimeout: async () => new TestError('ASSERTION_FAILED', `expect.${negated ? 'not.' : ''}${api} failed\nexpected: ${negated ? 'not ' : ''}${label}\nobserved: ${await observed()}`),
|
|
334
384
|
});
|
|
335
385
|
};
|
|
386
|
+
const validate = (api, options, keys = ['timeout']) => rejectUnknownOptions(`expect.${negated ? 'not.' : ''}${api}`, options, keys);
|
|
336
387
|
return {
|
|
337
388
|
get not() {
|
|
338
389
|
return createBrowserExpectation(deps, !negated);
|
|
339
390
|
},
|
|
340
391
|
toHaveURL(expected, options) {
|
|
392
|
+
validate('toHaveURL', options, ['ignoreCase', 'timeout']);
|
|
393
|
+
const ignoreCase = options?.ignoreCase;
|
|
394
|
+
if (ignoreCase !== undefined && typeof ignoreCase !== 'boolean') {
|
|
395
|
+
throw new TestError('INVALID_ARGUMENT', `expect.${negated ? 'not.' : ''}toHaveURL option ignoreCase must be a boolean`);
|
|
396
|
+
}
|
|
341
397
|
const label = typeof expected === 'string' ? expected : String(expected);
|
|
342
398
|
const target = deps.baseHref();
|
|
343
|
-
return poll('toHaveURL', `URL ${label}`, async () => urlMatches(await deps.currentUrl(), expected, target), async () => `URL ${await deps.currentUrl()}`, options?.timeout);
|
|
399
|
+
return poll('toHaveURL', `URL ${label}${ignoreCase === true ? ' (ignoring case)' : ''}`, async () => urlMatches(await deps.currentUrl(), expected, target, ignoreCase), async () => `URL ${await deps.currentUrl()}`, options?.timeout);
|
|
344
400
|
},
|
|
345
401
|
toHaveTitle(expected, options) {
|
|
402
|
+
validate('toHaveTitle', options);
|
|
346
403
|
const pattern = toTextPattern(expected, { exact: true });
|
|
347
404
|
return poll('toHaveTitle', `title ${describePattern(pattern)}`, async () => matchesText(await deps.currentTitle(), pattern), async () => `title ${JSON.stringify(await deps.currentTitle())}`, options?.timeout);
|
|
348
405
|
},
|
|
349
406
|
toHaveClass(target, expected, options) {
|
|
407
|
+
validate('toHaveClass', options);
|
|
350
408
|
const pattern = toTextPattern(expected, { exact: true });
|
|
351
409
|
return poll('toHaveClass', `class ${describePattern(pattern)}`, async () => {
|
|
352
410
|
try {
|
|
@@ -395,4 +453,3 @@ function frameScreen(context, frames) {
|
|
|
395
453
|
frameLocator: (selector) => frameScreen(context, [...frames, requireSelector('frameLocator', selector)]),
|
|
396
454
|
});
|
|
397
455
|
}
|
|
398
|
-
//# sourceMappingURL=browser.js.map
|
package/dist/capture-scope.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Deadline and handle ownership for one observation capture operation. */
|
|
2
|
-
import type { JSHandle } from 'playwright';
|
|
2
|
+
import type { JSHandle } from 'playwright-core';
|
|
3
3
|
/** Releases acquired and late-arriving handles without waiting on the transport. */
|
|
4
4
|
export declare class CaptureScope {
|
|
5
5
|
readonly deadline: number;
|
|
@@ -18,4 +18,3 @@ export declare class CaptureScope {
|
|
|
18
18
|
/** Refuses further work after cancellation, expiry, or completion. */
|
|
19
19
|
check(): void;
|
|
20
20
|
}
|
|
21
|
-
//# sourceMappingURL=capture-scope.d.ts.map
|
package/dist/capture-scope.js
CHANGED
package/dist/cdp-recovery.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Validates a persistent CDP binding without publishing or retaining it. */
|
|
2
|
-
import type { Browser, BrowserContext, Page } from 'playwright';
|
|
2
|
+
import type { Browser, BrowserContext, Page } from 'playwright-core';
|
|
3
3
|
import { EngineError } from 'e2e/engine';
|
|
4
4
|
import { type ConnectionBudget } from './operation-budget.ts';
|
|
5
5
|
export type CdpEndpointResolver = (signal: AbortSignal) => string | Promise<string>;
|
|
@@ -26,4 +26,3 @@ export declare function attachPersistent(resolve: CdpEndpointResolver, budget: C
|
|
|
26
26
|
readonly identity: PersistentIdentity;
|
|
27
27
|
}>;
|
|
28
28
|
export {};
|
|
29
|
-
//# sourceMappingURL=cdp-recovery.d.ts.map
|
package/dist/cdp-recovery.js
CHANGED
package/dist/cdp-selectors.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Registers the engine's selector engines in a CDP default context; every locator and secure-field mask needs them. */
|
|
2
|
-
import type { BrowserContext, Page } from 'playwright';
|
|
2
|
+
import type { BrowserContext, Page } from 'playwright-core';
|
|
3
3
|
import { type ConnectionBudget } from './operation-budget.ts';
|
|
4
4
|
/**
|
|
5
5
|
* Playwright 1.63 seeds custom selectors only in newly created contexts, not
|
|
@@ -12,4 +12,3 @@ import { type ConnectionBudget } from './operation-budget.ts';
|
|
|
12
12
|
export declare function registerCdpSelectors(context: BrowserContext, budget: ConnectionBudget): Promise<void>;
|
|
13
13
|
/** Existing closed roots cannot be recovered if their document loaded without the tracking hook. */
|
|
14
14
|
export declare function requireCdpShadowTracking(page: Page, budget: ConnectionBudget): Promise<void>;
|
|
15
|
-
//# sourceMappingURL=cdp-selectors.d.ts.map
|
package/dist/cdp-selectors.js
CHANGED
|
@@ -49,4 +49,3 @@ export async function requireCdpShadowTracking(page, budget) {
|
|
|
49
49
|
throw new EngineError('ENGINE_FAILURE', 'CDP recovery failed: closed shadow root tracking is unavailable after a document changed while disconnected; locators and secure-field masks need it', { retryable: false });
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
//# sourceMappingURL=cdp-selectors.js.map
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** The `e2e-web` command: browser installs with the Playwright version this engine pins. */
|
|
2
|
+
/** What `main` writes to and spawns; tests replace them. */
|
|
3
|
+
export interface CliIo {
|
|
4
|
+
readonly stdout: (text: string) => void;
|
|
5
|
+
readonly stderr: (text: string) => void;
|
|
6
|
+
readonly run: (args: readonly string[]) => Promise<number>;
|
|
7
|
+
}
|
|
8
|
+
/** Runs `e2e-web` with the arguments after the command name and resolves to the exit code. */
|
|
9
|
+
export declare function main(args: readonly string[], io?: CliIo): Promise<number>;
|