chore: use a single binding for all Playwright needs (#32039)

This makes it easier to manage bindings, being just init scripts.
Fixes the BFCache binding problem.
Makes bindings removable in Firefox.

Fixes #31515.
This commit is contained in:
Dmitry Gozman 2024-08-07 06:20:12 -07:00 committed by GitHub
parent fd9276f2ac
commit ea747afcdd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 122 additions and 144 deletions

View file

@ -86,7 +86,7 @@ export abstract class BrowserContext extends SdkObject {
private _customCloseHandler?: () => Promise<any>; private _customCloseHandler?: () => Promise<any>;
readonly _tempDirs: string[] = []; readonly _tempDirs: string[] = [];
private _settingStorageState = false; private _settingStorageState = false;
readonly initScripts: InitScript[] = []; initScripts: InitScript[] = [];
private _routesInFlight = new Set<network.Route>(); private _routesInFlight = new Set<network.Route>();
private _debugger!: Debugger; private _debugger!: Debugger;
_closeReason: string | undefined; _closeReason: string | undefined;
@ -271,9 +271,7 @@ export abstract class BrowserContext extends SdkObject {
protected abstract doClearPermissions(): Promise<void>; protected abstract doClearPermissions(): Promise<void>;
protected abstract doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void>; protected abstract doSetHTTPCredentials(httpCredentials?: types.Credentials): Promise<void>;
protected abstract doAddInitScript(initScript: InitScript): Promise<void>; protected abstract doAddInitScript(initScript: InitScript): Promise<void>;
protected abstract doRemoveInitScripts(): Promise<void>; protected abstract doRemoveNonInternalInitScripts(): Promise<void>;
protected abstract doExposeBinding(binding: PageBinding): Promise<void>;
protected abstract doRemoveExposedBindings(): Promise<void>;
protected abstract doUpdateRequestInterception(): Promise<void>; protected abstract doUpdateRequestInterception(): Promise<void>;
protected abstract doClose(reason: string | undefined): Promise<void>; protected abstract doClose(reason: string | undefined): Promise<void>;
protected abstract onClosePersistent(): void; protected abstract onClosePersistent(): void;
@ -320,15 +318,16 @@ export abstract class BrowserContext extends SdkObject {
} }
const binding = new PageBinding(name, playwrightBinding, needsHandle); const binding = new PageBinding(name, playwrightBinding, needsHandle);
this._pageBindings.set(name, binding); this._pageBindings.set(name, binding);
await this.doExposeBinding(binding); await this.doAddInitScript(binding.initScript);
const frames = this.pages().map(page => page.frames()).flat();
await Promise.all(frames.map(frame => frame.evaluateExpression(binding.initScript.source).catch(e => {})));
} }
async _removeExposedBindings() { async _removeExposedBindings() {
for (const key of this._pageBindings.keys()) { for (const [key, binding] of this._pageBindings) {
if (!key.startsWith('__pw')) if (!binding.internal)
this._pageBindings.delete(key); this._pageBindings.delete(key);
} }
await this.doRemoveExposedBindings();
} }
async grantPermissions(permissions: string[], origin?: string) { async grantPermissions(permissions: string[], origin?: string) {
@ -414,8 +413,8 @@ export abstract class BrowserContext extends SdkObject {
} }
async _removeInitScripts(): Promise<void> { async _removeInitScripts(): Promise<void> {
this.initScripts.splice(0, this.initScripts.length); this.initScripts = this.initScripts.filter(script => script.internal);
await this.doRemoveInitScripts(); await this.doRemoveNonInternalInitScripts();
} }
async setRequestInterceptor(handler: network.RouteHandler | undefined): Promise<void> { async setRequestInterceptor(handler: network.RouteHandler | undefined): Promise<void> {

View file

@ -21,7 +21,7 @@ import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import { assert, createGuid } from '../../utils'; import { assert, createGuid } from '../../utils';
import * as network from '../network'; import * as network from '../network';
import type { InitScript, PageBinding, PageDelegate, Worker } from '../page'; import type { InitScript, PageDelegate, Worker } from '../page';
import { Page } from '../page'; import { Page } from '../page';
import { Frame } from '../frames'; import { Frame } from '../frames';
import type { Dialog } from '../dialog'; import type { Dialog } from '../dialog';
@ -491,19 +491,9 @@ export class CRBrowserContext extends BrowserContext {
await (page._delegate as CRPage).addInitScript(initScript); await (page._delegate as CRPage).addInitScript(initScript);
} }
async doRemoveInitScripts() { async doRemoveNonInternalInitScripts() {
for (const page of this.pages()) for (const page of this.pages())
await (page._delegate as CRPage).removeInitScripts(); await (page._delegate as CRPage).removeNonInternalInitScripts();
}
async doExposeBinding(binding: PageBinding) {
for (const page of this.pages())
await (page._delegate as CRPage).exposeBinding(binding);
}
async doRemoveExposedBindings() {
for (const page of this.pages())
await (page._delegate as CRPage).removeExposedBindings();
} }
async doUpdateRequestInterception(): Promise<void> { async doUpdateRequestInterception(): Promise<void> {

View file

@ -26,7 +26,7 @@ import * as dom from '../dom';
import * as frames from '../frames'; import * as frames from '../frames';
import { helper } from '../helper'; import { helper } from '../helper';
import * as network from '../network'; import * as network from '../network';
import type { InitScript, PageBinding, PageDelegate } from '../page'; import { type InitScript, PageBinding, type PageDelegate } from '../page';
import { Page, Worker } from '../page'; import { Page, Worker } from '../page';
import type { Progress } from '../progress'; import type { Progress } from '../progress';
import type * as types from '../types'; import type * as types from '../types';
@ -182,15 +182,6 @@ export class CRPage implements PageDelegate {
return this._sessionForFrame(frame)._navigate(frame, url, referrer); return this._sessionForFrame(frame)._navigate(frame, url, referrer);
} }
async exposeBinding(binding: PageBinding) {
await this._forAllFrameSessions(frame => frame._initBinding(binding));
await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(binding.source).catch(e => {})));
}
async removeExposedBindings() {
await this._forAllFrameSessions(frame => frame._removeExposedBindings());
}
async updateExtraHTTPHeaders(): Promise<void> { async updateExtraHTTPHeaders(): Promise<void> {
const headers = network.mergeHeaders([ const headers = network.mergeHeaders([
this._browserContext._options.extraHTTPHeaders, this._browserContext._options.extraHTTPHeaders,
@ -260,7 +251,7 @@ export class CRPage implements PageDelegate {
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world)); await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world));
} }
async removeInitScripts() { async removeNonInternalInitScripts() {
await this._forAllFrameSessions(frame => frame._removeEvaluatesOnNewDocument()); await this._forAllFrameSessions(frame => frame._removeEvaluatesOnNewDocument());
} }
@ -420,7 +411,6 @@ class FrameSession {
private _screencastId: string | null = null; private _screencastId: string | null = null;
private _screencastClients = new Set<any>(); private _screencastClients = new Set<any>();
private _evaluateOnNewDocumentIdentifiers: string[] = []; private _evaluateOnNewDocumentIdentifiers: string[] = [];
private _exposedBindingNames: string[] = [];
private _metricsOverride: Protocol.Emulation.setDeviceMetricsOverrideParameters | undefined; private _metricsOverride: Protocol.Emulation.setDeviceMetricsOverrideParameters | undefined;
private _workerSessions = new Map<string, CRSession>(); private _workerSessions = new Map<string, CRSession>();
@ -519,9 +509,7 @@ class FrameSession {
grantUniveralAccess: true, grantUniveralAccess: true,
worldName: UTILITY_WORLD_NAME, worldName: UTILITY_WORLD_NAME,
}); });
for (const binding of this._crPage._browserContext._pageBindings.values()) for (const initScript of this._crPage._page.allInitScripts())
frame.evaluateExpression(binding.source).catch(e => {});
for (const initScript of this._crPage._browserContext.initScripts)
frame.evaluateExpression(initScript.source).catch(e => {}); frame.evaluateExpression(initScript.source).catch(e => {});
} }
@ -541,6 +529,7 @@ class FrameSession {
this._client.send('Log.enable', {}), this._client.send('Log.enable', {}),
lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }), lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
this._client.send('Runtime.enable', {}), this._client.send('Runtime.enable', {}),
this._client.send('Runtime.addBinding', { name: PageBinding.kPlaywrightBinding }),
this._client.send('Page.addScriptToEvaluateOnNewDocument', { this._client.send('Page.addScriptToEvaluateOnNewDocument', {
source: '', source: '',
worldName: UTILITY_WORLD_NAME, worldName: UTILITY_WORLD_NAME,
@ -573,11 +562,7 @@ class FrameSession {
promises.push(this._updateGeolocation(true)); promises.push(this._updateGeolocation(true));
promises.push(this._updateEmulateMedia()); promises.push(this._updateEmulateMedia());
promises.push(this._updateFileChooserInterception(true)); promises.push(this._updateFileChooserInterception(true));
for (const binding of this._crPage._page.allBindings()) for (const initScript of this._crPage._page.allInitScripts())
promises.push(this._initBinding(binding));
for (const initScript of this._crPage._browserContext.initScripts)
promises.push(this._evaluateOnNewDocument(initScript, 'main'));
for (const initScript of this._crPage._page.initScripts)
promises.push(this._evaluateOnNewDocument(initScript, 'main')); promises.push(this._evaluateOnNewDocument(initScript, 'main'));
if (screencastOptions) if (screencastOptions)
promises.push(this._startVideoRecording(screencastOptions)); promises.push(this._startVideoRecording(screencastOptions));
@ -834,25 +819,6 @@ class FrameSession {
this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace)); this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace));
} }
async _initBinding(binding: PageBinding) {
const [, response] = await Promise.all([
this._client.send('Runtime.addBinding', { name: binding.name }),
this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: binding.source })
]);
this._exposedBindingNames.push(binding.name);
if (!binding.name.startsWith('__pw'))
this._evaluateOnNewDocumentIdentifiers.push(response.identifier);
}
async _removeExposedBindings() {
const toRetain: string[] = [];
const toRemove: string[] = [];
for (const name of this._exposedBindingNames)
(name.startsWith('__pw_') ? toRetain : toRemove).push(name);
this._exposedBindingNames = toRetain;
await Promise.all(toRemove.map(name => this._client.send('Runtime.removeBinding', { name })));
}
async _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) { async _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) {
const pageOrError = await this._crPage.pageOrError(); const pageOrError = await this._crPage.pageOrError();
if (!(pageOrError instanceof Error)) { if (!(pageOrError instanceof Error)) {
@ -1102,6 +1068,7 @@ class FrameSession {
async _evaluateOnNewDocument(initScript: InitScript, world: types.World): Promise<void> { async _evaluateOnNewDocument(initScript: InitScript, world: types.World): Promise<void> {
const worldName = world === 'utility' ? UTILITY_WORLD_NAME : undefined; const worldName = world === 'utility' ? UTILITY_WORLD_NAME : undefined;
const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: initScript.source, worldName }); const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: initScript.source, worldName });
if (!initScript.internal)
this._evaluateOnNewDocumentIdentifiers.push(identifier); this._evaluateOnNewDocumentIdentifiers.push(identifier);
} }

