chore: use a single binding for all Playwright needs
This makes it easier to manage bindings, being just init scripts. Fixes the BFCache binding problem. Makes bindings removable in Firefox.
This commit is contained in:
parent
a54ed48b42
commit
fcb8033c81
|
|
@ -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,6 +1068,7 @@ 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 });
|
||||
if (!initScript.internal)
|
||||
this._evaluateOnNewDocumentIdentifiers.push(identifier);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
|||
11
tests/assets/cached/bfcached.html
Normal file
11
tests/assets/cached/bfcached.html
Normal 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>
|
||||
37
tests/library/chromium/bfcache.spec.ts
Normal file
37
tests/library/chromium/bfcache.spec.ts
Normal 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);
|
||||
});
|
||||
|
|
@ -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 a new issue