@e2e-dev/web 0.11.0-canary-20260928184528
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/LICENSE +202 -0
- package/NOTICE +5 -0
- package/README.md +138 -0
- package/dist/.build.json +1 -0
- package/dist/actions.d.ts +23 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +174 -0
- package/dist/actions.js.map +1 -0
- package/dist/attempt-session.d.ts +77 -0
- package/dist/attempt-session.d.ts.map +1 -0
- package/dist/attempt-session.js +293 -0
- package/dist/attempt-session.js.map +1 -0
- package/dist/browser-connection.d.ts +34 -0
- package/dist/browser-connection.d.ts.map +1 -0
- package/dist/browser-connection.js +86 -0
- package/dist/browser-connection.js.map +1 -0
- package/dist/capture-scope.d.ts +21 -0
- package/dist/capture-scope.d.ts.map +1 -0
- package/dist/capture-scope.js +61 -0
- package/dist/capture-scope.js.map +1 -0
- package/dist/cdp-recovery.d.ts +29 -0
- package/dist/cdp-recovery.d.ts.map +1 -0
- package/dist/cdp-recovery.js +84 -0
- package/dist/cdp-recovery.js.map +1 -0
- package/dist/cdp-selectors.d.ts +15 -0
- package/dist/cdp-selectors.d.ts.map +1 -0
- package/dist/cdp-selectors.js +52 -0
- package/dist/cdp-selectors.js.map +1 -0
- package/dist/closed-shadow.d.ts +64 -0
- package/dist/closed-shadow.d.ts.map +1 -0
- package/dist/closed-shadow.js +142 -0
- package/dist/closed-shadow.js.map +1 -0
- package/dist/dialogs.d.ts +38 -0
- package/dist/dialogs.d.ts.map +1 -0
- package/dist/dialogs.js +87 -0
- package/dist/dialogs.js.map +1 -0
- package/dist/engine.d.ts +26 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +176 -0
- package/dist/engine.js.map +1 -0
- package/dist/evaluation.d.ts +18 -0
- package/dist/evaluation.d.ts.map +1 -0
- package/dist/evaluation.js +35 -0
- package/dist/evaluation.js.map +1 -0
- package/dist/in-page/read-semantics.d.ts +28 -0
- package/dist/in-page/read-semantics.d.ts.map +1 -0
- package/dist/in-page/read-semantics.js +1178 -0
- package/dist/in-page/read-semantics.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/install.d.ts +40 -0
- package/dist/install.d.ts.map +1 -0
- package/dist/install.js +117 -0
- package/dist/install.js.map +1 -0
- package/dist/label-selector.d.ts +37 -0
- package/dist/label-selector.d.ts.map +1 -0
- package/dist/label-selector.js +53 -0
- package/dist/label-selector.js.map +1 -0
- package/dist/locators.d.ts +76 -0
- package/dist/locators.d.ts.map +1 -0
- package/dist/locators.js +289 -0
- package/dist/locators.js.map +1 -0
- package/dist/observation-capture.d.ts +12 -0
- package/dist/observation-capture.d.ts.map +1 -0
- package/dist/observation-capture.js +93 -0
- package/dist/observation-capture.js.map +1 -0
- package/dist/observation.d.ts +63 -0
- package/dist/observation.d.ts.map +1 -0
- package/dist/observation.js +280 -0
- package/dist/observation.js.map +1 -0
- package/dist/observe.d.ts +51 -0
- package/dist/observe.d.ts.map +1 -0
- package/dist/observe.js +105 -0
- package/dist/observe.js.map +1 -0
- package/dist/operation-budget.d.ts +11 -0
- package/dist/operation-budget.d.ts.map +1 -0
- package/dist/operation-budget.js +40 -0
- package/dist/operation-budget.js.map +1 -0
- package/dist/protected-app.d.ts +32 -0
- package/dist/protected-app.d.ts.map +1 -0
- package/dist/protected-app.js +39 -0
- package/dist/protected-app.js.map +1 -0
- package/dist/provider.d.ts +158 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +311 -0
- package/dist/provider.js.map +1 -0
- package/dist/read-node.d.ts +152 -0
- package/dist/read-node.d.ts.map +1 -0
- package/dist/read-node.js +42 -0
- package/dist/read-node.js.map +1 -0
- package/dist/refs.d.ts +43 -0
- package/dist/refs.d.ts.map +1 -0
- package/dist/refs.js +85 -0
- package/dist/refs.js.map +1 -0
- package/dist/route-pattern.d.ts +13 -0
- package/dist/route-pattern.d.ts.map +1 -0
- package/dist/route-pattern.js +55 -0
- package/dist/route-pattern.js.map +1 -0
- package/dist/selector-engines.d.ts +6 -0
- package/dist/selector-engines.d.ts.map +1 -0
- package/dist/selector-engines.js +8 -0
- package/dist/selector-engines.js.map +1 -0
- package/dist/support.d.ts +147 -0
- package/dist/support.d.ts.map +1 -0
- package/dist/support.js +366 -0
- package/dist/support.js.map +1 -0
- package/dist/surface.d.ts +313 -0
- package/dist/surface.d.ts.map +1 -0
- package/dist/surface.js +691 -0
- package/dist/surface.js.map +1 -0
- package/dist/video.d.ts +59 -0
- package/dist/video.d.ts.map +1 -0
- package/dist/video.js +116 -0
- package/dist/video.js.map +1 -0
- package/dist/web.d.ts +191 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +382 -0
- package/dist/web.js.map +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { EngineError } from 'e2e/engine';
|
|
2
|
+
import { connectCdp } from './browser-connection.js';
|
|
3
|
+
import { registerCdpSelectors, requireCdpShadowTracking } from './cdp-selectors.js';
|
|
4
|
+
import { connectionAbort } from './operation-budget.js';
|
|
5
|
+
/** A failed reconnect must never quietly provision another session. */
|
|
6
|
+
export function recoveryFailed(detail) {
|
|
7
|
+
return new EngineError('ENGINE_FAILURE', `CDP recovery failed: ${detail}`, { retryable: false });
|
|
8
|
+
}
|
|
9
|
+
/** Reads protocol identity, independent of URL, document contents, and tab order. */
|
|
10
|
+
export async function targetIdentity(page) {
|
|
11
|
+
const session = await page.context().newCDPSession(page);
|
|
12
|
+
try {
|
|
13
|
+
return (await session.send('Target.getTargetInfo')).targetInfo;
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await session.detach().catch(() => undefined);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** Owns an uncommitted connection until its identity is proven; aborted and late connections detach. */
|
|
20
|
+
export async function attachPersistent(resolve, budget, previous) {
|
|
21
|
+
const { signal } = budget;
|
|
22
|
+
const deadline = Date.now() + budget.timeoutMs;
|
|
23
|
+
const check = () => { if (signal.aborted)
|
|
24
|
+
throw connectionAbort(signal, 'CDP connection'); };
|
|
25
|
+
check();
|
|
26
|
+
const endpoint = await resolve(signal);
|
|
27
|
+
check();
|
|
28
|
+
if (typeof endpoint !== 'string' || endpoint.trim() === '')
|
|
29
|
+
throw recoveryFailed('the endpoint is empty');
|
|
30
|
+
const remaining = deadline - Date.now();
|
|
31
|
+
if (remaining <= 0)
|
|
32
|
+
throw new EngineError('OPERATION_TIMEOUT', 'CDP connection timed out', { retryable: false });
|
|
33
|
+
const browser = await connectCdp(endpoint, remaining);
|
|
34
|
+
const detach = () => { void browser.close().catch(() => undefined); };
|
|
35
|
+
signal.addEventListener('abort', detach, { once: true });
|
|
36
|
+
try {
|
|
37
|
+
check();
|
|
38
|
+
const session = await browser.newBrowserCDPSession();
|
|
39
|
+
let contextId;
|
|
40
|
+
try {
|
|
41
|
+
const contexts = await session.send('Target.getBrowserContexts');
|
|
42
|
+
if (contexts.browserContextIds.length > 0) {
|
|
43
|
+
throw recoveryFailed('the remote browser must contain only its dedicated default context');
|
|
44
|
+
}
|
|
45
|
+
contextId = contexts.defaultBrowserContextId;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await session.detach().catch(() => undefined);
|
|
49
|
+
}
|
|
50
|
+
check();
|
|
51
|
+
const context = browser.contexts()[0];
|
|
52
|
+
if (context === undefined)
|
|
53
|
+
throw recoveryFailed('the default context is missing');
|
|
54
|
+
if (contextId === undefined || contextId === '')
|
|
55
|
+
throw recoveryFailed('the default context identity is unavailable');
|
|
56
|
+
if (previous !== undefined && contextId !== previous.contextId) {
|
|
57
|
+
throw recoveryFailed('reconnectEndpoint returned a different browser');
|
|
58
|
+
}
|
|
59
|
+
await registerCdpSelectors(context, { signal, timeoutMs: deadline - Date.now() });
|
|
60
|
+
check();
|
|
61
|
+
if (previous?.target === undefined)
|
|
62
|
+
return { browser, context, page: null, identity: { contextId } };
|
|
63
|
+
for (const page of context.pages()) {
|
|
64
|
+
const target = await targetIdentity(page);
|
|
65
|
+
check();
|
|
66
|
+
if (target.targetId !== previous.target.targetId)
|
|
67
|
+
continue;
|
|
68
|
+
if (target.browserContextId !== previous.target.browserContextId) {
|
|
69
|
+
throw recoveryFailed('the original page belongs to a different context');
|
|
70
|
+
}
|
|
71
|
+
await requireCdpShadowTracking(page, { signal, timeoutMs: deadline - Date.now() });
|
|
72
|
+
return { browser, context, page, identity: { contextId, target } };
|
|
73
|
+
}
|
|
74
|
+
throw recoveryFailed('the original page no longer exists');
|
|
75
|
+
}
|
|
76
|
+
catch (cause) {
|
|
77
|
+
await browser.close().catch(() => undefined);
|
|
78
|
+
throw cause;
|
|
79
|
+
}
|
|
80
|
+
finally {
|
|
81
|
+
signal.removeEventListener('abort', detach);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=cdp-recovery.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdp-recovery.js","sourceRoot":"","sources":["../src/cdp-recovery.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
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';
|
|
3
|
+
import { type ConnectionBudget } from './operation-budget.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Playwright 1.63 seeds custom selectors only in newly created contexts, not
|
|
6
|
+
* the persistent context returned by connectOverCDP. Its public register API
|
|
7
|
+
* rejects an already registered name before reaching that context. Use the
|
|
8
|
+
* same context channel as selectors.register, without allocating global names
|
|
9
|
+
* on every reconnect. Missing channel support must fail the connection: every
|
|
10
|
+
* locator resolves through these engines and every masked capture relies on them.
|
|
11
|
+
*/
|
|
12
|
+
export declare function registerCdpSelectors(context: BrowserContext, budget: ConnectionBudget): Promise<void>;
|
|
13
|
+
/** Existing closed roots cannot be recovered if their document loaded without the tracking hook. */
|
|
14
|
+
export declare function requireCdpShadowTracking(page: Page, budget: ConnectionBudget): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=cdp-selectors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdp-selectors.d.ts","sourceRoot":"","sources":["../src/cdp-selectors.ts"],"names":[],"mappings":"AAAA,wHAAwH;AAExH,OAAO,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvD,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAW/E;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuB3G;AAED,oGAAoG;AACpG,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CASlG"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Registers the engine's selector engines in a CDP default context; every locator and secure-field mask needs them. */
|
|
2
|
+
import { EngineError } from 'e2e/engine';
|
|
3
|
+
import { connectionAbort } from './operation-budget.js';
|
|
4
|
+
import { CLOSED_SHADOW_ROOTS_KEY } from './closed-shadow.js';
|
|
5
|
+
import { SELECTOR_ENGINES } from './selector-engines.js';
|
|
6
|
+
/**
|
|
7
|
+
* Playwright 1.63 seeds custom selectors only in newly created contexts, not
|
|
8
|
+
* the persistent context returned by connectOverCDP. Its public register API
|
|
9
|
+
* rejects an already registered name before reaching that context. Use the
|
|
10
|
+
* same context channel as selectors.register, without allocating global names
|
|
11
|
+
* on every reconnect. Missing channel support must fail the connection: every
|
|
12
|
+
* locator resolves through these engines and every masked capture relies on them.
|
|
13
|
+
*/
|
|
14
|
+
export async function registerCdpSelectors(context, budget) {
|
|
15
|
+
if (budget.signal.aborted)
|
|
16
|
+
throw connectionAbort(budget.signal, 'CDP selector registration');
|
|
17
|
+
if (budget.timeoutMs <= 0) {
|
|
18
|
+
throw new EngineError('OPERATION_TIMEOUT', 'CDP selector registration timed out', { retryable: false });
|
|
19
|
+
}
|
|
20
|
+
const channel = Reflect.get(context, '_channel');
|
|
21
|
+
const register = channel !== null && typeof channel === 'object' ? Reflect.get(channel, 'registerSelectorEngine') : undefined;
|
|
22
|
+
if (typeof register !== 'function') {
|
|
23
|
+
throw new EngineError('UNSUPPORTED_CAPABILITY', 'this Playwright version cannot register selector engines in the CDP context; locators and secure-field masks need them', { retryable: false });
|
|
24
|
+
}
|
|
25
|
+
for (const { name, source } of SELECTOR_ENGINES) {
|
|
26
|
+
try {
|
|
27
|
+
await channel.registerSelectorEngine({ selectorEngine: {
|
|
28
|
+
name,
|
|
29
|
+
source: `(${source})(undefined)`,
|
|
30
|
+
contentScript: false,
|
|
31
|
+
} }, { timeout: budget.timeoutMs, signal: budget.signal });
|
|
32
|
+
}
|
|
33
|
+
catch (cause) {
|
|
34
|
+
// A version that seeds CDP contexts already has our process-wide registration.
|
|
35
|
+
const duplicate = `"${name}" selector engine has been already registered`;
|
|
36
|
+
if (!(cause instanceof Error) || !cause.message.endsWith(duplicate))
|
|
37
|
+
throw cause;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Existing closed roots cannot be recovered if their document loaded without the tracking hook. */
|
|
42
|
+
export async function requireCdpShadowTracking(page, budget) {
|
|
43
|
+
if (budget.signal.aborted)
|
|
44
|
+
throw connectionAbort(budget.signal, 'CDP tracking verification');
|
|
45
|
+
const tracked = await Promise.all(page.frames().map((frame) => frame.evaluate((key) => Reflect.get(globalThis, Symbol.for(key)) instanceof WeakMap, CLOSED_SHADOW_ROOTS_KEY)));
|
|
46
|
+
if (budget.signal.aborted)
|
|
47
|
+
throw connectionAbort(budget.signal, 'CDP tracking verification');
|
|
48
|
+
if (tracked.some((present) => !present)) {
|
|
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
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=cdp-selectors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cdp-selectors.js","sourceRoot":"","sources":["../src/cdp-selectors.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closed shadow roots, kept reachable for the reader, the locators, and the
|
|
3
|
+
* screenshot masks: the context init script that records them, and the two
|
|
4
|
+
* Playwright selector engines that search them. Both engines are page code as
|
|
5
|
+
* source strings; the engine list is what the browser connection and the CDP
|
|
6
|
+
* context register.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Where the init script below records closed shadow roots, keyed by host, and
|
|
10
|
+
* where the in-page reader looks for them. The reader is serialized into the
|
|
11
|
+
* page and cannot import this constant, so the literal is repeated inside it;
|
|
12
|
+
* the two must agree, like `SECURE_FIELD_SELECTOR` on both sides of masking.
|
|
13
|
+
*
|
|
14
|
+
* The record is a `WeakMap` from host to root with one own property beside
|
|
15
|
+
* the map: `count`, the number of closed roots the document has attached. It
|
|
16
|
+
* lets a selector engine answer for a document without closed roots without
|
|
17
|
+
* walking it.
|
|
18
|
+
*/
|
|
19
|
+
export declare const CLOSED_SHADOW_ROOTS_KEY = "e2e.closedShadowRoots";
|
|
20
|
+
/**
|
|
21
|
+
* Context init script that keeps every closed shadow root reachable for the
|
|
22
|
+
* reader. A closed root hides its tree from `element.shadowRoot`, so a checkout
|
|
23
|
+
* button a third-party widget renders that way is on screen for a person yet
|
|
24
|
+
* absent from the walk. `attachShadow` is the one way a script creates such a
|
|
25
|
+
* root; wrapping it before any page script runs records host and root in a
|
|
26
|
+
* WeakMap under a well-known symbol, which the reader consults where it reads
|
|
27
|
+
* `shadowRoot`, and counts the roots attached so far. The map is keyed by the
|
|
28
|
+
* host element and never enumerated, so it holds nothing alive and changes
|
|
29
|
+
* nothing the page can observe about the root itself. Declarative
|
|
30
|
+
* `<template shadowrootmode="closed">` roots are parsed rather than attached
|
|
31
|
+
* and stay out of reach.
|
|
32
|
+
*/
|
|
33
|
+
export declare const CLOSED_SHADOW_ROOTS_INIT_SCRIPT: string;
|
|
34
|
+
/** Playwright selector engine name; `e2e-closed=<css>` matches inside recorded closed shadow roots. */
|
|
35
|
+
export declare const CLOSED_SHADOW_SELECTOR_ENGINE = "e2e-closed";
|
|
36
|
+
/**
|
|
37
|
+
* Playwright selector engine name for the roots a semantic query searches.
|
|
38
|
+
* `e2e-roots=` (an empty body) resolves to the query root followed by every
|
|
39
|
+
* closed shadow root recorded under it, so the Playwright query part that
|
|
40
|
+
* follows (`internal:role`, `internal:text`, `internal:label`, ...) runs its
|
|
41
|
+
* own matching in each of them; `e2e-roots=<css>` resolves to the elements the
|
|
42
|
+
* CSS selector matches in the query root and in every closed root under it.
|
|
43
|
+
*/
|
|
44
|
+
export declare const SEARCH_ROOTS_SELECTOR_ENGINE = "e2e-roots";
|
|
45
|
+
/**
|
|
46
|
+
* In-page helpers the selector engines below and the label engine
|
|
47
|
+
* (`label-selector.ts`) are built on, as source: the
|
|
48
|
+
* closed roots the init script recorded under a query root (the root's own,
|
|
49
|
+
* when it is a closed host, and every one below through open and closed
|
|
50
|
+
* roots alike), and the elements a CSS selector matches in one tree and the
|
|
51
|
+
* open roots below it. A closed root is searched only as an entry of the
|
|
52
|
+
* first list, never by recursion from the tree above it, so each element is
|
|
53
|
+
* reached from exactly one root. A document that has attached no closed root
|
|
54
|
+
* (the record's `count` is zero) has none to find, and the walk over every
|
|
55
|
+
* element is skipped; every locator resolution runs this, so a page without
|
|
56
|
+
* closed roots must pay nothing for the reach.
|
|
57
|
+
*/
|
|
58
|
+
export declare const CLOSED_SHADOW_HELPERS_SOURCE: string;
|
|
59
|
+
/** The two closed-root selector engines; `selector-engines.ts` lists them with the label engine for registration. */
|
|
60
|
+
export declare const CLOSED_SHADOW_SELECTOR_ENGINES: readonly {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly source: string;
|
|
63
|
+
}[];
|
|
64
|
+
//# sourceMappingURL=closed-shadow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"closed-shadow.d.ts","sourceRoot":"","sources":["../src/closed-shadow.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;AAE/D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,+BAA+B,QAetC,CAAC;AAEP,uGAAuG;AACvG,eAAO,MAAM,6BAA6B,eAAe,CAAC;AAE1D;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,cAAc,CAAC;AAExD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,4BAA4B,QAsBpC,CAAC;AA6CN,qHAAqH;AACrH,eAAO,MAAM,8BAA8B,EAAE,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAGvG,CAAC"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closed shadow roots, kept reachable for the reader, the locators, and the
|
|
3
|
+
* screenshot masks: the context init script that records them, and the two
|
|
4
|
+
* Playwright selector engines that search them. Both engines are page code as
|
|
5
|
+
* source strings; the engine list is what the browser connection and the CDP
|
|
6
|
+
* context register.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Where the init script below records closed shadow roots, keyed by host, and
|
|
10
|
+
* where the in-page reader looks for them. The reader is serialized into the
|
|
11
|
+
* page and cannot import this constant, so the literal is repeated inside it;
|
|
12
|
+
* the two must agree, like `SECURE_FIELD_SELECTOR` on both sides of masking.
|
|
13
|
+
*
|
|
14
|
+
* The record is a `WeakMap` from host to root with one own property beside
|
|
15
|
+
* the map: `count`, the number of closed roots the document has attached. It
|
|
16
|
+
* lets a selector engine answer for a document without closed roots without
|
|
17
|
+
* walking it.
|
|
18
|
+
*/
|
|
19
|
+
export const CLOSED_SHADOW_ROOTS_KEY = 'e2e.closedShadowRoots';
|
|
20
|
+
/**
|
|
21
|
+
* Context init script that keeps every closed shadow root reachable for the
|
|
22
|
+
* reader. A closed root hides its tree from `element.shadowRoot`, so a checkout
|
|
23
|
+
* button a third-party widget renders that way is on screen for a person yet
|
|
24
|
+
* absent from the walk. `attachShadow` is the one way a script creates such a
|
|
25
|
+
* root; wrapping it before any page script runs records host and root in a
|
|
26
|
+
* WeakMap under a well-known symbol, which the reader consults where it reads
|
|
27
|
+
* `shadowRoot`, and counts the roots attached so far. The map is keyed by the
|
|
28
|
+
* host element and never enumerated, so it holds nothing alive and changes
|
|
29
|
+
* nothing the page can observe about the root itself. Declarative
|
|
30
|
+
* `<template shadowrootmode="closed">` roots are parsed rather than attached
|
|
31
|
+
* and stay out of reach.
|
|
32
|
+
*/
|
|
33
|
+
export const CLOSED_SHADOW_ROOTS_INIT_SCRIPT = `(() => {
|
|
34
|
+
const key = Symbol.for(${JSON.stringify(CLOSED_SHADOW_ROOTS_KEY)});
|
|
35
|
+
if (Object.prototype.hasOwnProperty.call(globalThis, key)) return;
|
|
36
|
+
const roots = new WeakMap();
|
|
37
|
+
Object.defineProperty(roots, 'count', { value: 0, enumerable: false, configurable: false, writable: true });
|
|
38
|
+
Object.defineProperty(globalThis, key, { value: roots, enumerable: false, configurable: false, writable: false });
|
|
39
|
+
const attachShadow = Element.prototype.attachShadow;
|
|
40
|
+
Element.prototype.attachShadow = function (init) {
|
|
41
|
+
const root = attachShadow.call(this, init);
|
|
42
|
+
if (root.mode === 'closed') {
|
|
43
|
+
roots.set(this, root);
|
|
44
|
+
roots.count += 1;
|
|
45
|
+
}
|
|
46
|
+
return root;
|
|
47
|
+
};
|
|
48
|
+
})();`;
|
|
49
|
+
/** Playwright selector engine name; `e2e-closed=<css>` matches inside recorded closed shadow roots. */
|
|
50
|
+
export const CLOSED_SHADOW_SELECTOR_ENGINE = 'e2e-closed';
|
|
51
|
+
/**
|
|
52
|
+
* Playwright selector engine name for the roots a semantic query searches.
|
|
53
|
+
* `e2e-roots=` (an empty body) resolves to the query root followed by every
|
|
54
|
+
* closed shadow root recorded under it, so the Playwright query part that
|
|
55
|
+
* follows (`internal:role`, `internal:text`, `internal:label`, ...) runs its
|
|
56
|
+
* own matching in each of them; `e2e-roots=<css>` resolves to the elements the
|
|
57
|
+
* CSS selector matches in the query root and in every closed root under it.
|
|
58
|
+
*/
|
|
59
|
+
export const SEARCH_ROOTS_SELECTOR_ENGINE = 'e2e-roots';
|
|
60
|
+
/**
|
|
61
|
+
* In-page helpers the selector engines below and the label engine
|
|
62
|
+
* (`label-selector.ts`) are built on, as source: the
|
|
63
|
+
* closed roots the init script recorded under a query root (the root's own,
|
|
64
|
+
* when it is a closed host, and every one below through open and closed
|
|
65
|
+
* roots alike), and the elements a CSS selector matches in one tree and the
|
|
66
|
+
* open roots below it. A closed root is searched only as an entry of the
|
|
67
|
+
* first list, never by recursion from the tree above it, so each element is
|
|
68
|
+
* reached from exactly one root. A document that has attached no closed root
|
|
69
|
+
* (the record's `count` is zero) has none to find, and the walk over every
|
|
70
|
+
* element is skipped; every locator resolution runs this, so a page without
|
|
71
|
+
* closed roots must pay nothing for the reach.
|
|
72
|
+
*/
|
|
73
|
+
export const CLOSED_SHADOW_HELPERS_SOURCE = `
|
|
74
|
+
const roots = globalThis[Symbol.for(${JSON.stringify(CLOSED_SHADOW_ROOTS_KEY)})];
|
|
75
|
+
const tracked = roots instanceof WeakMap;
|
|
76
|
+
const closedRootsUnder = (root, out) => {
|
|
77
|
+
if (!tracked || roots.count === 0) return out;
|
|
78
|
+
const scan = (el) => {
|
|
79
|
+
const closed = roots.get(el);
|
|
80
|
+
if (closed !== undefined) out.push(closed);
|
|
81
|
+
const nested = el.shadowRoot ?? closed;
|
|
82
|
+
if (nested !== undefined && nested !== null) closedRootsUnder(nested, out);
|
|
83
|
+
};
|
|
84
|
+
if (root.nodeType === 1) scan(root);
|
|
85
|
+
for (const el of root.querySelectorAll('*')) scan(el);
|
|
86
|
+
return out;
|
|
87
|
+
};
|
|
88
|
+
const matchesIn = (root, selector, out) => {
|
|
89
|
+
for (const el of root.querySelectorAll(selector)) out.push(el);
|
|
90
|
+
if (root.nodeType === 1 && root.shadowRoot !== null) matchesIn(root.shadowRoot, selector, out);
|
|
91
|
+
for (const el of root.querySelectorAll('*')) {
|
|
92
|
+
if (el.shadowRoot !== null) matchesIn(el.shadowRoot, selector, out);
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
};`;
|
|
96
|
+
/**
|
|
97
|
+
* The engine behind `e2e-closed=<css>`: every element matching the CSS
|
|
98
|
+
* selector inside a closed shadow root the init script recorded, reached from
|
|
99
|
+
* the query root through light DOM and open roots, and through roots of either
|
|
100
|
+
* kind nested below a closed one. Playwright's own selectors stop at a closed
|
|
101
|
+
* root, so this is what lets a screenshot mask cover a secure field the reader
|
|
102
|
+
* now reports there. It runs in the page's main world (not as a content
|
|
103
|
+
* script) because that is where the record lives. A document without the
|
|
104
|
+
* record is refused: a mask that cannot prove its coverage must fail closed.
|
|
105
|
+
*/
|
|
106
|
+
const CLOSED_SHADOW_SELECTOR_ENGINE_SOURCE = `() => {${CLOSED_SHADOW_HELPERS_SOURCE}
|
|
107
|
+
const queryAll = (root, selector) => {
|
|
108
|
+
if (!tracked) throw new Error('closed-shadow root tracking is unavailable for this document');
|
|
109
|
+
const out = [];
|
|
110
|
+
for (const closed of closedRootsUnder(root, [])) matchesIn(closed, selector, out);
|
|
111
|
+
return out;
|
|
112
|
+
};
|
|
113
|
+
return { queryAll, query: (root, selector) => queryAll(root, selector)[0] ?? null };
|
|
114
|
+
}`;
|
|
115
|
+
/**
|
|
116
|
+
* The engine behind `e2e-roots=`: the roots a semantic query searches, or the
|
|
117
|
+
* elements a CSS selector matches across them. Playwright's own query engines
|
|
118
|
+
* accept a shadow root as their root and never cross a closed boundary
|
|
119
|
+
* downward, so listing every closed root as a root of its own lets them match
|
|
120
|
+
* inside with the same role, name, text, label, and attribute rules they apply
|
|
121
|
+
* outside, and finds each element exactly once. A CSS selector list is matched
|
|
122
|
+
* here rather than by Playwright's CSS engine, which sorts a list's matches in
|
|
123
|
+
* DOM order through `shadowRoot` and so drops the direct children of a closed
|
|
124
|
+
* root; the matches of one root keep their document order and roots follow in
|
|
125
|
+
* the order they were found. A document without the record answers as
|
|
126
|
+
* Playwright alone would, so a locator never fails where it used to work.
|
|
127
|
+
*/
|
|
128
|
+
const SEARCH_ROOTS_SELECTOR_ENGINE_SOURCE = `() => {${CLOSED_SHADOW_HELPERS_SOURCE}
|
|
129
|
+
const queryAll = (root, selector) => {
|
|
130
|
+
if (selector === '') return [root, ...closedRootsUnder(root, [])];
|
|
131
|
+
const out = matchesIn(root, selector, []);
|
|
132
|
+
for (const closed of closedRootsUnder(root, [])) matchesIn(closed, selector, out);
|
|
133
|
+
return out;
|
|
134
|
+
};
|
|
135
|
+
return { queryAll, query: (root, selector) => queryAll(root, selector)[0] ?? null };
|
|
136
|
+
}`;
|
|
137
|
+
/** The two closed-root selector engines; `selector-engines.ts` lists them with the label engine for registration. */
|
|
138
|
+
export const CLOSED_SHADOW_SELECTOR_ENGINES = [
|
|
139
|
+
{ name: CLOSED_SHADOW_SELECTOR_ENGINE, source: CLOSED_SHADOW_SELECTOR_ENGINE_SOURCE },
|
|
140
|
+
{ name: SEARCH_ROOTS_SELECTOR_ENGINE, source: SEARCH_ROOTS_SELECTOR_ENGINE_SOURCE },
|
|
141
|
+
];
|
|
142
|
+
//# sourceMappingURL=closed-shadow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"closed-shadow.js","sourceRoot":"","sources":["../src/closed-shadow.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,uBAAuB,CAAC;AAE/D;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG;2BACpB,IAAI,CAAC,SAAS,CAAC,uBAAuB,CAAC;;;;;;;;;;;;;;MAc5D,CAAC;AAEP,uGAAuG;AACvG,MAAM,CAAC,MAAM,6BAA6B,GAAG,YAAY,CAAC;AAE1D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG,WAAW,CAAC;AAExD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,4BAA4B,GAAG;wCACJ,IAAI,CAAC,SAAS,CAAC,uBAAuB,CAAC;;;;;;;;;;;;;;;;;;;;;KAqB1E,CAAC;AAEN;;;;;;;;;GASG;AACH,MAAM,oCAAoC,GAAG,UAAU,4BAA4B;;;;;;;;EAQjF,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,mCAAmC,GAAG,UAAU,4BAA4B;;;;;;;;EAQhF,CAAC;AAEH,qHAAqH;AACrH,MAAM,CAAC,MAAM,8BAA8B,GAAkE;IAC3G,EAAE,IAAI,EAAE,6BAA6B,EAAE,MAAM,EAAE,oCAAoC,EAAE;IACrF,EAAE,IAAI,EAAE,4BAA4B,EAAE,MAAM,EAAE,mCAAmC,EAAE;CACpF,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native dialog routing for one attempt. Handlers are registered by the `web`
|
|
3
|
+
* fixture; the surface owns the router so a dialog that fires before a test
|
|
4
|
+
* ever touches `web` is still accounted for instead of silently dismissed.
|
|
5
|
+
*/
|
|
6
|
+
import type { Dialog as PwDialog } from 'playwright';
|
|
7
|
+
import { ErrorLatch } from './support.ts';
|
|
8
|
+
/** A native dialog as a test's handler sees it. */
|
|
9
|
+
export interface Dialog {
|
|
10
|
+
/** The dialog's text. */
|
|
11
|
+
readonly message: string;
|
|
12
|
+
/** Accepts the dialog once. */
|
|
13
|
+
accept(text?: string): Promise<void>;
|
|
14
|
+
/** Dismisses the dialog once. */
|
|
15
|
+
dismiss(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export type DialogHandler = 'accept' | 'dismiss' | ((dialog: Dialog) => void | Promise<void>);
|
|
18
|
+
export declare class DialogRouter {
|
|
19
|
+
private readonly latch;
|
|
20
|
+
private registrations;
|
|
21
|
+
/**
|
|
22
|
+
* `latch` is shared with the surface: a dialog failure and a route-handler
|
|
23
|
+
* failure surface through one check at the next step.
|
|
24
|
+
*/
|
|
25
|
+
constructor(latch?: ErrorLatch);
|
|
26
|
+
/**
|
|
27
|
+
* Registers a handler (the newest wins) and returns its idempotent
|
|
28
|
+
* unsubscribe. Registrations are tracked by identity, so registering the
|
|
29
|
+
* same handler twice and unsubscribing once leaves one in place.
|
|
30
|
+
*/
|
|
31
|
+
add(handler: DialogHandler): () => void;
|
|
32
|
+
/** Rethrows an error latched by an unhandled or failing dialog handler. */
|
|
33
|
+
throwPending(): void;
|
|
34
|
+
/** Routes one native dialog to the newest registered handler; the attempt end waits for it. */
|
|
35
|
+
dispatch(dialog: PwDialog): Promise<void>;
|
|
36
|
+
private route;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=dialogs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialogs.d.ts","sourceRoot":"","sources":["../src/dialogs.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,IAAI,QAAQ,EAAE,MAAM,YAAY,CAAC;AAErD,OAAO,EAAE,UAAU,EAAyB,MAAM,cAAc,CAAC;AAEjE,mDAAmD;AACnD,MAAM,WAAW,MAAM;IACrB,yBAAyB;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,+BAA+B;IAC/B,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,iCAAiC;IACjC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,SAAS,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;AAM9F,qBAAa,YAAY;IAOX,OAAO,CAAC,QAAQ,CAAC,KAAK;IANlC,OAAO,CAAC,aAAa,CAAsB;IAE3C;;;OAGG;IACH,YAA6B,KAAK,GAAE,UAA6B,EAAI;IAErE;;;;OAIG;IACH,GAAG,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,IAAI,CAMtC;IAED,2EAA2E;IAC3E,YAAY,IAAI,IAAI,CAEnB;IAED,+FAA+F;IAC/F,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAIxC;YAEa,KAAK;CAyDpB"}
|
package/dist/dialogs.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native dialog routing for one attempt. Handlers are registered by the `web`
|
|
3
|
+
* fixture; the surface owns the router so a dialog that fires before a test
|
|
4
|
+
* ever touches `web` is still accounted for instead of silently dismissed.
|
|
5
|
+
*/
|
|
6
|
+
import { EngineError } from 'e2e/engine';
|
|
7
|
+
import { ErrorLatch, isClassified, message } from './support.js';
|
|
8
|
+
export class DialogRouter {
|
|
9
|
+
latch;
|
|
10
|
+
registrations = [];
|
|
11
|
+
/**
|
|
12
|
+
* `latch` is shared with the surface: a dialog failure and a route-handler
|
|
13
|
+
* failure surface through one check at the next step.
|
|
14
|
+
*/
|
|
15
|
+
constructor(latch = new ErrorLatch()) {
|
|
16
|
+
this.latch = latch;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Registers a handler (the newest wins) and returns its idempotent
|
|
20
|
+
* unsubscribe. Registrations are tracked by identity, so registering the
|
|
21
|
+
* same handler twice and unsubscribing once leaves one in place.
|
|
22
|
+
*/
|
|
23
|
+
add(handler) {
|
|
24
|
+
const registration = { handler };
|
|
25
|
+
this.registrations.push(registration);
|
|
26
|
+
return () => {
|
|
27
|
+
this.registrations = this.registrations.filter((entry) => entry !== registration);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Rethrows an error latched by an unhandled or failing dialog handler. */
|
|
31
|
+
throwPending() {
|
|
32
|
+
this.latch.throwPending();
|
|
33
|
+
}
|
|
34
|
+
/** Routes one native dialog to the newest registered handler; the attempt end waits for it. */
|
|
35
|
+
dispatch(dialog) {
|
|
36
|
+
const work = this.route(dialog);
|
|
37
|
+
this.latch.track('dialog', work);
|
|
38
|
+
return work;
|
|
39
|
+
}
|
|
40
|
+
async route(dialog) {
|
|
41
|
+
const handler = this.registrations.at(-1)?.handler;
|
|
42
|
+
if (handler === undefined) {
|
|
43
|
+
this.latch.latch(new EngineError('INVALID_STATE', `unhandled ${dialog.type()} dialog: ${dialog.message()}`, { retryable: false }));
|
|
44
|
+
await dialog.dismiss().catch(() => undefined);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let decided = false;
|
|
48
|
+
const publicDialog = {
|
|
49
|
+
message: dialog.message(),
|
|
50
|
+
accept: async (text) => {
|
|
51
|
+
decided = true;
|
|
52
|
+
await dialog.accept(text);
|
|
53
|
+
},
|
|
54
|
+
dismiss: async () => {
|
|
55
|
+
decided = true;
|
|
56
|
+
await dialog.dismiss();
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
if (handler === 'accept')
|
|
61
|
+
await dialog.accept();
|
|
62
|
+
else if (handler === 'dismiss')
|
|
63
|
+
await dialog.dismiss();
|
|
64
|
+
else {
|
|
65
|
+
await handler(publicDialog);
|
|
66
|
+
// A dialog left open blocks the page, and the failure would surface
|
|
67
|
+
// later as an unrelated action timeout. The handler contract is the
|
|
68
|
+
// same as a route handler's: decide, or the step fails naming you.
|
|
69
|
+
if (!decided) {
|
|
70
|
+
await dialog.dismiss().catch(() => undefined);
|
|
71
|
+
this.latch.latch(new EngineError('INVALID_STATE', `dialog handler returned without calling accept or dismiss for ${dialog.type()} dialog: ${dialog.message()}`, { retryable: false }));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (cause) {
|
|
76
|
+
// A failed assertion or a policy refusal keeps its code; only an
|
|
77
|
+
// unclassified throw is the engine's to name.
|
|
78
|
+
this.latch.latch(isClassified(cause)
|
|
79
|
+
? cause
|
|
80
|
+
: new EngineError('ENGINE_FAILURE', `dialog handler failed: ${message(cause)}`, {
|
|
81
|
+
retryable: false,
|
|
82
|
+
cause,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=dialogs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dialogs.js","sourceRoot":"","sources":["../src/dialogs.ts"],"names":[],"mappings":"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"}
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Playwright engine for e2e: a browser body built with the
|
|
3
|
+
* public `defineEngine`, validated by the same rules and graded by the same
|
|
4
|
+
* capabilities as any other engine. Core imports nothing from here; this
|
|
5
|
+
* package imports the contract from `e2e/engine` and contributes the `web`
|
|
6
|
+
* fixture the way a device engine contributes `device`.
|
|
7
|
+
*/
|
|
8
|
+
import type { BrowserContext, Page } from 'playwright';
|
|
9
|
+
import { type EngineHandle } from 'e2e/engine';
|
|
10
|
+
import { type WebOptions } from './surface.ts';
|
|
11
|
+
/**
|
|
12
|
+
* The live browser objects behind a `web()` handle, for agent-side code
|
|
13
|
+
* that replaces the toolset wholesale (spec chapter 16) and drives the page with
|
|
14
|
+
* its own Playwright tooling. Both accessors read the current attempt: the
|
|
15
|
+
* context exists from `startAttempt`, the page from the first `app.open()` or
|
|
16
|
+
* `web.goto()`, and either throws `INVALID_STATE` before that. The harness
|
|
17
|
+
* remains the notary for what it witnesses; a caller here acts out of band.
|
|
18
|
+
*/
|
|
19
|
+
export interface PlaywrightLiveSurface {
|
|
20
|
+
readonly page: () => Page;
|
|
21
|
+
readonly context: () => BrowserContext;
|
|
22
|
+
}
|
|
23
|
+
/** The live surface of a handle this module created, or undefined for any other engine. */
|
|
24
|
+
export declare function surfaceOf(engine: EngineHandle): PlaywrightLiveSurface | undefined;
|
|
25
|
+
export declare function web(options?: WebOptions): EngineHandle;
|
|
26
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAOL,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AAGpB,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,cAAc,CAAC;AAMlE;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,cAAc,CAAC;CACxC;AAED,2FAA2F;AAC3F,wBAAgB,SAAS,CAAC,MAAM,EAAE,YAAY,GAAG,qBAAqB,GAAG,SAAS,CAIjF;AAED,wBAAgB,GAAG,CAAC,OAAO,GAAE,UAAe,GAAG,YAAY,CAyF1D"}
|