View file

@ -21,7 +21,8 @@ import type { BrowserOptions } from '../browser';
import { Browser } from '../browser'; import { Browser } from '../browser';
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext'; import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
import * as network from '../network'; import * as network from '../network';
import type { InitScript, Page, PageBinding, PageDelegate } from '../page'; import type { InitScript, Page, PageDelegate } from '../page';
import { PageBinding } from '../page';
import type { ConnectionTransport } from '../transport'; import type { ConnectionTransport } from '../transport';
import type * as types from '../types'; import type * as types from '../types';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
@ -178,7 +179,10 @@ export class FFBrowserContext extends BrowserContext {
override async _initialize() { override async _initialize() {
assert(!this._ffPages().length); assert(!this._ffPages().length);
const browserContextId = this._browserContextId; const browserContextId = this._browserContextId;
const promises: Promise<any>[] = [super._initialize()]; const promises: Promise<any>[] = [
super._initialize(),
this._browser.session.send('Browser.addBinding', { browserContextId: this._browserContextId, name: PageBinding.kPlaywrightBinding, script: '' }),
];
if (this._options.acceptDownloads !== 'internal-browser-default') { if (this._options.acceptDownloads !== 'internal-browser-default') {
promises.push(this._browser.session.send('Browser.setDownloadOptions', { promises.push(this._browser.session.send('Browser.setDownloadOptions', {
browserContextId, browserContextId,
@ -353,21 +357,17 @@ export class FFBrowserContext extends BrowserContext {
} }
async doAddInitScript(initScript: InitScript) { async doAddInitScript(initScript: InitScript) {
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: this.initScripts.map(script => ({ script: script.source })) }); await this._updateInitScripts();
} }
async doRemoveInitScripts() { async doRemoveNonInternalInitScripts() {
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: [] }); await this._updateInitScripts();
} }
async doExposeBinding(binding: PageBinding) { private async _updateInitScripts() {
await this._browser.session.send('Browser.addBinding', { browserContextId: this._browserContextId, name: binding.name, script: binding.source }); const bindingScripts = [...this._pageBindings.values()].map(binding => binding.initScript.source);
} const initScripts = this.initScripts.map(script => script.source);
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: [...bindingScripts, ...initScripts].map(script => ({ script })) });
async doRemoveExposedBindings() {
// TODO: implement me.
// This is not a critical problem, what ends up happening is
// an old binding will be restored upon page reload and will point nowhere.
} }
async doUpdateRequestInterception(): Promise<void> { async doUpdateRequestInterception(): Promise<void> {

View file

@ -20,7 +20,7 @@ import * as dom from '../dom';
import type * as frames from '../frames'; import type * as frames from '../frames';
import type { RegisteredListener } from '../../utils/eventsHelper'; import type { RegisteredListener } from '../../utils/eventsHelper';
import { eventsHelper } from '../../utils/eventsHelper'; import { eventsHelper } from '../../utils/eventsHelper';
import type { PageBinding, PageDelegate } from '../page'; import type { PageDelegate } from '../page';
import { InitScript } from '../page'; import { InitScript } from '../page';
import { Page, Worker } from '../page'; import { Page, Worker } from '../page';
import type * as types from '../types'; import type * as types from '../types';
@ -114,7 +114,7 @@ export class FFPage implements PageDelegate {
}); });
// Ideally, we somehow ensure that utility world is created before Page.ready arrives, but currently it is racy. // 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. // Therefore, we can end up with an initialized page without utility world, although very unlikely.
this.addInitScript(new InitScript(''), UTILITY_WORLD_NAME).catch(e => this._markAsError(e)); this.addInitScript(new InitScript('', true), UTILITY_WORLD_NAME).catch(e => this._markAsError(e));
} }
potentiallyUninitializedPage(): Page { potentiallyUninitializedPage(): Page {
@ -336,14 +336,6 @@ export class FFPage implements PageDelegate {
this._browserContext._browser._videoStarted(this._browserContext, event.screencastId, event.file, this.pageOrError()); this._browserContext._browser._videoStarted(this._browserContext, event.screencastId, event.file, this.pageOrError());
} }
async exposeBinding(binding: PageBinding) {
await this._session.send('Page.addBinding', { name: binding.name, script: binding.source });
}
async removeExposedBindings() {
// TODO: implement me.
}
didClose() { didClose() {
this._markAsError(new TargetClosedError()); this._markAsError(new TargetClosedError());
this._session.dispose(); this._session.dispose();
@ -412,9 +404,9 @@ export class FFPage implements PageDelegate {
await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) }); await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) });
} }
async removeInitScripts() { async removeNonInternalInitScripts() {
this._initScripts = []; this._initScripts = this._initScripts.filter(s => s.initScript.internal);
await this._session.send('Page.setInitScripts', { scripts: [] }); await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) });
} }
async closePage(runBeforeUnload: boolean): Promise<void> { async closePage(runBeforeUnload: boolean): Promise<void> {

View file

@ -54,10 +54,8 @@ export interface PageDelegate {
reload(): Promise<void>; reload(): Promise<void>;
goBack(): Promise<boolean>; goBack(): Promise<boolean>;
goForward(): Promise<boolean>; goForward(): Promise<boolean>;
exposeBinding(binding: PageBinding): Promise<void>;
removeExposedBindings(): Promise<void>;
addInitScript(initScript: InitScript): Promise<void>; addInitScript(initScript: InitScript): Promise<void>;
removeInitScripts(): Promise<void>; removeNonInternalInitScripts(): Promise<void>;
closePage(runBeforeUnload: boolean): Promise<void>; closePage(runBeforeUnload: boolean): Promise<void>;
potentiallyUninitializedPage(): Page; potentiallyUninitializedPage(): Page;
pageOrError(): Promise<Page | Error>; pageOrError(): Promise<Page | Error>;
@ -154,7 +152,7 @@ export class Page extends SdkObject {
private _emulatedMedia: Partial<EmulatedMedia> = {}; private _emulatedMedia: Partial<EmulatedMedia> = {};
private _interceptFileChooser = false; private _interceptFileChooser = false;
private readonly _pageBindings = new Map<string, PageBinding>(); private readonly _pageBindings = new Map<string, PageBinding>();
readonly initScripts: InitScript[] = []; initScripts: InitScript[] = [];
readonly _screenshotter: Screenshotter; readonly _screenshotter: Screenshotter;
readonly _frameManager: frames.FrameManager; readonly _frameManager: frames.FrameManager;
readonly accessibility: accessibility.Accessibility; readonly accessibility: accessibility.Accessibility;
@ -342,15 +340,15 @@ export class Page extends SdkObject {
throw new Error(`Function "${name}" has been already registered in the browser context`); throw new Error(`Function "${name}" has been already registered in the browser context`);
const binding = new PageBinding(name, playwrightBinding, needsHandle); const binding = new PageBinding(name, playwrightBinding, needsHandle);
this._pageBindings.set(name, binding); this._pageBindings.set(name, binding);
await this._delegate.exposeBinding(binding); await this._delegate.addInitScript(binding.initScript);
await Promise.all(this.frames().map(frame => frame.evaluateExpression(binding.initScript.source).catch(e => {})));
} }
async _removeExposedBindings() { async _removeExposedBindings() {
for (const key of this._pageBindings.keys()) { for (const [key, binding] of this._pageBindings) {
if (!key.startsWith('__pw')) if (!binding.internal)
this._pageBindings.delete(key); this._pageBindings.delete(key);
} }
await this._delegate.removeExposedBindings();
} }
setExtraHTTPHeaders(headers: types.HeadersArray) { setExtraHTTPHeaders(headers: types.HeadersArray) {
@ -533,8 +531,8 @@ export class Page extends SdkObject {
} }
async _removeInitScripts() { async _removeInitScripts() {
this.initScripts.splice(0, this.initScripts.length); this.initScripts = this.initScripts.filter(script => script.internal);
await this._delegate.removeInitScripts(); await this._delegate.removeNonInternalInitScripts();
} }
needsRequestInterception(): boolean { needsRequestInterception(): boolean {
@ -727,8 +725,9 @@ export class Page extends SdkObject {
this._browserContext.addVisitedOrigin(origin); this._browserContext.addVisitedOrigin(origin);
} }
allBindings() { allInitScripts() {
return [...this._browserContext._pageBindings.values(), ...this._pageBindings.values()]; const bindings = [...this._browserContext._pageBindings.values(), ...this._pageBindings.values()];
return [...bindings.map(binding => binding.initScript), ...this._browserContext.initScripts, ...this.initScripts];
} }
getBinding(name: string) { getBinding(name: string) {
@ -819,23 +818,29 @@ type BindingPayload = {
}; };
export class PageBinding { export class PageBinding {
static kPlaywrightBinding = '__playwright__binding__';
readonly name: string; readonly name: string;
readonly playwrightFunction: frames.FunctionWithSource; readonly playwrightFunction: frames.FunctionWithSource;
readonly source: string; readonly initScript: InitScript;
readonly needsHandle: boolean; readonly needsHandle: boolean;
readonly internal: boolean;
constructor(name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) { constructor(name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) {
this.name = name; this.name = name;
this.playwrightFunction = playwrightFunction; this.playwrightFunction = playwrightFunction;
this.source = `(${addPageBinding.toString()})(${JSON.stringify(name)}, ${needsHandle}, (${source})())`; this.initScript = new InitScript(`(${addPageBinding.toString()})(${JSON.stringify(PageBinding.kPlaywrightBinding)}, ${JSON.stringify(name)}, ${needsHandle}, (${source})())`, true /* internal */);
this.needsHandle = needsHandle; this.needsHandle = needsHandle;
this.internal = name.startsWith('__pw');
} }
static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) { static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload; const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload;
try { try {
assert(context.world); assert(context.world);
const binding = page.getBinding(name)!; const binding = page.getBinding(name);
if (!binding)
throw new Error(`Function "${name}" is not exposed`);
let result: any; let result: any;
if (binding.needsHandle) { if (binding.needsHandle) {
const handle = await context.evaluateHandle(takeHandle, { name, seq }).catch(e => null); const handle = await context.evaluateHandle(takeHandle, { name, seq }).catch(e => null);
@ -877,10 +882,8 @@ export class PageBinding {
} }
} }
function addPageBinding(bindingName: string, needsHandle: boolean, utilityScriptSerializers: ReturnType<typeof source>) { function addPageBinding(playwrightBinding: string, bindingName: string, needsHandle: boolean, utilityScriptSerializers: ReturnType<typeof source>) {
const binding = (globalThis as any)[bindingName]; const binding = (globalThis as any)[playwrightBinding];
if (binding.__installed)
return;
(globalThis as any)[bindingName] = (...args: any[]) => { (globalThis as any)[bindingName] = (...args: any[]) => {
const me = (globalThis as any)[bindingName]; const me = (globalThis as any)[bindingName];
if (needsHandle && args.slice(1).some(arg => arg !== undefined)) if (needsHandle && args.slice(1).some(arg => arg !== undefined))
@ -919,8 +922,9 @@ function addPageBinding(bindingName: string, needsHandle: boolean, utilityScript
export class InitScript { export class InitScript {
readonly source: string; readonly source: string;
readonly internal: boolean;
constructor(source: string) { constructor(source: string, internal?: boolean) {
const guid = createGuid(); const guid = createGuid();
this.source = `(() => { this.source = `(() => {
globalThis.__pwInitScripts = globalThis.__pwInitScripts || {}; globalThis.__pwInitScripts = globalThis.__pwInitScripts || {};
@ -930,6 +934,7 @@ export class InitScript {
globalThis.__pwInitScripts[${JSON.stringify(guid)}] = true; globalThis.__pwInitScripts[${JSON.stringify(guid)}] = true;
${source} ${source}
})();`; })();`;
this.internal = !!internal;
} }
} }

View file

@ -22,7 +22,7 @@ import type { RegisteredListener } from '../../utils/eventsHelper';
import { assert } from '../../utils'; import { assert } from '../../utils';
import { eventsHelper } from '../../utils/eventsHelper'; import { eventsHelper } from '../../utils/eventsHelper';
import * as network from '../network'; import * as network from '../network';
import type { InitScript, Page, PageBinding, PageDelegate } from '../page'; import type { InitScript, Page, PageDelegate } from '../page';
import type { ConnectionTransport } from '../transport'; import type { ConnectionTransport } from '../transport';
import type * as types from '../types'; import type * as types from '../types';
import type * as channels from '@protocol/channels'; import type * as channels from '@protocol/channels';
@ -320,21 +320,11 @@ export class WKBrowserContext extends BrowserContext {
await (page._delegate as WKPage)._updateBootstrapScript(); await (page._delegate as WKPage)._updateBootstrapScript();
} }
async doRemoveInitScripts() { async doRemoveNonInternalInitScripts() {
for (const page of this.pages()) for (const page of this.pages())
await (page._delegate as WKPage)._updateBootstrapScript(); await (page._delegate as WKPage)._updateBootstrapScript();
} }
async doExposeBinding(binding: PageBinding) {
for (const page of this.pages())
await (page._delegate as WKPage).exposeBinding(binding);
}
async doRemoveExposedBindings() {
for (const page of this.pages())
await (page._delegate as WKPage).removeExposedBindings();
}
async doUpdateRequestInterception(): Promise<void> { async doUpdateRequestInterception(): Promise<void> {
for (const page of this.pages()) for (const page of this.pages())
await (page._delegate as WKPage).updateRequestInterception(); await (page._delegate as WKPage).updateRequestInterception();

View file

@ -30,7 +30,7 @@ import { eventsHelper } from '../../utils/eventsHelper';
import { helper } from '../helper'; import { helper } from '../helper';
import type { JSHandle } from '../javascript'; import type { JSHandle } from '../javascript';
import * as network from '../network'; import * as network from '../network';
import type { InitScript, PageBinding, PageDelegate } from '../page'; import { type InitScript, PageBinding, type PageDelegate } from '../page';
import { Page } from '../page'; import { Page } from '../page';
import type { Progress } from '../progress'; import type { Progress } from '../progress';
import type * as types from '../types'; import type * as types from '../types';
@ -179,6 +179,7 @@ export class WKPage implements PageDelegate {
const promises: Promise<any>[] = [ const promises: Promise<any>[] = [
// Resource tree should be received before first execution context. // Resource tree should be received before first execution context.
session.send('Runtime.enable'), session.send('Runtime.enable'),
session.send('Runtime.addBinding', { name: PageBinding.kPlaywrightBinding }),
session.send('Page.createUserWorld', { name: UTILITY_WORLD_NAME }).catch(_ => {}), // Worlds are per-process session.send('Page.createUserWorld', { name: UTILITY_WORLD_NAME }).catch(_ => {}), // Worlds are per-process
session.send('Console.enable'), session.send('Console.enable'),
session.send('Network.enable'), session.send('Network.enable'),
@ -200,8 +201,6 @@ export class WKPage implements PageDelegate {
const emulatedMedia = this._page.emulatedMedia(); const emulatedMedia = this._page.emulatedMedia();
if (emulatedMedia.media || emulatedMedia.colorScheme || emulatedMedia.reducedMotion || emulatedMedia.forcedColors) if (emulatedMedia.media || emulatedMedia.colorScheme || emulatedMedia.reducedMotion || emulatedMedia.forcedColors)
promises.push(WKPage._setEmulateMedia(session, emulatedMedia.media, emulatedMedia.colorScheme, emulatedMedia.reducedMotion, emulatedMedia.forcedColors)); promises.push(WKPage._setEmulateMedia(session, emulatedMedia.media, emulatedMedia.colorScheme, emulatedMedia.reducedMotion, emulatedMedia.forcedColors));
for (const binding of this._page.allBindings())
promises.push(session.send('Runtime.addBinding', { name: binding.name }));
const bootstrapScript = this._calculateBootstrapScript(); const bootstrapScript = this._calculateBootstrapScript();
if (bootstrapScript.length) if (bootstrapScript.length)
promises.push(session.send('Page.setBootstrapScript', { source: bootstrapScript })); promises.push(session.send('Page.setBootstrapScript', { source: bootstrapScript }));
@ -768,21 +767,11 @@ export class WKPage implements PageDelegate {
}); });
} }
async exposeBinding(binding: PageBinding): Promise<void> {
this._session.send('Runtime.addBinding', { name: binding.name });
await this._updateBootstrapScript();
await Promise.all(this._page.frames().map(frame => frame.evaluateExpression(binding.source).catch(e => {})));
}
async removeExposedBindings(): Promise<void> {
await this._updateBootstrapScript();
}
async addInitScript(initScript: InitScript): Promise<void> { async addInitScript(initScript: InitScript): Promise<void> {
await this._updateBootstrapScript(); await this._updateBootstrapScript();
} }
async removeInitScripts() { async removeNonInternalInitScripts() {
await this._updateBootstrapScript(); await this._updateBootstrapScript();
} }
@ -795,11 +784,7 @@ export class WKPage implements PageDelegate {
} }
scripts.push('if (!window.safari) window.safari = { pushNotification: { toString() { return "[object SafariRemoteNotification]"; } } };'); scripts.push('if (!window.safari) window.safari = { pushNotification: { toString() { return "[object SafariRemoteNotification]"; } } };');
scripts.push('if (!window.GestureEvent) window.GestureEvent = function GestureEvent() {};'); scripts.push('if (!window.GestureEvent) window.GestureEvent = function GestureEvent() {};');
scripts.push(...this._page.allInitScripts().map(script => script.source));
for (const binding of this._page.allBindings())
scripts.push(binding.source);
scripts.push(...this._browserContext.initScripts.map(s => s.source));
scripts.push(...this._page.initScripts.map(s => s.source));
return scripts.join(';\n'); return scripts.join(';\n');
} }

View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<link rel='stylesheet' href='./one-style.css'>
<div>BFCached</div>
<script>
window.didShow = new Promise(f => window.addEventListener('pageshow', event => {
console.log(event);
window._persisted = !!event.persisted;
window._event = event;
f({ persisted: !!event.persisted });
}));
</script>

View file

@ -0,0 +1,37 @@
/**
* Copyright Microsoft Corporation. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { contextTest as test, expect } from '../../config/browserTest';
test.use({
launchOptions: async ({ launchOptions }, use) => {
await use({ ...launchOptions, ignoreDefaultArgs: ['--disable-back-forward-cache'] });
}
});
test('bindings should work after restoring from bfcache', async ({ page, server }) => {
await page.exposeFunction('add', (a, b) => a + b);
await page.goto(server.PREFIX + '/cached/bfcached.html');
expect(await page.evaluate('window.add(1, 2)')).toBe(3);
await page.setContent(`<a href='about:blank'}>click me</a>`);
await page.click('a');
await page.goBack({ waitUntil: 'commit' });
await page.evaluate('window.didShow');
expect(await page.evaluate('window.add(2, 3)')).toBe(5);
});

View file

@ -92,15 +92,17 @@ it('page.goBack should work for file urls', async ({ page, server, asset, browse
}); });
it('goBack/goForward should work with bfcache-able pages', async ({ page, server }) => { it('goBack/goForward should work with bfcache-able pages', async ({ page, server }) => {
await page.goto(server.PREFIX + '/cached/one-style.html'); await page.goto(server.PREFIX + '/cached/bfcached.html');
await page.setContent(`<a href=${JSON.stringify(server.PREFIX + '/cached/one-style.html?foo')}>click me</a>`); await page.setContent(`<a href=${JSON.stringify(server.PREFIX + '/cached/bfcached.html?foo')}>click me</a>`);
await page.click('a'); await page.click('a');
let response = await page.goBack(); let response = await page.goBack();
expect(response.url()).toBe(server.PREFIX + '/cached/one-style.html'); expect(response.url()).toBe(server.PREFIX + '/cached/bfcached.html');
// BFCache should be disabled.
expect(await page.evaluate('window.didShow')).toEqual({ persisted: false });
response = await page.goForward(); response = await page.goForward();
expect(response.url()).toBe(server.PREFIX + '/cached/one-style.html?foo'); expect(response.url()).toBe(server.PREFIX + '/cached/bfcached.html?foo');
}); });
it('page.reload should work', async ({ page, server }) => { it('page.reload should work', async ({ page, server }) => {