@e2e-dev/web 0.14.1-nightly-20261013094930 → 0.15.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 +3 -1
- package/dist/.build.json +1 -1
- package/dist/attempt-session.d.ts +28 -0
- package/dist/attempt-session.js +58 -0
- package/dist/browser.d.ts +10 -0
- package/dist/browser.js +31 -3
- package/dist/cdp-selectors.d.ts +1 -1
- package/dist/cdp-selectors.js +1 -1
- package/dist/engine.js +16 -0
- package/dist/in-page/read-semantics.js +3 -0
- package/dist/index.d.ts +1 -0
- package/dist/media.d.ts +22 -0
- package/dist/media.js +45 -0
- package/dist/surface.d.ts +20 -0
- package/dist/surface.js +11 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -42,7 +42,9 @@ target, so a test can `requires: ['browser']`. Browser options:
|
|
|
42
42
|
`getByTestId` and a node's `testId` read; default `data-testid`), `userAgent`
|
|
43
43
|
(the `User-Agent` every attempt sends and `navigator.userAgent` reports),
|
|
44
44
|
`locale` and `timezoneId` (the language and time zone every attempt runs in,
|
|
45
|
-
such as `de-DE` and `Europe/Berlin`), `
|
|
45
|
+
such as `de-DE` and `Europe/Berlin`), `colorScheme` and `reducedMotion` (the
|
|
46
|
+
`prefers-color-scheme` and `prefers-reduced-motion` every page reports, from
|
|
47
|
+
first paint; `browser.emulateMedia` changes them in a test), `initScripts` (scripts every document
|
|
46
48
|
runs before the page's own), and
|
|
47
49
|
`connect` — attach to a remote browser over CDP instead of launching a local
|
|
48
50
|
one.
|
package/dist/.build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"0.
|
|
1
|
+
{"version":"0.15.0"}
|
|
@@ -5,6 +5,7 @@ import { type CdpEndpointResolver, type SessionBinding } from './cdp-recovery.ts
|
|
|
5
5
|
import { type OperationBound } from './operation-budget.ts';
|
|
6
6
|
import { RefRegistry } from './refs.ts';
|
|
7
7
|
import type { LeaseRecording } from './provider.ts';
|
|
8
|
+
import type { WebMedia } from './media.ts';
|
|
8
9
|
import type { WebScreencastOptions } from './surface.ts';
|
|
9
10
|
export type StorageState = Exclude<NonNullable<BrowserContextOptions['storageState']>, string>;
|
|
10
11
|
interface SessionOptions {
|
|
@@ -13,6 +14,8 @@ interface SessionOptions {
|
|
|
13
14
|
readonly viewport: ViewportSize | null;
|
|
14
15
|
/** How the engine's own screencast records, when the provider does not record. */
|
|
15
16
|
readonly screencast: WebScreencastOptions;
|
|
17
|
+
/** The media features every page the attempt opens emulates, until `emulateMedia` changes them. */
|
|
18
|
+
readonly media: WebMedia;
|
|
16
19
|
readonly contextOptions: BrowserContextOptions;
|
|
17
20
|
readonly acquire: (signal: AbortSignal) => Promise<Browser>;
|
|
18
21
|
readonly configure: (context: BrowserContext) => Promise<void>;
|
|
@@ -41,6 +44,9 @@ export declare class AttemptSession {
|
|
|
41
44
|
/** Until the attempt's first page opens: a persistent browser's own first tab can serve as that page. */
|
|
42
45
|
private firstPage;
|
|
43
46
|
private requestedViewport;
|
|
47
|
+
private media;
|
|
48
|
+
/** The emulation each page got as it appeared, so the attempt's own page is ready before it shows. */
|
|
49
|
+
private readonly emulated;
|
|
44
50
|
constructor(options: SessionOptions);
|
|
45
51
|
/** Captures an immutable connection generation for publication after asynchronous reads. */
|
|
46
52
|
token(): object;
|
|
@@ -76,6 +82,28 @@ export declare class AttemptSession {
|
|
|
76
82
|
readonly width: number;
|
|
77
83
|
readonly height: number;
|
|
78
84
|
}): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Emulates media features on every open tab and every tab opened after
|
|
87
|
+
* them, the app's own included; a feature `media` leaves out keeps its value.
|
|
88
|
+
*/
|
|
89
|
+
emulateMedia(media: WebMedia): Promise<void>;
|
|
90
|
+
/**
|
|
91
|
+
* Runs `emulate` on every open tab but the attempt's own. A tab the app
|
|
92
|
+
* closes meanwhile fails its own call only; an error on a tab still open
|
|
93
|
+
* propagates.
|
|
94
|
+
*/
|
|
95
|
+
private emulateOnOthers;
|
|
96
|
+
/** Sets the attempt's media features on a page before it shows anything; a page with none set keeps the browser's own. */
|
|
97
|
+
private applyMedia;
|
|
98
|
+
/**
|
|
99
|
+
* Emulates the attempt's media on each tab as it appears, so a tab the app
|
|
100
|
+
* opens itself follows a test's change too, on a persistent context as
|
|
101
|
+
* well, where the engine sets no context options. It paints with the
|
|
102
|
+
* context's configured values first: Playwright emulates per page, so a
|
|
103
|
+
* test's change reaches it once it exists. A tab the attempt opens awaits
|
|
104
|
+
* the same work in `ensurePage`; a tab the app opens is not awaited.
|
|
105
|
+
*/
|
|
106
|
+
private watchPages;
|
|
79
107
|
/** Restarts the document while retaining this context's storage. */
|
|
80
108
|
restart(): Promise<void>;
|
|
81
109
|
/** Replaces an ordinary context; persistent recovery never advertises this capability. */
|
package/dist/attempt-session.js
CHANGED
|
@@ -16,8 +16,12 @@ export class AttemptSession {
|
|
|
16
16
|
/** Until the attempt's first page opens: a persistent browser's own first tab can serve as that page. */
|
|
17
17
|
firstPage = true;
|
|
18
18
|
requestedViewport;
|
|
19
|
+
media;
|
|
20
|
+
/** The emulation each page got as it appeared, so the attempt's own page is ready before it shows. */
|
|
21
|
+
emulated = new WeakMap();
|
|
19
22
|
constructor(options) {
|
|
20
23
|
this.options = options;
|
|
24
|
+
this.media = options.media;
|
|
21
25
|
this.video = options.record === undefined
|
|
22
26
|
? new VideoRecorder(options.artifactsDir, options.screencast)
|
|
23
27
|
: new ProviderVideo(options.record, options.artifactsDir);
|
|
@@ -121,10 +125,14 @@ export class AttemptSession {
|
|
|
121
125
|
if (budget.signal.aborted)
|
|
122
126
|
throw connectionAbort(budget.signal, 'connection');
|
|
123
127
|
await this.options.configure(candidate.context);
|
|
128
|
+
this.watchPages(candidate.context);
|
|
124
129
|
if (recover && candidate.page !== null) {
|
|
125
130
|
const size = previous?.page?.viewportSize() ?? this.requestedViewport ?? this.options.viewport;
|
|
126
131
|
if (size !== null)
|
|
127
132
|
await candidate.page.setViewportSize(size);
|
|
133
|
+
// Emulation belongs to the CDP session that set it, so the new transport sets it again, on the app's own tabs too.
|
|
134
|
+
await this.applyMedia(candidate.page);
|
|
135
|
+
await this.emulateOnOthers(candidate.context, candidate.page, (page) => this.applyMedia(page));
|
|
128
136
|
await this.video.pageOpened(candidate.page);
|
|
129
137
|
}
|
|
130
138
|
if (budget.signal.aborted)
|
|
@@ -164,6 +172,7 @@ export class AttemptSession {
|
|
|
164
172
|
const size = this.requestedViewport ?? (identity === undefined ? null : this.options.viewport);
|
|
165
173
|
if (size !== null)
|
|
166
174
|
await page.setViewportSize(size);
|
|
175
|
+
await (this.emulated.get(page) ?? this.applyMedia(page));
|
|
167
176
|
if (identity !== undefined)
|
|
168
177
|
identity = { ...identity, target: await targetIdentity(page) };
|
|
169
178
|
this.check(token);
|
|
@@ -208,6 +217,55 @@ export class AttemptSession {
|
|
|
208
217
|
await page.setViewportSize({ width, height });
|
|
209
218
|
this.requestedViewport = { width, height };
|
|
210
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* Emulates media features on every open tab and every tab opened after
|
|
222
|
+
* them, the app's own included; a feature `media` leaves out keeps its value.
|
|
223
|
+
*/
|
|
224
|
+
async emulateMedia(media) {
|
|
225
|
+
// Merged before the await, so a second call made meanwhile builds on this one instead of the old value.
|
|
226
|
+
this.media = { ...this.media, ...media };
|
|
227
|
+
const { context, page } = this.current();
|
|
228
|
+
if (page !== null && !page.isClosed())
|
|
229
|
+
await page.emulateMedia(this.media);
|
|
230
|
+
await this.emulateOnOthers(context, page, (other) => other.emulateMedia(this.media));
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Runs `emulate` on every open tab but the attempt's own. A tab the app
|
|
234
|
+
* closes meanwhile fails its own call only; an error on a tab still open
|
|
235
|
+
* propagates.
|
|
236
|
+
*/
|
|
237
|
+
async emulateOnOthers(context, own, emulate) {
|
|
238
|
+
const others = context.pages().filter((page) => page !== own && !page.isClosed());
|
|
239
|
+
await Promise.all(others.map(async (page) => {
|
|
240
|
+
try {
|
|
241
|
+
await emulate(page);
|
|
242
|
+
}
|
|
243
|
+
catch (cause) {
|
|
244
|
+
if (!page.isClosed())
|
|
245
|
+
throw cause;
|
|
246
|
+
}
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
/** Sets the attempt's media features on a page before it shows anything; a page with none set keeps the browser's own. */
|
|
250
|
+
async applyMedia(page) {
|
|
251
|
+
if (Object.keys(this.media).length > 0)
|
|
252
|
+
await page.emulateMedia(this.media);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Emulates the attempt's media on each tab as it appears, so a tab the app
|
|
256
|
+
* opens itself follows a test's change too, on a persistent context as
|
|
257
|
+
* well, where the engine sets no context options. It paints with the
|
|
258
|
+
* context's configured values first: Playwright emulates per page, so a
|
|
259
|
+
* test's change reaches it once it exists. A tab the attempt opens awaits
|
|
260
|
+
* the same work in `ensurePage`; a tab the app opens is not awaited.
|
|
261
|
+
*/
|
|
262
|
+
watchPages(context) {
|
|
263
|
+
context.on('page', (page) => {
|
|
264
|
+
const applied = this.applyMedia(page);
|
|
265
|
+
this.emulated.set(page, applied);
|
|
266
|
+
applied.catch(() => undefined);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
211
269
|
/** Restarts the document while retaining this context's storage. */
|
|
212
270
|
async restart() {
|
|
213
271
|
const binding = this.current();
|
package/dist/browser.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import type { ActionOptions, Expectable, JsonValue, Locator, Screen, TextMatch } from 'e2e';
|
|
15
15
|
import { type EngineFixtureContext } from 'e2e/engine';
|
|
16
16
|
import type { DialogHandler } from './dialogs.ts';
|
|
17
|
+
import { type WebMedia } from './media.ts';
|
|
17
18
|
import type { PlaywrightSurface } from './surface.ts';
|
|
18
19
|
/**
|
|
19
20
|
* One of `json`, `body`, or `path`, or none for an empty body. `path` names a
|
|
@@ -200,6 +201,14 @@ export interface Browser extends Expectable<BrowserExpectation> {
|
|
|
200
201
|
width: number;
|
|
201
202
|
height: number;
|
|
202
203
|
}): Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* Emulates CSS media features on every open tab and every tab opened after
|
|
206
|
+
* them, the app's own included, for the rest of the attempt, as Playwright's
|
|
207
|
+
* `page.emulateMedia` does: the page's `@media` rules and `matchMedia` both
|
|
208
|
+
* read them. A feature left out keeps its value. Works before the first
|
|
209
|
+
* navigation, so the page loads with them.
|
|
210
|
+
*/
|
|
211
|
+
emulateMedia(media: WebMedia): Promise<void>;
|
|
203
212
|
/** Registers an attempt-scoped dialog handler and returns an unsubscribe function. */
|
|
204
213
|
onDialog(handler: DialogHandler): Promise<() => Promise<void>>;
|
|
205
214
|
/** Runs a trigger and waits for its download. */
|
|
@@ -207,6 +216,7 @@ export interface Browser extends Expectable<BrowserExpectation> {
|
|
|
207
216
|
timeout?: number;
|
|
208
217
|
}): Promise<{
|
|
209
218
|
path: string;
|
|
219
|
+
absolutePath: string;
|
|
210
220
|
suggestedFilename: string;
|
|
211
221
|
}>;
|
|
212
222
|
/** Viewport-level keyboard, for whatever has focus. */
|
package/dist/browser.js
CHANGED
|
@@ -11,12 +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 { ConfigurationError, Deadline, describePattern, EngineError, matchesText, pollCondition, raceAbort, rejectUnknownOptions, TestError, toTextPattern, urlMatches, validateJsonValue, withTimeout, } from 'e2e/engine';
|
|
14
|
+
import { ConfigurationError, Deadline, describePattern, EngineError, matchesText, obj, pollCondition, raceAbort, rejectUnknownOptions, TestError, toTextPattern, urlMatches, validateJsonValue, withTimeout, } from 'e2e/engine';
|
|
15
15
|
import { classifyInputError } from './actions.js';
|
|
16
16
|
import { saveDownloadsTo, saveFromBrowser, saveLocally } from './downloads.js';
|
|
17
17
|
import { isTestErrorCode, message as causeMessage, translatePwError } from './support.js';
|
|
18
18
|
import { compileEvaluation, pageFunctionSource } from './evaluation.js';
|
|
19
19
|
import { initScriptLabel, testInitScriptSource } from './init-scripts.js';
|
|
20
|
+
import { MEDIA_KEYS, mediaLabel, mediaProblem } from './media.js';
|
|
20
21
|
import { lowercaseNames } from './protected-app.js';
|
|
21
22
|
import { parseContinue, parseFulfill, requireNoArguments } from './route-options.js';
|
|
22
23
|
import { routePatternMatches, routePatternsEqual } from './route-pattern.js';
|
|
@@ -286,6 +287,20 @@ export function createBrowserFixture(surface, context) {
|
|
|
286
287
|
await surface.setViewport(size);
|
|
287
288
|
context.attachViewport({ width: size.width, height: size.height, scale: 1 });
|
|
288
289
|
}),
|
|
290
|
+
async emulateMedia(media) {
|
|
291
|
+
if (typeof media !== 'object' || media === null) {
|
|
292
|
+
throw new TestError('INVALID_ARGUMENT', 'browser.emulateMedia takes { colorScheme?, reducedMotion? }');
|
|
293
|
+
}
|
|
294
|
+
rejectUnknownOptions('browser.emulateMedia', media, MEDIA_KEYS);
|
|
295
|
+
for (const key of MEDIA_KEYS) {
|
|
296
|
+
const problem = mediaProblem((name) => `browser.emulateMedia({ ${name} })`, key, media[key]);
|
|
297
|
+
if (problem !== undefined)
|
|
298
|
+
throw new TestError('INVALID_ARGUMENT', problem);
|
|
299
|
+
}
|
|
300
|
+
// Read once, so a test that changes its object later changes nothing here.
|
|
301
|
+
const requested = obj({ colorScheme: media.colorScheme, reducedMotion: media.reducedMotion });
|
|
302
|
+
await surface.guard(context.operation(), 'emulateMedia', () => surface.emulateMedia(requested));
|
|
303
|
+
},
|
|
289
304
|
// Async so the harness records the registration as a `browser.onDialog` step.
|
|
290
305
|
async onDialog(handler) {
|
|
291
306
|
const unsubscribe = surface.dialogs.add(handler);
|
|
@@ -321,7 +336,7 @@ export function createBrowserFixture(surface, context) {
|
|
|
321
336
|
else
|
|
322
337
|
await saveFromBrowser(download, remote, absolute, currentOperation.signal);
|
|
323
338
|
context.attachArtifact('download', relative);
|
|
324
|
-
return { path: relative, suggestedFilename };
|
|
339
|
+
return { path: relative, absolutePath: absolute, suggestedFilename };
|
|
325
340
|
}, (cause, label) => {
|
|
326
341
|
// The trigger is test code, so its errors keep their original classification.
|
|
327
342
|
if (triggerFailure !== undefined && Object.is(cause, triggerFailure.cause))
|
|
@@ -369,6 +384,7 @@ export function createBrowserFixture(surface, context) {
|
|
|
369
384
|
cookies: action,
|
|
370
385
|
setCookies: action,
|
|
371
386
|
setViewport: action,
|
|
387
|
+
emulateMedia: { ...action, label: (media) => mediaLabel(media) },
|
|
372
388
|
onDialog: action,
|
|
373
389
|
waitForDownload: { ...action, timeout: (_trigger, options) => options?.timeout },
|
|
374
390
|
keyboard: { press: { ...action, label: (key) => key }, type: { ...action, label: (text) => `${text.length} chars` } },
|
|
@@ -430,13 +446,15 @@ function createBrowserExpectation(deps, negated = false) {
|
|
|
430
446
|
let value;
|
|
431
447
|
try {
|
|
432
448
|
// A locator read takes the action timeout; the poll's deadline is the tighter bound.
|
|
433
|
-
value = await withTimeout(target
|
|
449
|
+
value = await withTimeout(readClass(target), deadline.remaining(), () => new EngineError('OPERATION_TIMEOUT', 'class read timed out', { retryable: false }));
|
|
434
450
|
}
|
|
435
451
|
catch (error) {
|
|
436
452
|
if (isTestErrorCode(error, 'LOCATOR_NOT_FOUND'))
|
|
437
453
|
return { matches: undefined, observed: 'no node' };
|
|
438
454
|
throw error;
|
|
439
455
|
}
|
|
456
|
+
if (value === undefined)
|
|
457
|
+
return { matches: undefined, observed: 'no node' };
|
|
440
458
|
if (value === null)
|
|
441
459
|
return { matches: false, observed: 'no class attribute' };
|
|
442
460
|
return {
|
|
@@ -447,6 +465,16 @@ function createBrowserExpectation(deps, negated = false) {
|
|
|
447
465
|
},
|
|
448
466
|
};
|
|
449
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* The class attribute of one match, `undefined` while nothing matches. A
|
|
470
|
+
* read waits for its match until the action timeout, so the poll samples
|
|
471
|
+
* absence first: a node that never arrives stays an assertion failure.
|
|
472
|
+
*/
|
|
473
|
+
async function readClass(target) {
|
|
474
|
+
if ((await target.count()) === 0)
|
|
475
|
+
return undefined;
|
|
476
|
+
return target.getAttribute('class');
|
|
477
|
+
}
|
|
450
478
|
function requireSelector(method, selector) {
|
|
451
479
|
if (typeof selector !== 'string' || selector.length === 0) {
|
|
452
480
|
throw new TestError('INVALID_LOCATOR', `${method}() requires a nonempty selector`);
|
package/dist/cdp-selectors.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { BrowserContext, Page } from 'playwright-core';
|
|
3
3
|
import { type ConnectionBudget } from './operation-budget.ts';
|
|
4
4
|
/**
|
|
5
|
-
* Playwright 1.
|
|
5
|
+
* Playwright 1.64 seeds custom selectors only in newly created contexts, not
|
|
6
6
|
* the persistent context returned by connectOverCDP. Its public register API
|
|
7
7
|
* rejects an already registered name before reaching that context. Use the
|
|
8
8
|
* same context channel as selectors.register, without allocating global names
|
package/dist/cdp-selectors.js
CHANGED
|
@@ -4,7 +4,7 @@ import { connectionAbort } from './operation-budget.js';
|
|
|
4
4
|
import { CLOSED_SHADOW_ROOTS_KEY } from './closed-shadow.js';
|
|
5
5
|
import { SELECTOR_ENGINES } from './selector-engines.js';
|
|
6
6
|
/**
|
|
7
|
-
* Playwright 1.
|
|
7
|
+
* Playwright 1.64 seeds custom selectors only in newly created contexts, not
|
|
8
8
|
* the persistent context returned by connectOverCDP. Its public register API
|
|
9
9
|
* rejects an already registered name before reaching that context. Use the
|
|
10
10
|
* same context channel as selectors.register, without allocating global names
|
package/dist/engine.js
CHANGED
|
@@ -11,6 +11,7 @@ import { asBrowserProvider } from './provider.js';
|
|
|
11
11
|
import { PlaywrightSurface } from './surface.js';
|
|
12
12
|
import { createBrowserFixture } from './browser.js';
|
|
13
13
|
import { validateInitScripts } from './init-scripts.js';
|
|
14
|
+
import { MEDIA_KEYS, mediaProblem } from './media.js';
|
|
14
15
|
import { FIELD_VALUE_CONTROL, headerProblem } from './protected-app.js';
|
|
15
16
|
/** App fields only a device target reads. */
|
|
16
17
|
const NATIVE_APP_KEYS = ['bundleId', 'appPath', 'launchArguments', 'permissions'];
|
|
@@ -52,6 +53,7 @@ export function web(options = {}) {
|
|
|
52
53
|
validateLocale(options.locale, options.headers);
|
|
53
54
|
if (options.timezoneId !== undefined)
|
|
54
55
|
validateTimezoneId(options.timezoneId);
|
|
56
|
+
validateMedia(options);
|
|
55
57
|
if (options.initScripts !== undefined)
|
|
56
58
|
validateInitScripts(options.initScripts);
|
|
57
59
|
if (options.screencast !== undefined)
|
|
@@ -134,6 +136,8 @@ const WEB_OPTION_KEYS = Object.keys({
|
|
|
134
136
|
userAgent: true,
|
|
135
137
|
locale: true,
|
|
136
138
|
timezoneId: true,
|
|
139
|
+
colorScheme: true,
|
|
140
|
+
reducedMotion: true,
|
|
137
141
|
initScripts: true,
|
|
138
142
|
});
|
|
139
143
|
/** The options applied when the engine creates a browser context, which a persistent context never is. */
|
|
@@ -238,6 +242,18 @@ function validateTimezoneId(timezoneId) {
|
|
|
238
242
|
throw new ConfigurationError('INVALID_CONFIG', `web({ timezoneId }) must be an IANA time zone such as "Europe/Berlin", got ${JSON.stringify(timezoneId)}`);
|
|
239
243
|
}
|
|
240
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Refuses a media feature value the browser does not emulate, at config load
|
|
247
|
+
* rather than at the first page. A persistent context takes these too: they
|
|
248
|
+
* are set on each page, not only on a context the engine creates.
|
|
249
|
+
*/
|
|
250
|
+
function validateMedia(options) {
|
|
251
|
+
for (const key of MEDIA_KEYS) {
|
|
252
|
+
const problem = mediaProblem((name) => `web({ ${name} })`, key, options[key]);
|
|
253
|
+
if (problem !== undefined)
|
|
254
|
+
throw new ConfigurationError('INVALID_CONFIG', problem);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
241
257
|
/**
|
|
242
258
|
* True when `timeZone` is an IANA name `Intl` resolves, spelled with its own
|
|
243
259
|
* case. Node also takes an offset (`+01:00`) and any casing (`europe/berlin`),
|
|
@@ -38,6 +38,9 @@ export const readSemanticsFunction = (element, options) => {
|
|
|
38
38
|
'frameset',
|
|
39
39
|
'object',
|
|
40
40
|
'embed',
|
|
41
|
+
// Next.js App Router's route announcer echoes the title on a hydration
|
|
42
|
+
// schedule of its own, which an observation must not read as an alert.
|
|
43
|
+
'next-route-announcer',
|
|
41
44
|
];
|
|
42
45
|
const OPAQUE_TAGS = ['svg', 'math', 'canvas', 'video', 'audio'];
|
|
43
46
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export { web, surfaceOf } from './engine.ts';
|
|
|
9
9
|
export type { PlaywrightLiveSurface } from './engine.ts';
|
|
10
10
|
export type { WebBasicAuth, WebConnectOptions, WebOptions, WebScreencastOptions } from './surface.ts';
|
|
11
11
|
export type { WebInitScript } from './init-scripts.ts';
|
|
12
|
+
export type { WebMedia } from './media.ts';
|
|
12
13
|
export type { BrowserDownloadContext, BrowserLease, BrowserProvider, BrowserProviderDownloads, BrowserProviderScope, BrowserReleaseContext, BrowserRequest, } from './provider.ts';
|
|
13
14
|
export type { BrowserName } from './browser-connection.ts';
|
|
14
15
|
export type { Dialog, DialogHandler } from './dialogs.ts';
|
package/dist/media.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CSS media features the engine emulates: one set of values from
|
|
3
|
+
* `web({ colorScheme, reducedMotion })` for every attempt, and the same names
|
|
4
|
+
* on `browser.emulateMedia()` for the rest of one.
|
|
5
|
+
*/
|
|
6
|
+
/** User preferences a page reads through `@media` rules and `matchMedia`, as Playwright's `emulateMedia` sets them. */
|
|
7
|
+
export interface WebMedia {
|
|
8
|
+
/** `prefers-color-scheme`: `'light'`, `'dark'`, or `'no-preference'`. */
|
|
9
|
+
readonly colorScheme?: 'light' | 'dark' | 'no-preference';
|
|
10
|
+
/** `prefers-reduced-motion`: `'reduce'` or `'no-preference'`. */
|
|
11
|
+
readonly reducedMotion?: 'reduce' | 'no-preference';
|
|
12
|
+
}
|
|
13
|
+
/** The feature names `WebMedia` takes, for unknown-key checks. */
|
|
14
|
+
export declare const MEDIA_KEYS: readonly (keyof WebMedia)[];
|
|
15
|
+
/**
|
|
16
|
+
* Why one feature's value is refused, naming it the way `where` does; undefined
|
|
17
|
+
* for a value the browser emulates, and for an unset one. Config and tests run
|
|
18
|
+
* as JavaScript, so the types alone are no guard.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mediaProblem(where: (key: keyof WebMedia) => string, key: keyof WebMedia, value: unknown): string | undefined;
|
|
21
|
+
/** The step label of a `browser.emulateMedia` call: the features it sets, read defensively since the label runs before validation. */
|
|
22
|
+
export declare function mediaLabel(media: unknown): string;
|
package/dist/media.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CSS media features the engine emulates: one set of values from
|
|
3
|
+
* `web({ colorScheme, reducedMotion })` for every attempt, and the same names
|
|
4
|
+
* on `browser.emulateMedia()` for the rest of one.
|
|
5
|
+
*/
|
|
6
|
+
/** Every emulated feature and the values it takes, kept equal to `WebMedia` by the compiler. */
|
|
7
|
+
const MEDIA_VALUES = {
|
|
8
|
+
colorScheme: ['light', 'dark', 'no-preference'],
|
|
9
|
+
reducedMotion: ['reduce', 'no-preference'],
|
|
10
|
+
};
|
|
11
|
+
/** The feature names `WebMedia` takes, for unknown-key checks. */
|
|
12
|
+
export const MEDIA_KEYS = Object.keys(MEDIA_VALUES);
|
|
13
|
+
/**
|
|
14
|
+
* Why one feature's value is refused, naming it the way `where` does; undefined
|
|
15
|
+
* for a value the browser emulates, and for an unset one. Config and tests run
|
|
16
|
+
* as JavaScript, so the types alone are no guard.
|
|
17
|
+
*/
|
|
18
|
+
export function mediaProblem(where, key, value) {
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
const allowed = MEDIA_VALUES[key];
|
|
22
|
+
if (typeof value === 'string' && allowed.includes(value))
|
|
23
|
+
return undefined;
|
|
24
|
+
const names = allowed.map((name) => JSON.stringify(name));
|
|
25
|
+
return `${where(key)} must be ${names.slice(0, -1).join(', ')}${names.length > 2 ? ',' : ''} or ${names.at(-1)}, got ${describe(value)}`;
|
|
26
|
+
}
|
|
27
|
+
/** The step label of a `browser.emulateMedia` call: the features it sets, read defensively since the label runs before validation. */
|
|
28
|
+
export function mediaLabel(media) {
|
|
29
|
+
if (typeof media !== 'object' || media === null)
|
|
30
|
+
return '';
|
|
31
|
+
return MEDIA_KEYS.flatMap((key) => {
|
|
32
|
+
const value = Reflect.get(media, key);
|
|
33
|
+
return typeof value === 'string' && mediaProblem(String, key, value) === undefined ? [`${key}: ${value}`] : [];
|
|
34
|
+
}).join(', ');
|
|
35
|
+
}
|
|
36
|
+
/** A refused value as a message shows it: a string quoted, another primitive as written, anything else by its kind, never serialized. */
|
|
37
|
+
function describe(value) {
|
|
38
|
+
if (typeof value === 'string')
|
|
39
|
+
return JSON.stringify(value);
|
|
40
|
+
if (value === null || typeof value === 'number' || typeof value === 'boolean')
|
|
41
|
+
return String(value);
|
|
42
|
+
if (Array.isArray(value))
|
|
43
|
+
return 'an array';
|
|
44
|
+
return typeof value === 'object' ? 'an object' : typeof value;
|
|
45
|
+
}
|
package/dist/surface.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { type BrowserProvider, type LeaseDownloads } from './provider.ts';
|
|
|
13
13
|
import { DialogRouter } from './dialogs.ts';
|
|
14
14
|
import { type OperationBound } from './operation-budget.ts';
|
|
15
15
|
import { type WebInitScript } from './init-scripts.ts';
|
|
16
|
+
import type { WebMedia } from './media.ts';
|
|
16
17
|
import { ErrorLatch } from './support.ts';
|
|
17
18
|
type RoutePredicate = (url: URL) => boolean;
|
|
18
19
|
type RouteHandler = (route: Route) => Promise<void>;
|
|
@@ -149,6 +150,21 @@ export interface WebOptions {
|
|
|
149
150
|
* provider with `scope: 'attempt'`) is `INVALID_CONFIG`.
|
|
150
151
|
*/
|
|
151
152
|
readonly timezoneId?: string;
|
|
153
|
+
/**
|
|
154
|
+
* The `prefers-color-scheme` every page reports from its first paint, to
|
|
155
|
+
* its `@media` rules and to `matchMedia`, as Playwright's own
|
|
156
|
+
* `colorScheme` context option sets it. Defaults to the browser's own.
|
|
157
|
+
* `browser.emulateMedia` changes it for the rest of one attempt.
|
|
158
|
+
*/
|
|
159
|
+
readonly colorScheme?: WebMedia['colorScheme'];
|
|
160
|
+
/**
|
|
161
|
+
* The `prefers-reduced-motion` every page reports from its first paint, as
|
|
162
|
+
* Playwright's own `reducedMotion` context option sets it: `'reduce'`
|
|
163
|
+
* lets an app's own rule turn its animations off, so none races an
|
|
164
|
+
* assertion. Defaults to the browser's own. `browser.emulateMedia`
|
|
165
|
+
* changes it for the rest of one attempt.
|
|
166
|
+
*/
|
|
167
|
+
readonly reducedMotion?: WebMedia['reducedMotion'];
|
|
152
168
|
/**
|
|
153
169
|
* Scripts every document runs before the page's own, in every tab and
|
|
154
170
|
* frame: JavaScript source, a `{ path }` relative to the project root, or a
|
|
@@ -179,6 +195,8 @@ export declare class PlaywrightSurface {
|
|
|
179
195
|
private readonly userAgent;
|
|
180
196
|
private readonly locale;
|
|
181
197
|
private readonly timezoneId;
|
|
198
|
+
/** The configured media features, which every attempt starts from. */
|
|
199
|
+
private readonly media;
|
|
182
200
|
private readonly configuredInitScripts;
|
|
183
201
|
/** The attempt's init scripts, configured then added, applied to each context the attempt opens. */
|
|
184
202
|
private initScripts;
|
|
@@ -303,6 +321,8 @@ export declare class PlaywrightSurface {
|
|
|
303
321
|
readonly width: number;
|
|
304
322
|
readonly height: number;
|
|
305
323
|
}): Promise<void>;
|
|
324
|
+
/** Emulates media features on the open page, or the one the next navigation opens, for the rest of the attempt. */
|
|
325
|
+
emulateMedia(media: WebMedia): Promise<void>;
|
|
306
326
|
/**
|
|
307
327
|
* The single entry of every operation: rethrows an error latched on an
|
|
308
328
|
* unawaited path, refuses a cancelled operation, races `fn` against the
|
package/dist/surface.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { mkdirSync } from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
-
import { EngineError, raceAbort, TestError, withinCleanupBudget, } from 'e2e/engine';
|
|
11
|
+
import { EngineError, obj, raceAbort, TestError, withinCleanupBudget, } from 'e2e/engine';
|
|
12
12
|
import { matchesText } from 'e2e/engine';
|
|
13
13
|
import { classifyActionError, classifyInputError, dispatchLocatorAction, dispatchPointerAction } from './actions.js';
|
|
14
14
|
import { BrowserConnection, connectCdp } from './browser-connection.js';
|
|
@@ -67,6 +67,8 @@ export class PlaywrightSurface {
|
|
|
67
67
|
userAgent;
|
|
68
68
|
locale;
|
|
69
69
|
timezoneId;
|
|
70
|
+
/** The configured media features, which every attempt starts from. */
|
|
71
|
+
media;
|
|
70
72
|
configuredInitScripts;
|
|
71
73
|
/** The attempt's init scripts, configured then added, applied to each context the attempt opens. */
|
|
72
74
|
initScripts = [];
|
|
@@ -98,6 +100,7 @@ export class PlaywrightSurface {
|
|
|
98
100
|
this.userAgent = options.userAgent;
|
|
99
101
|
this.locale = options.locale;
|
|
100
102
|
this.timezoneId = options.timezoneId;
|
|
103
|
+
this.media = obj({ colorScheme: options.colorScheme, reducedMotion: options.reducedMotion });
|
|
101
104
|
this.configuredInitScripts = new ConfiguredInitScripts(options.initScripts ?? []);
|
|
102
105
|
}
|
|
103
106
|
// --- lifecycle ---
|
|
@@ -253,6 +256,7 @@ export class PlaywrightSurface {
|
|
|
253
256
|
artifactsDir: context.artifactsDir,
|
|
254
257
|
viewport: this.viewport,
|
|
255
258
|
screencast: this.screencast,
|
|
259
|
+
media: this.media,
|
|
256
260
|
acquire: (signal) => this.acquireBrowser(signal),
|
|
257
261
|
contextOptions: {
|
|
258
262
|
viewport: this.viewport,
|
|
@@ -261,6 +265,8 @@ export class PlaywrightSurface {
|
|
|
261
265
|
...(this.userAgent === undefined ? {} : { userAgent: this.userAgent }),
|
|
262
266
|
...(this.locale === undefined ? {} : { locale: this.locale }),
|
|
263
267
|
...(this.timezoneId === undefined ? {} : { timezoneId: this.timezoneId }),
|
|
268
|
+
// On the context too, so a tab the app opens itself paints with the configured values before the session reaches it.
|
|
269
|
+
...this.media,
|
|
264
270
|
...(this.headers === undefined ? {} : { serviceWorkers: 'block' }),
|
|
265
271
|
},
|
|
266
272
|
configure: async (target) => {
|
|
@@ -417,6 +423,10 @@ export class PlaywrightSurface {
|
|
|
417
423
|
setViewport(size) {
|
|
418
424
|
return this.requireSession().setViewport(size);
|
|
419
425
|
}
|
|
426
|
+
/** Emulates media features on the open page, or the one the next navigation opens, for the rest of the attempt. */
|
|
427
|
+
emulateMedia(media) {
|
|
428
|
+
return this.requireSession().emulateMedia(media);
|
|
429
|
+
}
|
|
420
430
|
/**
|
|
421
431
|
* The single entry of every operation: rethrows an error latched on an
|
|
422
432
|
* unawaited path, refuses a cancelled operation, races `fn` against the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e2e-dev/web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Web engine for e2e agentic end-to-end testing, built on Playwright",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"e2e",
|
|
@@ -47,14 +47,14 @@
|
|
|
47
47
|
"NOTICE"
|
|
48
48
|
],
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"playwright-core": "1.
|
|
50
|
+
"playwright-core": "1.64.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"e2e": "0.
|
|
53
|
+
"e2e": ">=0.18.0 <1"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "26.6.2",
|
|
57
|
-
"e2e": "0.21.0
|
|
57
|
+
"e2e": "0.21.0",
|
|
58
58
|
"@types/pngjs": "6.0.5",
|
|
59
59
|
"pngjs": "7.0.0",
|
|
60
60
|
"typescript": "7.0.2",
|