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>;
|
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> {
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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 }) => {
|
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 }) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue