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:
parent
fd9276f2ac
commit
ea747afcdd
|
@ -86,7 +86,7 @@ export abstract class BrowserContext extends SdkObject {
|
|||
private _customCloseHandler?: () => Promise<any>;
|
||||
readonly _tempDirs: string[] = [];
|
||||
private _settingStorageState = false;
|
||||
readonly initScripts: InitScript[] = [];
|
||||
initScripts: InitScript[] = [];
|
||||
private _routesInFlight = new Set<network.Route>();
|
||||
private _debugger!: Debugger;
|
||||
_closeReason: string | undefined;
|
||||
|
@ -271,9 +271,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 doRemoveInitScripts(): Promise<void>;
|
||||
protected abstract doExposeBinding(binding: PageBinding): Promise<void>;
|
||||
protected abstract doRemoveExposedBindings(): Promise<void>;
|
||||
protected abstract doRemoveNonInternalInitScripts(): Promise<void>;
|
||||
protected abstract doUpdateRequestInterception(): Promise<void>;
|
||||
protected abstract doClose(reason: string | undefined): Promise<void>;
|
||||
protected abstract onClosePersistent(): void;
|
||||
|
@ -320,15 +318,16 @@ export abstract class BrowserContext extends SdkObject {
|
|||
}
|
||||
const binding = new PageBinding(name, playwrightBinding, needsHandle);
|
||||
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() {
|
||||
for (const key of this._pageBindings.keys()) {
|
||||
if (!key.startsWith('__pw'))
|
||||
for (const [key, binding] of this._pageBindings) {
|
||||
if (!binding.internal)
|
||||
this._pageBindings.delete(key);
|
||||
}
|
||||
await this.doRemoveExposedBindings();
|
||||
}
|
||||
|
||||
async grantPermissions(permissions: string[], origin?: string) {
|
||||
|
@ -414,8 +413,8 @@ export abstract class BrowserContext extends SdkObject {
|
|||
}
|
||||
|
||||
async _removeInitScripts(): Promise<void> {
|
||||
this.initScripts.splice(0, this.initScripts.length);
|
||||
await this.doRemoveInitScripts();
|
||||
this.initScripts = this.initScripts.filter(script => script.internal);
|
||||
await this.doRemoveNonInternalInitScripts();
|
||||
}
|
||||
|
||||
async setRequestInterceptor(handler: network.RouteHandler | undefined): Promise<void> {
|
||||
|
|
|
@ -21,7 +21,7 @@ import { Browser } from '../browser';
|
|||
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
|
||||
import { assert, createGuid } from '../../utils';
|
||||
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 { Frame } from '../frames';
|
||||
import type { Dialog } from '../dialog';
|
||||
|
@ -491,19 +491,9 @@ export class CRBrowserContext extends BrowserContext {
|
|||
await (page._delegate as CRPage).addInitScript(initScript);
|
||||
}
|
||||
|
||||
async doRemoveInitScripts() {
|
||||
async doRemoveNonInternalInitScripts() {
|
||||
for (const page of this.pages())
|
||||
await (page._delegate as CRPage).removeInitScripts();
|
||||
}
|
||||
|
||||
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();
|
||||
await (page._delegate as CRPage).removeNonInternalInitScripts();
|
||||
}
|
||||
|
||||
async doUpdateRequestInterception(): Promise<void> {
|
||||
|
|
|
@ -26,7 +26,7 @@ import * as dom from '../dom';
|
|||
import * as frames from '../frames';
|
||||
import { helper } from '../helper';
|
||||
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 type { Progress } from '../progress';
|
||||
import type * as types from '../types';
|
||||
|
@ -182,15 +182,6 @@ export class CRPage implements PageDelegate {
|
|||
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> {
|
||||
const headers = network.mergeHeaders([
|
||||
this._browserContext._options.extraHTTPHeaders,
|
||||
|
@ -260,7 +251,7 @@ export class CRPage implements PageDelegate {
|
|||
await this._forAllFrameSessions(frame => frame._evaluateOnNewDocument(initScript, world));
|
||||
}
|
||||
|
||||
async removeInitScripts() {
|
||||
async removeNonInternalInitScripts() {
|
||||
await this._forAllFrameSessions(frame => frame._removeEvaluatesOnNewDocument());
|
||||
}
|
||||
|
||||
|
@ -420,7 +411,6 @@ class FrameSession {
|
|||
private _screencastId: string | null = null;
|
||||
private _screencastClients = new Set<any>();
|
||||
private _evaluateOnNewDocumentIdentifiers: string[] = [];
|
||||
private _exposedBindingNames: string[] = [];
|
||||
private _metricsOverride: Protocol.Emulation.setDeviceMetricsOverrideParameters | undefined;
|
||||
private _workerSessions = new Map<string, CRSession>();
|
||||
|
||||
|
@ -519,9 +509,7 @@ class FrameSession {
|
|||
grantUniveralAccess: true,
|
||||
worldName: UTILITY_WORLD_NAME,
|
||||
});
|
||||
for (const binding of this._crPage._browserContext._pageBindings.values())
|
||||
frame.evaluateExpression(binding.source).catch(e => {});
|
||||
for (const initScript of this._crPage._browserContext.initScripts)
|
||||
for (const initScript of this._crPage._page.allInitScripts())
|
||||
frame.evaluateExpression(initScript.source).catch(e => {});
|
||||
}
|
||||
|
||||
|
@ -541,6 +529,7 @@ class FrameSession {
|
|||
this._client.send('Log.enable', {}),
|
||||
lifecycleEventsEnabled = this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }),
|
||||
this._client.send('Runtime.enable', {}),
|
||||
this._client.send('Runtime.addBinding', { name: PageBinding.kPlaywrightBinding }),
|
||||
this._client.send('Page.addScriptToEvaluateOnNewDocument', {
|
||||
source: '',
|
||||
worldName: UTILITY_WORLD_NAME,
|
||||
|
@ -573,11 +562,7 @@ class FrameSession {
|
|||
promises.push(this._updateGeolocation(true));
|
||||
promises.push(this._updateEmulateMedia());
|
||||
promises.push(this._updateFileChooserInterception(true));
|
||||
for (const binding of this._crPage._page.allBindings())
|
||||
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)
|
||||
for (const initScript of this._crPage._page.allInitScripts())
|
||||
promises.push(this._evaluateOnNewDocument(initScript, 'main'));
|
||||
if (screencastOptions)
|
||||
promises.push(this._startVideoRecording(screencastOptions));
|
||||
|
@ -834,25 +819,6 @@ class FrameSession {
|
|||
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) {
|
||||
const pageOrError = await this._crPage.pageOrError();
|
||||
if (!(pageOrError instanceof Error)) {
|
||||
|
@ -1102,7 +1068,8 @@ class FrameSession {
|
|||
async _evaluateOnNewDocument(initScript: InitScript, world: types.World): Promise<void> {
|
||||
const worldName = world === 'utility' ? UTILITY_WORLD_NAME : undefined;
|
||||
const { identifier } = await this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: initScript.source, worldName });
|
||||
this._evaluateOnNewDocumentIdentifiers.push(identifier);
|
||||
if (!initScript.internal)
|
||||
this._evaluateOnNewDocumentIdentifiers.push(identifier);
|
||||
}
|
||||
|
||||
async _removeEvaluatesOnNewDocument(): Promise<void> {
|
||||
|
|
|
@ -21,7 +21,8 @@ import type { BrowserOptions } from '../browser';
|
|||
import { Browser } from '../browser';
|
||||
import { assertBrowserContextIsNotOwned, BrowserContext, verifyGeolocation } from '../browserContext';
|
||||
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 * as types from '../types';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
@ -178,7 +179,10 @@ export class FFBrowserContext extends BrowserContext {
|
|||
override async _initialize() {
|
||||
assert(!this._ffPages().length);
|
||||
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') {
|
||||
promises.push(this._browser.session.send('Browser.setDownloadOptions', {
|
||||
browserContextId,
|
||||
|
@ -353,21 +357,17 @@ export class FFBrowserContext extends BrowserContext {
|
|||
}
|
||||
|
||||
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() {
|
||||
await this._browser.session.send('Browser.setInitScripts', { browserContextId: this._browserContextId, scripts: [] });
|
||||
async doRemoveNonInternalInitScripts() {
|
||||
await this._updateInitScripts();
|
||||
}
|
||||
|
||||
async doExposeBinding(binding: PageBinding) {
|
||||
await this._browser.session.send('Browser.addBinding', { browserContextId: this._browserContextId, name: binding.name, script: binding.source });
|
||||
}
|
||||
|
||||
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.
|
||||
private async _updateInitScripts() {
|
||||
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 doUpdateRequestInterception(): Promise<void> {
|
||||
|
|
|
@ -20,7 +20,7 @@ import * as dom from '../dom';
|
|||
import type * as frames from '../frames';
|
||||
import type { RegisteredListener } from '../../utils/eventsHelper';
|
||||
import { eventsHelper } from '../../utils/eventsHelper';
|
||||
import type { PageBinding, PageDelegate } from '../page';
|
||||
import type { PageDelegate } from '../page';
|
||||
import { InitScript } from '../page';
|
||||
import { Page, Worker } from '../page';
|
||||
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.
|
||||
// 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 {
|
||||
|
@ -336,14 +336,6 @@ export class FFPage implements PageDelegate {
|
|||
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() {
|
||||
this._markAsError(new TargetClosedError());
|
||||
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 })) });
|
||||
}
|
||||
|
||||
async removeInitScripts() {
|
||||
this._initScripts = [];
|
||||
await this._session.send('Page.setInitScripts', { scripts: [] });
|
||||
async removeNonInternalInitScripts() {
|
||||
this._initScripts = this._initScripts.filter(s => s.initScript.internal);
|
||||
await this._session.send('Page.setInitScripts', { scripts: this._initScripts.map(s => ({ script: s.initScript.source, worldName: s.worldName })) });
|
||||
}
|
||||
|
||||
async closePage(runBeforeUnload: boolean): Promise<void> {
|
||||
|
|
|
@ -54,10 +54,8 @@ export interface PageDelegate {
|
|||
reload(): Promise<void>;
|
||||
goBack(): Promise<boolean>;
|
||||
goForward(): Promise<boolean>;
|
||||
exposeBinding(binding: PageBinding): Promise<void>;
|
||||
removeExposedBindings(): Promise<void>;
|
||||
addInitScript(initScript: InitScript): Promise<void>;
|
||||
removeInitScripts(): Promise<void>;
|
||||
removeNonInternalInitScripts(): Promise<void>;
|
||||
closePage(runBeforeUnload: boolean): Promise<void>;
|
||||
potentiallyUninitializedPage(): Page;
|
||||
pageOrError(): Promise<Page | Error>;
|
||||
|
@ -154,7 +152,7 @@ export class Page extends SdkObject {
|
|||
private _emulatedMedia: Partial<EmulatedMedia> = {};
|
||||
private _interceptFileChooser = false;
|
||||
private readonly _pageBindings = new Map<string, PageBinding>();
|
||||
readonly initScripts: InitScript[] = [];
|
||||
initScripts: InitScript[] = [];
|
||||
readonly _screenshotter: Screenshotter;
|
||||
readonly _frameManager: frames.FrameManager;
|
||||
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`);
|
||||
const binding = new PageBinding(name, playwrightBinding, needsHandle);
|
||||
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() {
|
||||
for (const key of this._pageBindings.keys()) {
|
||||
if (!key.startsWith('__pw'))
|
||||
for (const [key, binding] of this._pageBindings) {
|
||||
if (!binding.internal)
|
||||
this._pageBindings.delete(key);
|
||||
}
|
||||
await this._delegate.removeExposedBindings();
|
||||
}
|
||||
|
||||
setExtraHTTPHeaders(headers: types.HeadersArray) {
|
||||
|
@ -533,8 +531,8 @@ export class Page extends SdkObject {
|
|||
}
|
||||
|
||||
async _removeInitScripts() {
|
||||
this.initScripts.splice(0, this.initScripts.length);
|
||||
await this._delegate.removeInitScripts();
|
||||
this.initScripts = this.initScripts.filter(script => script.internal);
|
||||
await this._delegate.removeNonInternalInitScripts();
|
||||
}
|
||||
|
||||
needsRequestInterception(): boolean {
|
||||
|
@ -727,8 +725,9 @@ export class Page extends SdkObject {
|
|||
this._browserContext.addVisitedOrigin(origin);
|
||||
}
|
||||
|
||||
allBindings() {
|
||||
return [...this._browserContext._pageBindings.values(), ...this._pageBindings.values()];
|
||||
allInitScripts() {
|
||||
const bindings = [...this._browserContext._pageBindings.values(), ...this._pageBindings.values()];
|
||||
return [...bindings.map(binding => binding.initScript), ...this._browserContext.initScripts, ...this.initScripts];
|
||||
}
|
||||
|
||||
getBinding(name: string) {
|
||||
|
@ -819,23 +818,29 @@ type BindingPayload = {
|
|||
};
|
||||
|
||||
export class PageBinding {
|
||||
static kPlaywrightBinding = '__playwright__binding__';
|
||||
|
||||
readonly name: string;
|
||||
readonly playwrightFunction: frames.FunctionWithSource;
|
||||
readonly source: string;
|
||||
readonly initScript: InitScript;
|
||||
readonly needsHandle: boolean;
|
||||
readonly internal: boolean;
|
||||
|
||||
constructor(name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) {
|
||||
this.name = name;
|
||||
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.internal = name.startsWith('__pw');
|
||||
}
|
||||
|
||||
static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
|
||||
const { name, seq, serializedArgs } = JSON.parse(payload) as BindingPayload;
|
||||
try {
|
||||
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;
|
||||
if (binding.needsHandle) {
|
||||
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>) {
|
||||
const binding = (globalThis as any)[bindingName];
|
||||
if (binding.__installed)
|
||||
return;
|
||||
function addPageBinding(playwrightBinding: string, bindingName: string, needsHandle: boolean, utilityScriptSerializers: ReturnType<typeof source>) {
|
||||
const binding = (globalThis as any)[playwrightBinding];
|
||||
(globalThis as any)[bindingName] = (...args: any[]) => {
|
||||
const me = (globalThis as any)[bindingName];
|
||||
if (needsHandle && args.slice(1).some(arg => arg !== undefined))
|
||||
|
@ -919,8 +922,9 @@ function addPageBinding(bindingName: string, needsHandle: boolean, utilityScript
|
|||
|
||||
export class InitScript {
|
||||
readonly source: string;
|
||||
readonly internal: boolean;
|
||||
|
||||
constructor(source: string) {
|
||||
constructor(source: string, internal?: boolean) {
|
||||
const guid = createGuid();
|
||||
this.source = `(() => {
|
||||
globalThis.__pwInitScripts = globalThis.__pwInitScripts || {};
|
||||
|
@ -930,6 +934,7 @@ export class InitScript {
|
|||
globalThis.__pwInitScripts[${JSON.stringify(guid)}] = true;
|
||||
${source}
|
||||
})();`;
|
||||
this.internal = !!internal;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ import type { RegisteredListener } from '../../utils/eventsHelper';
|
|||
import { assert } from '../../utils';
|
||||
import { eventsHelper } from '../../utils/eventsHelper';
|
||||
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 * as types from '../types';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
@ -320,21 +320,11 @@ export class WKBrowserContext extends BrowserContext {
|
|||
await (page._delegate as WKPage)._updateBootstrapScript();
|
||||
}
|
||||
|
||||
async doRemoveInitScripts() {
|
||||
async doRemoveNonInternalInitScripts() {
|
||||
for (const page of this.pages())
|
||||
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> {
|
||||
for (const page of this.pages())
|
||||
await (page._delegate as WKPage).updateRequestInterception();
|
||||
|
|
|
@ -30,7 +30,7 @@ import { eventsHelper } from '../../utils/eventsHelper';
|
|||
import { helper } from '../helper';
|
||||
import type { JSHandle } from '../javascript';
|
||||
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 type { Progress } from '../progress';
|
||||
import type * as types from '../types';
|
||||
|
@ -179,6 +179,7 @@ export class WKPage implements PageDelegate {
|
|||
const promises: Promise<any>[] = [
|
||||
// Resource tree should be received before first execution context.
|
||||
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('Console.enable'),
|
||||
session.send('Network.enable'),
|
||||
|
@ -200,8 +201,6 @@ export class WKPage implements PageDelegate {
|
|||
const emulatedMedia = this._page.emulatedMedia();
|
||||
if (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();
|
||||
if (bootstrapScript.length)
|
||||
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> {
|
||||
await this._updateBootstrapScript();
|
||||
}
|
||||
|
||||
async removeInitScripts() {
|
||||
async removeNonInternalInitScripts() {
|
||||
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.GestureEvent) window.GestureEvent = function GestureEvent() {};');
|
||||
|
||||
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));
|
||||
scripts.push(...this._page.allInitScripts().map(script => script.source));
|
||||
return scripts.join(';\n');
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
});
|
|
@ -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 }) => {
|
||||
await page.goto(server.PREFIX + '/cached/one-style.html');
|
||||
await page.setContent(`<a href=${JSON.stringify(server.PREFIX + '/cached/one-style.html?foo')}>click me</a>`);
|
||||
await page.goto(server.PREFIX + '/cached/bfcached.html');
|
||||
await page.setContent(`<a href=${JSON.stringify(server.PREFIX + '/cached/bfcached.html?foo')}>click me</a>`);
|
||||
await page.click('a');
|
||||
|
||||
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();
|
||||
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 }) => {
|
||||
|
|
Loading…
Reference in New Issue