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>;
|
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