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:
Dmitry Gozman 2024-08-06 21:42:10 +01:00
parent a54ed48b42
commit fcb8033c81
11 changed files with 122 additions and 144 deletions

View file

@ -86,7 +86,7 @@ export abstract class BrowserContext extends SdkObject {
private _customCloseHandler?: () => Promise<any>;
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> {

View file

@ -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> {

View file

@ -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);
}

View file

@ -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> {

View file

@ -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> {

View file

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

View file

@ -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();

View file

@ -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');
}

View file

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

View file

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

View file

@ -92,15 +92,17 @@ it('page.goBack should work for file urls', async ({ page, server, asset, browse
});
it('goBack/goForward should work with bfcache-able pages', async ({ page, server }) => {
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 }) => {