chore: remove bindings and init scripts upon client disconnect (#36064)
infra / docs & lint (push) Waiting to run Details
infra / Lint snippets (push) Waiting to run Details
components / ${{ matrix.os }} - Node.js ${{ matrix.node-version }} (18, macos-latest) (push) Waiting to run Details
components / ${{ matrix.os }} - Node.js ${{ matrix.node-version }} (18, ubuntu-latest) (push) Waiting to run Details
components / ${{ matrix.os }} - Node.js ${{ matrix.node-version }} (18, windows-latest) (push) Waiting to run Details
components / ${{ matrix.os }} - Node.js ${{ matrix.node-version }} (20, ubuntu-latest) (push) Waiting to run Details
components / ${{ matrix.os }} - Node.js ${{ matrix.node-version }} (22, ubuntu-latest) (push) Waiting to run Details
tests others / Stress - ${{ matrix.os }} (macos-latest) (push) Waiting to run Details
tests others / Stress - ${{ matrix.os }} (ubuntu-latest) (push) Waiting to run Details
tests others / Stress - ${{ matrix.os }} (windows-latest) (push) Waiting to run Details
tests others / WebView2 (push) Waiting to run Details
tests others / time library - ${{ matrix.clock }} (frozen) (push) Waiting to run Details
tests others / time library - ${{ matrix.clock }} (realtime) (push) Waiting to run Details
tests others / time test runner - ${{ matrix.clock }} (frozen) (push) Waiting to run Details
tests others / time test runner - ${{ matrix.clock }} (realtime) (push) Waiting to run Details
tests others / Electron - ${{ matrix.os }} (macos-latest) (push) Waiting to run Details
tests others / Electron - ${{ matrix.os }} (ubuntu-latest) (push) Waiting to run Details
tests others / Electron - ${{ matrix.os }} (windows-latest) (push) Waiting to run Details
tests 1 / ${{ matrix.os }} (${{ matrix.browser }} - Node.js ${{ matrix.node-version }}) (chromium, 18, ubuntu-22.04) (push) Waiting to run Details
tests 1 / ${{ matrix.os }} (${{ matrix.browser }} - Node.js ${{ matrix.node-version }}) (chromium, 20, ubuntu-22.04) (push) Waiting to run Details
tests 1 / ${{ matrix.os }} (${{ matrix.browser }} - Node.js ${{ matrix.node-version }}) (chromium, 22, ubuntu-22.04) (push) Waiting to run Details
tests 1 / ${{ matrix.os }} (${{ matrix.browser }} - Node.js ${{ matrix.node-version }}) (firefox, 18, ubuntu-22.04) (push) Waiting to run Details
tests 1 / ${{ matrix.os }} (${{ matrix.browser }} - Node.js ${{ matrix.node-version }}) (webkit, 18, ubuntu-22.04) (push) Waiting to run Details
tests 1 / ${{ matrix.os }} (chromium tip-of-tree) (ubuntu-22.04) (push) Waiting to run Details
tests 1 / Test Runner (18, macos-latest, 1, 2) (push) Waiting to run Details
tests 1 / Test Runner (18, macos-latest, 2, 2) (push) Waiting to run Details
tests 1 / Test Runner (18, ubuntu-latest, 1, 2) (push) Waiting to run Details
tests 1 / Test Runner (18, ubuntu-latest, 2, 2) (push) Waiting to run Details
tests 1 / Test Runner (18, windows-latest, 1, 2) (push) Waiting to run Details
tests 1 / Test Runner (18, windows-latest, 2, 2) (push) Waiting to run Details
tests 1 / Test Runner (20, ubuntu-latest, 1, 2) (push) Waiting to run Details
tests 1 / Test Runner (20, ubuntu-latest, 2, 2) (push) Waiting to run Details
tests 1 / Test Runner (22, ubuntu-latest, 1, 2) (push) Waiting to run Details
tests 1 / Test Runner (22, ubuntu-latest, 2, 2) (push) Waiting to run Details
tests 1 / Web Components (push) Waiting to run Details
tests 1 / VSCode Extension (push) Waiting to run Details
tests 1 / Installation Test ${{ matrix.os }} (macos-latest) (push) Waiting to run Details
tests 1 / Installation Test ${{ matrix.os }} (ubuntu-latest) (push) Waiting to run Details
tests 1 / Installation Test ${{ matrix.os }} (windows-latest) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (chromium, ubuntu-24.04) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (firefox, ubuntu-24.04) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (webkit, ubuntu-24.04) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (chromium, macos-13-large) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (chromium, macos-13-xlarge) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (chromium, macos-14-large) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (chromium, macos-14-xlarge) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (firefox, macos-13-large) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (firefox, macos-13-xlarge) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (firefox, macos-14-large) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (firefox, macos-14-xlarge) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (webkit, macos-13-large) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (webkit, macos-13-xlarge) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (webkit, macos-14-large) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (webkit, macos-14-xlarge) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (webkit, macos-15-large) (push) Waiting to run Details
tests 2 / ${{ matrix.os }} (${{ matrix.browser }}) (webkit, macos-15-xlarge) (push) Waiting to run Details
tests 2 / Windows (chromium) (push) Waiting to run Details
tests 2 / Windows (firefox) (push) Waiting to run Details
tests 2 / Windows (webkit) (push) Waiting to run Details
tests 2 / Installation Test ${{ matrix.os }} (${{ matrix.node_version }}) (20, ubuntu-latest) (push) Waiting to run Details
tests 2 / Installation Test ${{ matrix.os }} (${{ matrix.node_version }}) (22, ubuntu-latest) (push) Waiting to run Details
tests 2 / headed ${{ matrix.browser }} (${{ matrix.os }}) (chromium, macos-14-xlarge) (push) Waiting to run Details
tests 2 / headed ${{ matrix.browser }} (${{ matrix.os }}) (chromium, ubuntu-24.04) (push) Waiting to run Details
tests 2 / headed ${{ matrix.browser }} (${{ matrix.os }}) (chromium, windows-latest) (push) Waiting to run Details
tests 2 / headed ${{ matrix.browser }} (${{ matrix.os }}) (firefox, macos-14-xlarge) (push) Waiting to run Details
tests 2 / headed ${{ matrix.browser }} (${{ matrix.os }}) (firefox, ubuntu-24.04) (push) Waiting to run Details
tests 2 / headed ${{ matrix.browser }} (${{ matrix.os }}) (firefox, windows-latest) (push) Waiting to run Details
tests 2 / headed ${{ matrix.browser }} (${{ matrix.os }}) (webkit, macos-14-xlarge) (push) Waiting to run Details
tests 2 / headed ${{ matrix.browser }} (${{ matrix.os }}) (webkit, ubuntu-22.04) (push) Waiting to run Details
tests 2 / headed ${{ matrix.browser }} (${{ matrix.os }}) (webkit, ubuntu-24.04) (push) Waiting to run Details
tests 2 / headed ${{ matrix.browser }} (${{ matrix.os }}) (webkit, windows-latest) (push) Waiting to run Details
tests 2 / Transport (driver) (push) Waiting to run Details
tests 2 / Transport (service) (push) Waiting to run Details
tests 2 / Tracing ${{ matrix.browser }} ${{ matrix.channel }} (chromium) (push) Waiting to run Details
tests 2 / Tracing ${{ matrix.browser }} ${{ matrix.channel }} (chromium, chromium-tip-of-tree) (push) Waiting to run Details
tests 2 / Tracing ${{ matrix.browser }} ${{ matrix.channel }} (firefox) (push) Waiting to run Details
tests 2 / Tracing ${{ matrix.browser }} ${{ matrix.channel }} (webkit) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (chrome, macos-latest) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (chrome, ubuntu-22.04) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (chrome, windows-latest) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (chrome-beta, macos-latest) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (chrome-beta, ubuntu-22.04) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (chrome-beta, windows-latest) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (msedge, macos-latest) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (msedge, ubuntu-22.04) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (msedge, windows-latest) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (msedge-beta, macos-latest) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (msedge-beta, ubuntu-22.04) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (msedge-beta, windows-latest) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (msedge-dev, macos-latest) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (msedge-dev, ubuntu-22.04) (push) Waiting to run Details
tests 2 / Test ${{ matrix.channel }} on ${{ matrix.runs-on }} (msedge-dev, windows-latest) (push) Waiting to run Details
tests 2 / Chromium tip-of-tree ${{ matrix.os }}${{ matrix.headed }} (, macos-13) (push) Waiting to run Details
tests 2 / Chromium tip-of-tree ${{ matrix.os }}${{ matrix.headed }} (, windows-latest) (push) Waiting to run Details
tests 2 / Chromium tip-of-tree ${{ matrix.os }}${{ matrix.headed }} (--headed, macos-13) (push) Waiting to run Details
tests 2 / Chromium tip-of-tree ${{ matrix.os }}${{ matrix.headed }} (--headed, ubuntu-22.04) (push) Waiting to run Details
tests 2 / Chromium tip-of-tree ${{ matrix.os }}${{ matrix.headed }} (--headed, windows-latest) (push) Waiting to run Details
tests 2 / Chromium tip-of-tree headless-shell-${{ matrix.os }} (ubuntu-22.04) (push) Waiting to run Details
tests 2 / Firefox Beta ${{ matrix.os }} (macos-latest) (push) Waiting to run Details
tests 2 / Firefox Beta ${{ matrix.os }} (ubuntu-22.04) (push) Waiting to run Details
tests 2 / Firefox Beta ${{ matrix.os }} (windows-latest) (push) Waiting to run Details
tests 2 / build-playwright-driver (push) Waiting to run Details
tests 2 / Test channel=chromium (macos-latest) (push) Waiting to run Details
tests 2 / Test channel=chromium (ubuntu-latest) (push) Waiting to run Details
tests 2 / Test channel=chromium (windows-latest) (push) Waiting to run Details
tests Video / Video Linux (chromium, ubuntu-22.04) (push) Waiting to run Details
tests Video / Video Linux (chromium, ubuntu-24.04) (push) Waiting to run Details
tests Video / Video Linux (firefox, ubuntu-22.04) (push) Waiting to run Details
tests Video / Video Linux (firefox, ubuntu-24.04) (push) Waiting to run Details
tests Video / Video Linux (webkit, ubuntu-22.04) (push) Waiting to run Details
tests Video / Video Linux (webkit, ubuntu-24.04) (push) Waiting to run Details
Internal Tests / trigger (push) Waiting to run Details

This commit is contained in:
Dmitry Gozman 2025-05-26 09:33:00 +00:00 committed by GitHub
parent 926c02735e
commit d1eb9589f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 252 additions and 109 deletions

View File

@ -28,6 +28,7 @@ type BindingData = {
callbacks: Map<number, { resolve: (value: any) => void, reject: (error: Error) => void }>;
lastSeq: number;
handles: Map<number, any>;
removed: boolean;
};
export class BindingsController {
@ -47,9 +48,12 @@ export class BindingsController {
callbacks: new Map(),
lastSeq: 0,
handles: new Map(),
removed: false,
};
this._bindings.set(bindingName, data);
(this._global as any)[bindingName] = (...args: any[]) => {
if (data.removed)
throw new Error(`binding "${bindingName}" has been removed`);
if (needsHandle && args.slice(1).some(arg => arg !== undefined))
throw new Error(`exposeBindingHandle supports a single argument, ${args.length} received`);
const seq = ++data.lastSeq;
@ -72,6 +76,14 @@ export class BindingsController {
};
}
removeBinding(bindingName: string) {
const data = this._bindings.get(bindingName);
if (data)
data.removed = true;
this._bindings.delete(bindingName);
delete (this._global as any)[bindingName];
}
takeBindingHandle(arg: { name: string, seq: number }) {
const handles = this._bindings.get(arg.name)!.handles;
const handle = handles.get(arg.seq);

View File

@ -207,7 +207,6 @@ export class BidiBrowser extends Browser {
export class BidiBrowserContext extends BrowserContext {
declare readonly _browser: BidiBrowser;
private _initScriptIds: bidi.Script.PreloadScript[] = [];
private _originToPermissions = new Map<string, string[]>();
private _blockingPageCreations: Set<Promise<unknown>> = new Set();
@ -372,14 +371,11 @@ export class BidiBrowserContext extends BrowserContext {
functionDeclaration: `() => { return ${initScript.source} }`,
userContexts: [this._browserContextId || 'default'],
});
if (!initScript.internal)
this._initScriptIds.push(script);
initScript.auxData = script;
}
async doRemoveNonInternalInitScripts() {
const promise = Promise.all(this._initScriptIds.map(script => this._browser._browserSession.send('script.removePreloadScript', { script })));
this._initScriptIds = [];
await promise;
async doRemoveInitScripts(initScripts: InitScript[]) {
await Promise.all(initScripts.map(script => this._browser._browserSession.send('script.removePreloadScript', { script: script.auxData })));
}
async doUpdateRequestInterception(): Promise<void> {

View File

@ -51,7 +51,6 @@ export class BidiPage implements PageDelegate {
readonly _browserContext: BidiBrowserContext;
readonly _networkManager: BidiNetworkManager;
private readonly _pdf: BidiPDF;
private _initScriptIds: bidi.Script.PreloadScript[] = [];
constructor(browserContext: BidiBrowserContext, bidiSession: BidiSession, opener: BidiPage | null) {
this._session = bidiSession;
@ -343,14 +342,11 @@ export class BidiPage implements PageDelegate {
// TODO: push to iframes?
contexts: [this._session.sessionId],
});
if (!initScript.internal)
this._initScriptIds.push(script);
initScript.auxData = script;
}
async removeNonInternalInitScripts() {
const promises = this._initScriptIds.map(script => this._session.send('script.removePreloadScript', { script }));
this._initScriptIds = [];
await Promise.all(promises);
async removeInitScripts(initScripts: InitScript[]): Promise<void> {
await Promise.all(initScripts.map(script => this._session.send('script.removePreloadScript', { script: script.auxData })));
}
async closePage(runBeforeUnload: boolean): Promise<void> {

View File

@ -189,7 +189,7 @@ export abstract class BrowserContext extends SdkObject {
}
async resetForReuse(metadata: CallMetadata, params: channels.BrowserNewContextForReuseParams | null) {
this.tracing.resetForReuse();
await this.tracing.resetForReuse();
if (params) {
for (const key of paramsThatAllowContextReuse)
@ -218,9 +218,7 @@ export abstract class BrowserContext extends SdkObject {
page?.frameManager.setCloseAllOpeningDialogs(false);
await this._resetStorage();
await this._removeExposedBindings();
await this._removeInitScripts();
this.clock.markAsUninstalled();
await this.clock.resetForReuse();
// TODO: following can be optimized to not perform noops.
if (this._options.permissions)
await this.grantPermissions(this._options.permissions);
@ -276,7 +274,7 @@ export abstract class BrowserContext extends SdkObject {
protected abstract doClearPermissions(): Promise<void>;
protected abstract doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void>;
protected abstract doAddInitScript(initScript: InitScript): Promise<void>;
protected abstract doRemoveNonInternalInitScripts(): Promise<void>;
protected abstract doRemoveInitScripts(initScripts: InitScript[]): Promise<void>;
protected abstract doUpdateRequestInterception(): Promise<void>;
protected abstract doExposePlaywrightBinding(): Promise<void>;
protected abstract doClose(reason: string | undefined): Promise<void>;
@ -335,7 +333,7 @@ export abstract class BrowserContext extends SdkObject {
return this._playwrightBindingExposed;
}
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource): Promise<void> {
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource): Promise<PageBinding> {
if (this._pageBindings.has(name))
throw new Error(`Function "${name}" has been already registered`);
for (const page of this.pages()) {
@ -347,13 +345,16 @@ export abstract class BrowserContext extends SdkObject {
this._pageBindings.set(name, binding);
await this.doAddInitScript(binding.initScript);
await this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main');
return binding;
}
async _removeExposedBindings() {
for (const [key, binding] of this._pageBindings) {
if (!binding.internal)
this._pageBindings.delete(key);
}
async removeExposedBindings(bindings: PageBinding[]) {
bindings = bindings.filter(binding => this._pageBindings.get(binding.name) === binding);
for (const binding of bindings)
this._pageBindings.delete(binding.name);
await this.doRemoveInitScripts(bindings.map(binding => binding.initScript));
const cleanup = bindings.map(binding => `{ ${binding.cleanupScript} };\n`).join('');
await this.safeNonStallingEvaluateInAllFrames(cleanup, 'main');
}
async grantPermissions(permissions: string[], origin?: string) {
@ -428,14 +429,16 @@ export abstract class BrowserContext extends SdkObject {
}
async addInitScript(source: string, name?: string) {
const initScript = new InitScript(source, false /* internal */, name);
const initScript = new InitScript(source, name);
this.initScripts.push(initScript);
await this.doAddInitScript(initScript);
return initScript;
}
async _removeInitScripts(): Promise<void> {
this.initScripts = this.initScripts.filter(script => script.internal);
await this.doRemoveNonInternalInitScripts();
async removeInitScripts(initScripts: InitScript[]) {
const set = new Set(initScripts);
this.initScripts = this.initScripts.filter(script => !set.has(script));
await this.doRemoveInitScripts(initScripts);
}
async setRequestInterceptor(handler: network.RouteHandler | undefined): Promise<void> {

View File

@ -476,9 +476,9 @@ export class CRBrowserContext extends BrowserContext {
await (page.delegate as CRPage).addInitScript(initScript);
}
async doRemoveNonInternalInitScripts() {
async doRemoveInitScripts(initScripts: InitScript[]) {
for (const page of this.pages())
await (page.delegate as CRPage).removeNonInternalInitScripts();
await (page.delegate as CRPage).removeInitScripts(initScripts);
}
async doUpdateRequestInterception(): Promise<void> {

View File

@ -240,8 +240,8 @@ export class CRPage implements PageDelegate {
await this._forAllFrameSessions(frame => frame.exposePlaywrightBinding());
}
async removeNonInternalInitScripts() {
await this._forAllFrameSessions(frame => frame._removeEvaluatesOnNewDocument());
async removeInitScripts(initScripts: InitScript[]): Promise<void> {
await this._forAllFrameSessions(frame => frame._removeEvaluatesOnNewDocument(initScripts));
}
async closePage(runBeforeUnload: boolean): Promise<void> {
@ -392,7 +392,6 @@ class FrameSession {
private _videoRecorder: VideoRecorder | null = null;
private _screencastId: string | null = null;
private _screencastClients = new Set<any>();
private _evaluateOnNewDocumentIdentifiers: string[] = [];
private _metricsOverride: Protocol.Emulation.setDeviceMetricsOverrideParameters | undefined;
private _workerSessions = new Map<string, CRSession>();
@ -1059,14 +1058,11 @@ class FrameSession {
async _evaluateOnNewDocument(initScript: InitScript, world: types.World, runImmediately?: boolean): Promise<void> {
const worldName = world === 'utility' ? this._crPage.utilityWorldName : undefined;
const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: initScript.source, worldName, runImmediately });
if (!initScript.internal)
this._evaluateOnNewDocumentIdentifiers.push(identifier);
initScript.auxData = identifier;
}
async _removeEvaluatesOnNewDocument(): Promise<void> {
const identifiers = this._evaluateOnNewDocumentIdentifiers;
this._evaluateOnNewDocumentIdentifiers = [];
await Promise.all(identifiers.map(identifier => this._client.send('Page.removeScriptToEvaluateOnNewDocument', { identifier })));
async _removeEvaluatesOnNewDocument(initScripts: InitScript[]): Promise<void> {
await Promise.all(initScripts.map(script => this._client.send('Page.removeScriptToEvaluateOnNewDocument', { identifier: script.auxData }).catch(() => {}))); // target can be closed
}
async exposePlaywrightBinding() {

View File

@ -17,77 +17,78 @@
import * as rawClockSource from '../generated/clockSource';
import type { BrowserContext } from './browserContext';
import type { InitScript } from './page';
export class Clock {
private _browserContext: BrowserContext;
private _scriptInstalled = false;
private _initScripts: InitScript[] = [];
constructor(browserContext: BrowserContext) {
this._browserContext = browserContext;
}
markAsUninstalled() {
this._scriptInstalled = false;
async resetForReuse() {
await this._browserContext.removeInitScripts(this._initScripts);
this._initScripts = [];
}
async fastForward(ticks: number | string) {
await this._installIfNeeded();
const ticksMillis = parseTicks(ticks);
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForward', ${Date.now()}, ${ticksMillis})`);
this._initScripts.push(await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('fastForward', ${Date.now()}, ${ticksMillis})`));
await this._evaluateInFrames(`globalThis.__pwClock.controller.fastForward(${ticksMillis})`);
}
async install(time: number | string | undefined) {
await this._installIfNeeded();
const timeMillis = time !== undefined ? parseTime(time) : Date.now();
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('install', ${Date.now()}, ${timeMillis})`);
this._initScripts.push(await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('install', ${Date.now()}, ${timeMillis})`));
await this._evaluateInFrames(`globalThis.__pwClock.controller.install(${timeMillis})`);
}
async pauseAt(ticks: number | string) {
await this._installIfNeeded();
const timeMillis = parseTime(ticks);
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('pauseAt', ${Date.now()}, ${timeMillis})`);
this._initScripts.push(await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('pauseAt', ${Date.now()}, ${timeMillis})`));
await this._evaluateInFrames(`globalThis.__pwClock.controller.pauseAt(${timeMillis})`);
}
async resume() {
await this._installIfNeeded();
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('resume', ${Date.now()})`);
this._initScripts.push(await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('resume', ${Date.now()})`));
await this._evaluateInFrames(`globalThis.__pwClock.controller.resume()`);
}
async setFixedTime(time: string | number) {
await this._installIfNeeded();
const timeMillis = parseTime(time);
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setFixedTime', ${Date.now()}, ${timeMillis})`);
this._initScripts.push(await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setFixedTime', ${Date.now()}, ${timeMillis})`));
await this._evaluateInFrames(`globalThis.__pwClock.controller.setFixedTime(${timeMillis})`);
}
async setSystemTime(time: string | number) {
await this._installIfNeeded();
const timeMillis = parseTime(time);
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setSystemTime', ${Date.now()}, ${timeMillis})`);
this._initScripts.push(await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('setSystemTime', ${Date.now()}, ${timeMillis})`));
await this._evaluateInFrames(`globalThis.__pwClock.controller.setSystemTime(${timeMillis})`);
}
async runFor(ticks: number | string) {
await this._installIfNeeded();
const ticksMillis = parseTicks(ticks);
await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('runFor', ${Date.now()}, ${ticksMillis})`);
this._initScripts.push(await this._browserContext.addInitScript(`globalThis.__pwClock.controller.log('runFor', ${Date.now()}, ${ticksMillis})`));
await this._evaluateInFrames(`globalThis.__pwClock.controller.runFor(${ticksMillis})`);
}
private async _installIfNeeded() {
if (this._scriptInstalled)
if (this._initScripts.length)
return;
this._scriptInstalled = true;
const script = `(() => {
const module = {};
${rawClockSource.source}
globalThis.__pwClock = (module.exports.inject())(globalThis);
})();`;
await this._browserContext.addInitScript(script);
this._initScripts.push(await this._browserContext.addInitScript(script));
await this._evaluateInFrames(script);
}

View File

@ -40,7 +40,7 @@ import type { ConsoleMessage } from '../console';
import type { Dialog } from '../dialog';
import type { CallMetadata } from '../instrumentation';
import type { Request, Response } from '../network';
import type { Page } from '../page';
import type { InitScript, Page, PageBinding } from '../page';
import type { DispatcherScope } from './dispatcher';
import type { FrameDispatcher } from './frameDispatcher';
import type * as channels from '@protocol/channels';
@ -51,6 +51,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
private _context: BrowserContext;
private _subscriptions = new Set<channels.BrowserContextUpdateSubscriptionParams['event']>();
_webSocketInterceptionPatterns: channels.BrowserContextSetWebSocketInterceptionPatternsParams['patterns'] = [];
private _bindings: PageBinding[] = [];
private _initScritps: InitScript[] = [];
static from(parentScope: DispatcherScope, context: BrowserContext): BrowserContextDispatcher {
const result = parentScope.connection.existingDispatcher<BrowserContextDispatcher>(context);
@ -206,7 +208,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
}
async exposeBinding(params: channels.BrowserContextExposeBindingParams): Promise<void> {
await this._context.exposeBinding(params.name, !!params.needsHandle, (source, ...args) => {
const binding = await this._context.exposeBinding(params.name, !!params.needsHandle, (source, ...args) => {
// When reusing the context, we might have some bindings called late enough,
// after context and page dispatchers have been disposed.
if (this._disposed)
@ -216,6 +218,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
this._dispatchEvent('bindingCall', { binding });
return binding.promise();
});
this._bindings.push(binding);
}
async newPage(params: channels.BrowserContextNewPageParams, metadata: CallMetadata): Promise<channels.BrowserContextNewPageResult> {
@ -266,7 +269,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
}
async addInitScript(params: channels.BrowserContextAddInitScriptParams): Promise<void> {
await this._context.addInitScript(params.source);
this._initScritps.push(await this._context.addInitScript(params.source));
}
async setNetworkInterceptionPatterns(params: channels.BrowserContextSetNetworkInterceptionPatternsParams): Promise<void> {
@ -373,7 +376,12 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
override _onDispose() {
// Avoid protocol calls for the closed context.
if (!this._context.isClosingOrClosed())
this._context.setRequestInterceptor(undefined).catch(() => {});
if (this._context.isClosingOrClosed())
return;
this._context.setRequestInterceptor(undefined).catch(() => {});
this._context.removeExposedBindings(this._bindings).catch(() => {});
this._bindings = [];
this._context.removeInitScripts(this._initScritps).catch(() => {});
this._initScritps = [];
}
}

View File

@ -37,6 +37,7 @@ import type { CallMetadata } from '../instrumentation';
import type { JSHandle } from '../javascript';
import type { BrowserContextDispatcher } from './browserContextDispatcher';
import type { Frame } from '../frames';
import type { InitScript, PageBinding } from '../page';
import type * as channels from '@protocol/channels';
export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, BrowserContextDispatcher> implements channels.PageChannel {
@ -45,6 +46,8 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
private _page: Page;
_subscriptions = new Set<channels.PageUpdateSubscriptionParams['event']>();
_webSocketInterceptionPatterns: channels.PageSetWebSocketInterceptionPatternsParams['patterns'] = [];
private _bindings: PageBinding[] = [];
private _initScripts: InitScript[] = [];
static from(parentScope: BrowserContextDispatcher, page: Page): PageDispatcher {
return PageDispatcher.fromNullable(parentScope, page)!;
@ -107,7 +110,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
}
async exposeBinding(params: channels.PageExposeBindingParams, metadata: CallMetadata): Promise<void> {
await this._page.exposeBinding(params.name, !!params.needsHandle, (source, ...args) => {
const binding = await this._page.exposeBinding(params.name, !!params.needsHandle, (source, ...args) => {
// When reusing the context, we might have some bindings called late enough,
// after context and page dispatchers have been disposed.
if (this._disposed)
@ -116,6 +119,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
this._dispatchEvent('bindingCall', { binding });
return binding.promise();
});
this._bindings.push(binding);
}
async setExtraHTTPHeaders(params: channels.PageSetExtraHTTPHeadersParams, metadata: CallMetadata): Promise<void> {
@ -166,7 +170,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
}
async addInitScript(params: channels.PageAddInitScriptParams, metadata: CallMetadata): Promise<void> {
await this._page.addInitScript(params.source);
this._initScripts.push(await this._page.addInitScript(params.source));
}
async setNetworkInterceptionPatterns(params: channels.PageSetNetworkInterceptionPatternsParams, metadata: CallMetadata): Promise<void> {
@ -326,8 +330,13 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
override _onDispose() {
// Avoid protocol calls for the closed page.
if (!this._page.isClosedOrClosingOrCrashed())
this._page.setClientRequestInterceptor(undefined).catch(() => {});
if (this._page.isClosedOrClosingOrCrashed())
return;
this._page.setClientRequestInterceptor(undefined).catch(() => {});
this._page.removeExposedBindings(this._bindings).catch(() => {});
this._bindings = [];
this._page.removeInitScripts(this._initScripts).catch(() => {});
this._initScripts = [];
}
}

View File

@ -373,7 +373,7 @@ export class FFBrowserContext extends BrowserContext {
await this._updateInitScripts();
}
async doRemoveNonInternalInitScripts() {
async doRemoveInitScripts(initScripts: InitScript[]) {
await this._updateInitScripts();
}

View File

@ -108,7 +108,7 @@ export class FFPage implements PageDelegate {
});
// Ideally, we somehow ensure that utility world is created before Page.ready arrives, but currently it is racy.
// Therefore, we can end up with an initialized page without utility world, although very unlikely.
this.addInitScript(new InitScript('', true), UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
this.addInitScript(new InitScript(''), UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
}
async _markAsError(error: Error) {
@ -387,11 +387,16 @@ export class FFPage implements PageDelegate {
async addInitScript(initScript: InitScript, worldName?: string): Promise<void> {
this._initScripts.push({ initScript, worldName });
await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) });
await this._updateInitScripts();
}
async removeNonInternalInitScripts() {
this._initScripts = this._initScripts.filter(s => s.initScript.internal);
async removeInitScripts(initScripts: InitScript[]): Promise<void> {
const set = new Set(initScripts);
this._initScripts = this._initScripts.filter(s => !set.has(s.initScript));
await this._updateInitScripts();
}
private async _updateInitScripts() {
await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) });
}

View File

@ -58,7 +58,7 @@ export interface PageDelegate {
goForward(): Promise<boolean>;
requestGC(): Promise<void>;
addInitScript(initScript: InitScript): Promise<void>;
removeNonInternalInitScripts(): Promise<void>;
removeInitScripts(initScripts: InitScript[]): Promise<void>;
closePage(runBeforeUnload: boolean): Promise<void>;
navigateFrame(frame: frames.Frame, url: string, referrer: string | undefined): Promise<frames.GotoResult>;
@ -258,8 +258,6 @@ export class Page extends SdkObject {
async resetForReuse(metadata: CallMetadata) {
this._locatorHandlers.clear();
await this._removeExposedBindings();
await this._removeInitScripts();
await this.setClientRequestInterceptor(undefined);
await this.setServerRequestInterceptor(undefined);
await this.setFileChooserIntercepted(false);
@ -328,7 +326,7 @@ export class Page extends SdkObject {
return this.frameManager.frames();
}
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource) {
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource): Promise<PageBinding> {
if (this._pageBindings.has(name))
throw new Error(`Function "${name}" has been already registered`);
if (this.browserContext._pageBindings.has(name))
@ -338,13 +336,16 @@ export class Page extends SdkObject {
this._pageBindings.set(name, binding);
await this.delegate.addInitScript(binding.initScript);
await this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main');
return binding;
}
private async _removeExposedBindings() {
for (const [key, binding] of this._pageBindings) {
if (!binding.internal)
this._pageBindings.delete(key);
}
async removeExposedBindings(bindings: PageBinding[]) {
bindings = bindings.filter(binding => this._pageBindings.get(binding.name) === binding);
for (const binding of bindings)
this._pageBindings.delete(binding.name);
await this.delegate.removeInitScripts(bindings.map(binding => binding.initScript));
const cleanup = bindings.map(binding => `{ ${binding.cleanupScript} };\n`).join('');
await this.safeNonStallingEvaluateInAllFrames(cleanup, 'main');
}
setExtraHTTPHeaders(headers: types.HeadersArray) {
@ -560,14 +561,16 @@ export class Page extends SdkObject {
}
async addInitScript(source: string, name?: string) {
const initScript = new InitScript(source, false /* internal */, name);
const initScript = new InitScript(source, name);
this.initScripts.push(initScript);
await this.delegate.addInitScript(initScript);
return initScript;
}
private async _removeInitScripts() {
this.initScripts = this.initScripts.filter(script => script.internal);
await this.delegate.removeNonInternalInitScripts();
async removeInitScripts(initScripts: InitScript[]) {
const set = new Set(initScripts);
this.initScripts = this.initScripts.filter(script => !set.has(script));
await this.delegate.removeInitScripts(initScripts);
}
needsRequestInterception(): boolean {
@ -862,21 +865,21 @@ export class PageBinding {
if (!globalThis[property])
globalThis[property] = new (module.exports.BindingsController())(globalThis, '${PageBinding.kBindingName}');
})();
`, true /* internal */);
`);
}
readonly name: string;
readonly playwrightFunction: frames.FunctionWithSource;
readonly initScript: InitScript;
readonly needsHandle: boolean;
readonly internal: boolean;
readonly cleanupScript: string;
constructor(name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) {
this.name = name;
this.playwrightFunction = playwrightFunction;
this.initScript = new InitScript(`globalThis['${PageBinding.kController}'].addBinding(${JSON.stringify(name)}, ${needsHandle})`, true /* internal */);
this.initScript = new InitScript(`globalThis['${PageBinding.kController}'].addBinding(${JSON.stringify(name)}, ${needsHandle})`);
this.needsHandle = needsHandle;
this.internal = name.startsWith('__pw');
this.cleanupScript = `globalThis['${PageBinding.kController}'].removeBinding(${JSON.stringify(name)})`;
}
static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
@ -905,14 +908,13 @@ export class PageBinding {
export class InitScript {
readonly source: string;
readonly internal: boolean;
readonly name?: string;
auxData: any; // Can be arbitrarily used by a browser-specific implementation.
constructor(source: string, internal?: boolean, name?: string) {
constructor(source: string, name?: string) {
this.source = `(() => {
${source}
})();`;
this.internal = !!internal;
this.name = name;
}
}

View File

@ -26,6 +26,7 @@ import { Page } from '../../page';
import type { SnapshotData } from './snapshotterInjected';
import type { RegisteredListener } from '../../utils/eventsHelper';
import type { Frame } from '../../frames';
import type { InitScript } from '../../page';
import type { FrameSnapshot } from '@trace/snapshot';
export type SnapshotterBlob = {
@ -43,7 +44,7 @@ export class Snapshotter {
private _delegate: SnapshotterDelegate;
private _eventListeners: RegisteredListener[] = [];
private _snapshotStreamer: string;
private _initialized = false;
private _initScript: InitScript | undefined;
private _started = false;
constructor(context: BrowserContext, delegate: SnapshotterDelegate) {
@ -59,25 +60,26 @@ export class Snapshotter {
async start() {
this._started = true;
if (!this._initialized) {
this._initialized = true;
if (!this._initScript)
await this._initialize();
}
await this.reset();
}
async reset() {
if (this._started)
await this._runInAllFrames(`window["${this._snapshotStreamer}"].reset()`);
await this._context.safeNonStallingEvaluateInAllFrames(`window["${this._snapshotStreamer}"].reset()`, 'main');
}
async stop() {
this._started = false;
}
resetForReuse() {
async resetForReuse() {
// Next time we start recording, we will call addInitScript again.
this._initialized = false;
if (this._initScript) {
await this._context.removeInitScripts([this._initScript]);
this._initScript = undefined;
}
}
async _initialize() {
@ -88,18 +90,9 @@ export class Snapshotter {
];
const { javaScriptEnabled } = this._context._options;
const initScript = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", ${javaScriptEnabled || javaScriptEnabled === undefined})`;
await this._context.addInitScript(initScript);
await this._runInAllFrames(initScript);
}
private async _runInAllFrames(expression: string) {
const frames = [];
for (const page of this._context.pages())
frames.push(...page.frames());
await Promise.all(frames.map(frame => {
return frame.nonStallingRawEvaluateInExistingMainContext(expression).catch(e => debugLogger.log('error', e));
}));
const initScriptSource = `(${frameSnapshotStreamer})("${this._snapshotStreamer}", ${javaScriptEnabled || javaScriptEnabled === undefined})`;
this._initScript = await this._context.addInitScript(initScriptSource);
await this._context.safeNonStallingEvaluateInAllFrames(initScriptSource, 'main');
}
dispose() {

View File

@ -126,7 +126,7 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
// Discard previous chunk if any and ignore any errors there.
await this.stopChunk({ mode: 'discard' }).catch(() => {});
await this.stop();
this._snapshotter?.resetForReuse();
await this._snapshotter?.resetForReuse();
}
async start(options: TracerOptions) {

View File

@ -319,7 +319,7 @@ export class WKBrowserContext extends BrowserContext {
await (page.delegate as WKPage)._updateBootstrapScript();
}
async doRemoveNonInternalInitScripts() {
async doRemoveInitScripts(initScripts: InitScript[]) {
for (const page of this.pages())
await (page.delegate as WKPage)._updateBootstrapScript();
}

View File

@ -768,7 +768,7 @@ export class WKPage implements PageDelegate {
await this._updateBootstrapScript();
}
async removeNonInternalInitScripts() {
async removeInitScripts(initScripts: InitScript[]): Promise<void> {
await this._updateBootstrapScript();
}

View File

@ -20,6 +20,7 @@ import type { Browser, BrowserContext, BrowserServer, ConnectOptions, Page } fro
type ExtraFixtures = {
remoteServer: BrowserServer;
connect: (wsEndpoint: string, options?: ConnectOptions) => Promise<Browser>,
twoPages: { pageA: Page, pageB: Page },
};
const test = playwrightTest.extend<ExtraFixtures>({
remoteServer: async ({ browserType }, use) => {
@ -35,6 +36,17 @@ const test = playwrightTest.extend<ExtraFixtures>({
});
await browser?.close();
},
twoPages: async ({ remoteServer, connect }, use) => {
const browserA = await connect(remoteServer.wsEndpoint());
const contextA = await browserA.newContext();
const pageA = await contextA.newPage();
const browserB = await connect(remoteServer.wsEndpoint());
const contextB = browserB.contexts()[0];
const pageB = contextB.pages()[0];
await use({ pageA, pageB });
},
});
test.slow(true, 'All connect tests are slow');
@ -65,4 +77,114 @@ test('should connect two clients', async ({ connect, remoteServer, server }) =>
const pageB2 = await pageEventPromise;
await pageA2.goto('/frames/frame.html');
await expect(pageB2).toHaveURL('/frames/frame.html');
// Both contexts and pages should be still operational after any client disconnects.
await browserA.close();
await expect(pageB1).toHaveURL(server.EMPTY_PAGE);
await expect(pageB2).toHaveURL(server.PREFIX + '/frames/frame.html');
});
test('should have separate default timeouts', async ({ twoPages }) => {
const { pageA, pageB } = twoPages;
pageA.setDefaultTimeout(500);
pageB.setDefaultTimeout(600);
const [errorA, errorB] = await Promise.all([
pageA.click('div').catch(e => e),
pageB.click('div').catch(e => e),
]);
expect(errorA.message).toContain('Timeout 500ms exceeded');
expect(errorB.message).toContain('Timeout 600ms exceeded');
});
test('should receive viewport size changes', async ({ twoPages }) => {
const { pageA, pageB } = twoPages;
await pageA.setViewportSize({ width: 567, height: 456 });
expect(pageA.viewportSize()).toEqual({ width: 567, height: 456 });
await expect.poll(() => pageB.viewportSize()).toEqual({ width: 567, height: 456 });
await pageB.setViewportSize({ width: 456, height: 567 });
expect(pageB.viewportSize()).toEqual({ width: 456, height: 567 });
await expect.poll(() => pageA.viewportSize()).toEqual({ width: 456, height: 567 });
});
test('should not allow parallel js coverage', async ({ twoPages, browserName }) => {
test.skip(browserName !== 'chromium');
const { pageA, pageB } = twoPages;
await pageA.coverage.startJSCoverage();
const error = await pageB.coverage.startJSCoverage().catch(e => e);
expect(error.message).toContain('JSCoverage is already enabled');
});
test('should not allow parallel css coverage', async ({ twoPages, browserName }) => {
test.skip(browserName !== 'chromium');
const { pageA, pageB } = twoPages;
await pageA.coverage.startCSSCoverage();
const error = await pageB.coverage.startCSSCoverage().catch(e => e);
expect(error.message).toContain('CSSCoverage is already enabled');
});
test('last emulateMedia wins', async ({ twoPages }) => {
const { pageA, pageB } = twoPages;
await pageA.emulateMedia({ media: 'print' });
expect(await pageB.evaluate(() => window.matchMedia('screen').matches)).toBe(false);
expect(await pageA.evaluate(() => window.matchMedia('print').matches)).toBe(true);
await pageB.emulateMedia({ media: 'screen' });
expect(await pageB.evaluate(() => window.matchMedia('screen').matches)).toBe(true);
expect(await pageA.evaluate(() => window.matchMedia('print').matches)).toBe(false);
});
test('should remove exposed bindings upon disconnect', async ({ twoPages }) => {
const { pageA, pageB } = twoPages;
await pageA.exposeBinding('pageBindingA', () => 'pageBindingAResult');
await pageA.evaluate(() => {
(window as any).pageBindingACopy = (window as any).pageBindingA;
});
expect(await pageB.evaluate(() => (window as any).pageBindingA())).toBe('pageBindingAResult');
expect(await pageB.evaluate(() => !!(window as any).pageBindingACopy)).toBe(true);
await pageA.context().exposeBinding('contextBindingA', () => 'contextBindingAResult');
expect(await pageB.evaluate(() => (window as any).contextBindingA())).toBe('contextBindingAResult');
await pageB.exposeBinding('pageBindingB', () => 'pageBindingBResult');
expect(await pageA.evaluate(() => (window as any).pageBindingB())).toBe('pageBindingBResult');
await pageB.context().exposeBinding('contextBindingB', () => 'contextBindingBResult');
expect(await pageA.evaluate(() => (window as any).contextBindingB())).toBe('contextBindingBResult');
await pageA.context().browser().close();
await new Promise(f => setTimeout(f, 1000)); // Give disconnect some time to cleanup.
expect(await pageB.evaluate(() => (window as any).pageBindingA)).toBe(undefined);
expect(await pageB.evaluate(() => (window as any).contextBindingA)).toBe(undefined);
const error = await pageB.evaluate(() => (window as any).pageBindingACopy()).catch(e => e);
expect(error.message).toContain('binding "pageBindingA" has been removed');
expect(await pageB.evaluate(() => (window as any).pageBindingB())).toBe('pageBindingBResult');
});
test('should remove init scripts upon disconnect', async ({ twoPages, server }) => {
const { pageA, pageB } = twoPages;
await pageA.addInitScript(() => (window as any).pageValueA = 'pageValueA');
await pageA.context().addInitScript(() => (window as any).contextValueA = 'contextValueA');
await pageB.goto(server.EMPTY_PAGE);
expect(await pageB.evaluate(() => (window as any).pageValueA)).toBe('pageValueA');
expect(await pageB.evaluate(() => (window as any).contextValueA)).toBe('contextValueA');
await pageB.addInitScript(() => (window as any).pageValueB = 'pageValueB');
await pageB.context().addInitScript(() => (window as any).contextValueB = 'contextValueB');
await pageA.goto(server.EMPTY_PAGE);
expect(await pageA.evaluate(() => (window as any).pageValueB)).toBe('pageValueB');
expect(await pageA.evaluate(() => (window as any).contextValueB)).toBe('contextValueB');
await pageB.context().browser().close();
await new Promise(f => setTimeout(f, 1000)); // Give disconnect some time to cleanup.
await pageA.goto(server.EMPTY_PAGE);
expect(await pageA.evaluate(() => (window as any).pageValueB)).toBe(undefined);
expect(await pageA.evaluate(() => (window as any).contextValueB)).toBe(undefined);
expect(await pageA.evaluate(() => (window as any).pageValueA)).toBe('pageValueA');
expect(await pageA.evaluate(() => (window as any).contextValueA)).toBe('contextValueA');
});