From 6c6cdc033bd292c056cf23136f79ba4e781bfd74 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 3 Mar 2020 16:46:06 -0800 Subject: [PATCH] api(popup): introduce BrowserContext.exposeFunction (#1176) --- docs/api.md | 103 +++++++++++++++++++++++++----- src/browserContext.ts | 4 +- src/chromium/crBrowser.ts | 16 ++++- src/chromium/crPage.ts | 18 ++++-- src/firefox/ffBrowser.ts | 15 ++++- src/firefox/ffPage.ts | 10 +-- src/page.ts | 124 +++++++++++++++++++++--------------- src/webkit/wkBrowser.ts | 16 ++++- src/webkit/wkPage.ts | 30 +++++++-- test/browsercontext.spec.js | 42 ++++++++++++ test/evaluation.spec.js | 10 ++- test/popup.spec.js | 12 ++++ 12 files changed, 311 insertions(+), 89 deletions(-) diff --git a/docs/api.md b/docs/api.md index c0f722f62a..9b3c0e4fba 100644 --- a/docs/api.md +++ b/docs/api.md @@ -271,6 +271,7 @@ await context.close(); - [browserContext.clearPermissions()](#browsercontextclearpermissions) - [browserContext.close()](#browsercontextclose) - [browserContext.cookies([...urls])](#browsercontextcookiesurls) +- [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction) - [browserContext.newPage()](#browsercontextnewpage) - [browserContext.pages()](#browsercontextpages) - [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies) @@ -361,6 +362,73 @@ will be closed. If no URLs are specified, this method returns all cookies. If URLs are specified, only cookies that affect those URLs are returned. +#### browserContext.exposeFunction(name, playwrightFunction) +- `name` <[string]> Name of the function on the window object. +- `playwrightFunction` <[function]> Callback function which will be called in Playwright's context. +- returns: <[Promise]> + +The method adds a function called `name` on the `window` object of every frame in every page in the context. +When called, the function executes `playwrightFunction` in node.js and returns a [Promise] which resolves to the return value of `playwrightFunction`. + +If the `playwrightFunction` returns a [Promise], it will be awaited. + +See [page.exposeFunction(name, playwrightFunction)](#pageexposefunctionname-playwrightfunction) for page-only version. + +> **NOTE** Functions installed via `page.exposeFunction` survive navigations. + +An example of adding an `md5` function to all pages in the context: +```js +const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'. +const crypto = require('crypto'); + +(async () => { + const browser = await webkit.launch({ headless: false }); + const context = await browser.newContext(); + await context.exposeFunction('md5', text => crypto.createHash('md5').update(text).digest('hex')); + const page = await context.newPage(); + await page.setContent(` + + +
+ `); + await page.click('button'); +})(); +``` + +An example of adding a `window.readfile` function to all pages in the context: + +```js +const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'. +const fs = require('fs'); + +(async () => { + const browser = await chromium.launch(); + const context = await browser.newContext(); + await context.exposeFunction('readfile', async filePath => { + return new Promise((resolve, reject) => { + fs.readFile(filePath, 'utf8', (err, text) => { + if (err) + reject(err); + else + resolve(text); + }); + }); + }); + const page = await context.newPage(); + page.on('console', msg => console.log(msg.text())); + await page.evaluate(async () => { + // use window.readfile to read contents of a file + const content = await window.readfile('/etc/hosts'); + console.log(content); + }); + await browser.close(); +})(); +``` + #### browserContext.newPage() - returns: <[Promise]<[Page]>> @@ -1007,36 +1075,38 @@ await resultHandle.dispose(); - `playwrightFunction` <[function]> Callback function which will be called in Playwright's context. - returns: <[Promise]> -The method adds a function called `name` on the page's `window` object. +The method adds a function called `name` on the `window` object of every frame in the page. When called, the function executes `playwrightFunction` in node.js and returns a [Promise] which resolves to the return value of `playwrightFunction`. If the `playwrightFunction` returns a [Promise], it will be awaited. +See [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction) for context-wide exposed function. + > **NOTE** Functions installed via `page.exposeFunction` survive navigations. -An example of adding an `md5` function into the page: +An example of adding an `md5` function to the page: ```js -const { firefox } = require('playwright'); // Or 'chromium' or 'webkit'. +const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'. const crypto = require('crypto'); (async () => { - const browser = await firefox.launch(); + const browser = await webkit.launch({ headless: false }); const page = await browser.newPage(); - page.on('console', msg => console.log(msg.text())); - await page.exposeFunction('md5', text => - crypto.createHash('md5').update(text).digest('hex') - ); - await page.evaluate(async () => { - // use window.md5 to compute hashes - const myString = 'PLAYWRIGHT'; - const myHash = await window.md5(myString); - console.log(`md5 of ${myString} is ${myHash}`); - }); - await browser.close(); + await page.exposeFunction('md5', text => crypto.createHash('md5').update(text).digest('hex')); + await page.setContent(` + + +
+ `); + await page.click('button'); })(); ``` -An example of adding a `window.readfile` function into the page: +An example of adding a `window.readfile` function to the page: ```js const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'. @@ -3624,6 +3694,7 @@ const backgroundPage = await backroundPageTarget.page(); - [browserContext.clearPermissions()](#browsercontextclearpermissions) - [browserContext.close()](#browsercontextclose) - [browserContext.cookies([...urls])](#browsercontextcookiesurls) +- [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction) - [browserContext.newPage()](#browsercontextnewpage) - [browserContext.pages()](#browsercontextpages) - [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies) diff --git a/src/browserContext.ts b/src/browserContext.ts index 85c51dc90f..54fdd05a02 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { Page } from './page'; +import { Page, PageBinding } from './page'; import * as network from './network'; import * as types from './types'; import { helper } from './helper'; @@ -47,11 +47,13 @@ export interface BrowserContext { setGeolocation(geolocation: types.Geolocation | null): Promise; setExtraHTTPHeaders(headers: network.Headers): Promise; addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]): Promise; + exposeFunction(name: string, playwrightFunction: Function): Promise; close(): Promise; _existingPages(): Page[]; readonly _timeoutSettings: TimeoutSettings; readonly _options: BrowserContextOptions; + readonly _pageBindings: Map; } export function assertBrowserContextIsNotOwned(context: BrowserContext) { diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 5a7ddf5b97..8c627439ce 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -20,7 +20,7 @@ import { Events as CommonEvents } from '../events'; import { assert, helper, debugError } from '../helper'; import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext'; import { CRConnection, ConnectionEvents, CRSession } from './crConnection'; -import { Page, PageEvent } from '../page'; +import { Page, PageEvent, PageBinding } from '../page'; import { CRTarget } from './crTarget'; import { Protocol } from './protocol'; import { CRPage } from './crPage'; @@ -204,6 +204,7 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo readonly _options: BrowserContextOptions; readonly _timeoutSettings: TimeoutSettings; readonly _evaluateOnNewDocumentSources: string[]; + readonly _pageBindings = new Map(); private _closed = false; constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) { @@ -325,6 +326,19 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo await (page._delegate as CRPage).evaluateOnNewDocument(source); } + async exposeFunction(name: string, playwrightFunction: Function): Promise { + for (const page of this._existingPages()) { + if (page._pageBindings.has(name)) + throw new Error(`Function "${name}" has been already registered in one of the pages`); + } + if (this._pageBindings.has(name)) + throw new Error(`Function "${name}" has been already registered`); + const binding = new PageBinding(name, playwrightFunction); + this._pageBindings.set(name, binding); + for (const page of this._existingPages()) + await (page._delegate as CRPage).exposeBinding(binding); + } + async close() { if (this._closed) return; diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index fd70da0338..c4d99aa652 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -23,7 +23,7 @@ import * as network from '../network'; import { CRSession, CRConnection } from './crConnection'; import { EVALUATION_SCRIPT_URL, CRExecutionContext } from './crExecutionContext'; import { CRNetworkManager } from './crNetworkManager'; -import { Page, Worker } from '../page'; +import { Page, Worker, PageBinding } from '../page'; import { Protocol } from './protocol'; import { Events } from '../events'; import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crProtocolHelper'; @@ -120,6 +120,8 @@ export class CRPage implements PageDelegate { if (options.geolocation) promises.push(this._client.send('Emulation.setGeolocationOverride', options.geolocation)); promises.push(this.updateExtraHTTPHeaders()); + for (const binding of this._browserContext._pageBindings.values()) + promises.push(this._initBinding(binding)); for (const source of this._browserContext._evaluateOnNewDocumentSources) promises.push(this.evaluateOnNewDocument(source)); await Promise.all(promises); @@ -276,10 +278,16 @@ export class CRPage implements PageDelegate { this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace)); } - async exposeBinding(name: string, bindingFunction: string) { - await this._client.send('Runtime.addBinding', {name: name}); - await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: bindingFunction}); - await Promise.all(this._page.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); + async exposeBinding(binding: PageBinding) { + await this._initBinding(binding); + await Promise.all(this._page.frames().map(frame => frame.evaluate(binding.source).catch(debugError))); + } + + async _initBinding(binding: PageBinding) { + await Promise.all([ + this._client.send('Runtime.addBinding', { name: binding.name }), + this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: binding.source }) + ]); } _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) { diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 95fc7ef6db..059dca2f6c 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -21,7 +21,7 @@ import { Events } from '../events'; import { assert, helper, RegisteredListener, debugError } from '../helper'; import * as network from '../network'; import * as types from '../types'; -import { Page, PageEvent } from '../page'; +import { Page, PageEvent, PageBinding } from '../page'; import { ConnectionEvents, FFConnection, FFSessionEvents, FFSession } from './ffConnection'; import { FFPage } from './ffPage'; import * as platform from '../platform'; @@ -265,6 +265,7 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo readonly _timeoutSettings: TimeoutSettings; private _closed = false; private readonly _evaluateOnNewDocumentSources: string[]; + readonly _pageBindings = new Map(); constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) { super(); @@ -368,6 +369,18 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo await this._browser._connection.send('Browser.addScriptToEvaluateOnNewDocument', { browserContextId: this._browserContextId || undefined, script: source }); } + async exposeFunction(name: string, playwrightFunction: Function): Promise { + for (const page of this._existingPages()) { + if (page._pageBindings.has(name)) + throw new Error(`Function "${name}" has been already registered in one of the pages`); + } + if (this._pageBindings.has(name)) + throw new Error(`Function "${name}" has been already registered`); + const binding = new PageBinding(name, playwrightFunction); + this._pageBindings.set(name, binding); + throw new Error('Not implemented'); + } + async close() { if (this._closed) return; diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index ad5d60ba3a..ea9a271f09 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -20,7 +20,7 @@ import { helper, RegisteredListener, debugError, assert } from '../helper'; import * as dom from '../dom'; import { FFSession } from './ffConnection'; import { FFExecutionContext } from './ffExecutionContext'; -import { Page, PageDelegate, Worker } from '../page'; +import { Page, PageDelegate, Worker, PageBinding } from '../page'; import { FFNetworkManager, headersArray } from './ffNetworkManager'; import { Events } from '../events'; import * as dialog from '../dialog'; @@ -233,10 +233,10 @@ export class FFPage implements PageDelegate { this._page._didCrash(); } - async exposeBinding(name: string, bindingFunction: string): Promise { - await this._session.send('Page.addBinding', {name: name}); - await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: bindingFunction}); - await Promise.all(this._page.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); + async exposeBinding(binding: PageBinding) { + await this._session.send('Page.addBinding', {name: binding.name}); + await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: binding.source}); + await Promise.all(this._page.frames().map(frame => frame.evaluate(binding.source).catch(debugError))); } didClose() { diff --git a/src/page.ts b/src/page.ts index e664b0d578..16bb044288 100644 --- a/src/page.ts +++ b/src/page.ts @@ -39,7 +39,7 @@ export interface PageDelegate { reload(): Promise; goBack(): Promise; goForward(): Promise; - exposeBinding(name: string, bindingFunction: string): Promise; + exposeBinding(binding: PageBinding): Promise; evaluateOnNewDocument(source: string): Promise; closePage(runBeforeUnload: boolean): Promise; @@ -117,7 +117,7 @@ export class Page extends platform.EventEmitter { readonly _timeoutSettings: TimeoutSettings; readonly _delegate: PageDelegate; readonly _state: PageState; - private _pageBindings = new Map(); + readonly _pageBindings = new Map(); readonly _screenshotter: Screenshotter; readonly _frameManager: frames.FrameManager; readonly accessibility: accessibility.Accessibility; @@ -256,26 +256,12 @@ export class Page extends platform.EventEmitter { async exposeFunction(name: string, playwrightFunction: Function) { if (this._pageBindings.has(name)) - throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`); - this._pageBindings.set(name, playwrightFunction); - await this._delegate.exposeBinding(name, helper.evaluationString(addPageBinding, name)); - - function addPageBinding(bindingName: string) { - const binding = (window as any)[bindingName]; - (window as any)[bindingName] = (...args: any[]) => { - const me = (window as any)[bindingName]; - let callbacks = me['callbacks']; - if (!callbacks) { - callbacks = new Map(); - me['callbacks'] = callbacks; - } - const seq = (me['lastSeq'] || 0) + 1; - me['lastSeq'] = seq; - const promise = new Promise((resolve, reject) => callbacks.set(seq, {resolve, reject})); - binding(JSON.stringify({name: bindingName, seq, args})); - return promise; - }; - } + throw new Error(`Function "${name}" has been already registered`); + if (this._browserContext._pageBindings.has(name)) + throw new Error(`Function "${name}" has been already registered in the browser context`); + const binding = new PageBinding(name, playwrightFunction); + this._pageBindings.set(name, binding); + await this._delegate.exposeBinding(binding); } setExtraHTTPHeaders(headers: network.Headers) { @@ -284,35 +270,7 @@ export class Page extends platform.EventEmitter { } async _onBindingCalled(payload: string, context: js.ExecutionContext) { - const {name, seq, args} = JSON.parse(payload); - let expression = null; - try { - const result = await this._pageBindings.get(name)!(...args); - expression = helper.evaluationString(deliverResult, name, seq, result); - } catch (error) { - if (error instanceof Error) - expression = helper.evaluationString(deliverError, name, seq, error.message, error.stack); - else - expression = helper.evaluationString(deliverErrorValue, name, seq, error); - } - context.evaluate(expression).catch(debugError); - - function deliverResult(name: string, seq: number, result: any) { - (window as any)[name]['callbacks'].get(seq).resolve(result); - (window as any)[name]['callbacks'].delete(seq); - } - - function deliverError(name: string, seq: number, message: string, stack: string) { - const error = new Error(message); - error.stack = stack; - (window as any)[name]['callbacks'].get(seq).reject(error); - (window as any)[name]['callbacks'].delete(seq); - } - - function deliverErrorValue(name: string, seq: number, value: any) { - (window as any)[name]['callbacks'].get(seq).reject(value); - (window as any)[name]['callbacks'].delete(seq); - } + await PageBinding.dispatch(this, payload, context); } _addConsoleMessage(type: string, args: js.JSHandle[], location: ConsoleMessageLocation, text?: string) { @@ -609,3 +567,67 @@ export class Worker extends platform.EventEmitter { return (await this._executionContextPromise).evaluateHandle(pageFunction, ...args as any); } } + +export class PageBinding { + readonly name: string; + readonly playwrightFunction: Function; + readonly source: string; + + constructor(name: string, playwrightFunction: Function) { + this.name = name; + this.playwrightFunction = playwrightFunction; + this.source = helper.evaluationString(addPageBinding, name); + } + + static async dispatch(page: Page, payload: string, context: js.ExecutionContext) { + const {name, seq, args} = JSON.parse(payload); + let expression = null; + try { + let binding = page._pageBindings.get(name); + if (!binding) + binding = page.context()._pageBindings.get(name); + const result = await binding!.playwrightFunction(...args); + expression = helper.evaluationString(deliverResult, name, seq, result); + } catch (error) { + if (error instanceof Error) + expression = helper.evaluationString(deliverError, name, seq, error.message, error.stack); + else + expression = helper.evaluationString(deliverErrorValue, name, seq, error); + } + context.evaluate(expression).catch(debugError); + + function deliverResult(name: string, seq: number, result: any) { + (window as any)[name]['callbacks'].get(seq).resolve(result); + (window as any)[name]['callbacks'].delete(seq); + } + + function deliverError(name: string, seq: number, message: string, stack: string) { + const error = new Error(message); + error.stack = stack; + (window as any)[name]['callbacks'].get(seq).reject(error); + (window as any)[name]['callbacks'].delete(seq); + } + + function deliverErrorValue(name: string, seq: number, value: any) { + (window as any)[name]['callbacks'].get(seq).reject(value); + (window as any)[name]['callbacks'].delete(seq); + } + } +} + +function addPageBinding(bindingName: string) { + const binding = (window as any)[bindingName]; + (window as any)[bindingName] = (...args: any[]) => { + const me = (window as any)[bindingName]; + let callbacks = me['callbacks']; + if (!callbacks) { + callbacks = new Map(); + me['callbacks'] = callbacks; + } + const seq = (me['lastSeq'] || 0) + 1; + me['lastSeq'] = seq; + const promise = new Promise((resolve, reject) => callbacks.set(seq, {resolve, reject})); + binding(JSON.stringify({name: bindingName, seq, args})); + return promise; + }; +} diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index f43aed611e..b0684d4070 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -19,7 +19,7 @@ import { Browser, createPageInNewContext } from '../browser'; import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext'; import { assert, helper, RegisteredListener, debugError } from '../helper'; import * as network from '../network'; -import { Page, PageEvent } from '../page'; +import { Page, PageBinding, PageEvent } from '../page'; import { ConnectionTransport, SlowMoTransport } from '../transport'; import * as types from '../types'; import { Events } from '../events'; @@ -187,6 +187,7 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo readonly _timeoutSettings: TimeoutSettings; private _closed = false; readonly _evaluateOnNewDocumentSources: string[]; + readonly _pageBindings = new Map(); constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) { super(); @@ -297,6 +298,19 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo await (page._delegate as WKPage)._updateBootstrapScript(); } + async exposeFunction(name: string, playwrightFunction: Function): Promise { + for (const page of this._existingPages()) { + if (page._pageBindings.has(name)) + throw new Error(`Function "${name}" has been already registered in one of the pages`); + } + if (this._pageBindings.has(name)) + throw new Error(`Function "${name}" has been already registered`); + const binding = new PageBinding(name, playwrightFunction); + this._pageBindings.set(name, binding); + for (const page of this._existingPages()) + await (page._delegate as WKPage).exposeBinding(binding); + } + async close() { if (this._closed) return; diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index e9adfb2eb6..de923b1881 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -24,7 +24,7 @@ import { Events } from '../events'; import { WKExecutionContext } from './wkExecutionContext'; import { WKInterceptableRequest } from './wkInterceptableRequest'; import { WKWorkers } from './wkWorkers'; -import { Page, PageDelegate } from '../page'; +import { Page, PageDelegate, PageBinding } from '../page'; import { Protocol } from './protocol'; import * as dialog from '../dialog'; import { RawMouseImpl, RawKeyboardImpl } from './wkInput'; @@ -52,7 +52,7 @@ export class WKPage implements PageDelegate { private readonly _contextIdToContext: Map; private _mainFrameContextId?: number; private _sessionListeners: RegisteredListener[] = []; - private readonly _bootstrapScripts: string[] = []; + private readonly _evaluateOnNewDocumentSources: string[] = []; private readonly _browserContext: WKBrowserContext; constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPageProxy | null) { @@ -140,6 +140,8 @@ export class WKPage implements PageDelegate { if (this._page._state.mediaType || this._page._state.colorScheme) promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme)); promises.push(session.send('Page.setBootstrapScript', { source: this._calculateBootstrapScript() })); + for (const binding of this._browserContext._pageBindings.values()) + promises.push(this._evaluateBindingScript(binding)); if (contextOptions.bypassCSP) promises.push(session.send('Page.setBypassCSP', { enabled: true })); promises.push(session.send('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() })); @@ -461,20 +463,34 @@ export class WKPage implements PageDelegate { }); } - async exposeBinding(name: string, bindingFunction: string): Promise { - const script = `self.${name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${bindingFunction}`; - this._bootstrapScripts.unshift(script); + async exposeBinding(binding: PageBinding): Promise { await this._updateBootstrapScript(); + await this._evaluateBindingScript(binding); + } + + private async _evaluateBindingScript(binding: PageBinding): Promise { + const script = this._bindingToScript(binding); await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(debugError))); } async evaluateOnNewDocument(script: string): Promise { - this._bootstrapScripts.push(script); + this._evaluateOnNewDocumentSources.push(script); await this._updateBootstrapScript(); } + private _bindingToScript(binding: PageBinding): string { + return `self.${binding.name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${binding.source}`; + } + private _calculateBootstrapScript(): string { - return [...this._browserContext._evaluateOnNewDocumentSources, ...this._bootstrapScripts].join(';'); + const scripts: string[] = []; + for (const binding of this._browserContext._pageBindings.values()) + scripts.push(this._bindingToScript(binding)); + for (const binding of this._page._pageBindings.values()) + scripts.push(this._bindingToScript(binding)); + scripts.push(...this._browserContext._evaluateOnNewDocumentSources); + scripts.push(...this._evaluateOnNewDocumentSources); + return scripts.join(';'); } async _updateBootstrapScript(): Promise { diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js index e7538580bb..35b5506a76 100644 --- a/test/browsercontext.spec.js +++ b/test/browsercontext.spec.js @@ -306,6 +306,48 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, FF }); }); + describe('BrowserContext.exposeFunction', () => { + it.fail(CHROMIUM || FFOX)('should work', async({browser, server}) => { + const context = await browser.newContext(); + await context.exposeFunction('add', (a, b) => a + b); + const page = await context.newPage(); + await page.exposeFunction('mul', (a, b) => a * b); + const result = await page.evaluate(async function() { + return { mul: await mul(9, 4), add: await add(9, 4) }; + }); + expect(result).toEqual({ mul: 36, add: 13 }); + await context.close(); + }); + it.fail(FFOX)('should throw for duplicate registrations', async({browser, server}) => { + const context = await browser.newContext(); + await context.exposeFunction('foo', () => {}); + await context.exposeFunction('bar', () => {}); + let error = await context.exposeFunction('foo', () => {}).catch(e => e); + expect(error.message).toBe('Function "foo" has been already registered'); + const page = await context.newPage(); + error = await page.exposeFunction('foo', () => {}).catch(e => e); + expect(error.message).toBe('Function "foo" has been already registered in the browser context'); + await page.exposeFunction('baz', () => {}); + error = await context.exposeFunction('baz', () => {}).catch(e => e); + expect(error.message).toBe('Function "baz" has been already registered in one of the pages'); + await context.close(); + }); + it.skip(FFOX)('should be callable from-inside addInitScript', async({browser, server}) => { + const context = await browser.newContext(); + let args = []; + await context.exposeFunction('woof', function(arg) { + args.push(arg); + }); + await context.addInitScript(() => woof('context')); + const page = await context.newPage(); + await page.addInitScript(() => woof('page')); + args = []; + await page.reload(); + expect(args).toEqual(['context', 'page']); + await context.close(); + }); + }); + describe('Events.BrowserContext.Page', function() { it('should report when a new page is created and closed', async({browser, server}) => { const context = await browser.newContext(); diff --git a/test/evaluation.spec.js b/test/evaluation.spec.js index 154fcaad1e..1f376d390b 100644 --- a/test/evaluation.spec.js +++ b/test/evaluation.spec.js @@ -274,7 +274,15 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) await page.evaluate(() => { window.JSON.stringify = null; window.JSON = null; }); const result = await page.evaluate(() => ({abc: 123})); expect(result).toEqual({abc: 123}); - }) + }); + it.fail(WEBKIT)('should await promise from popup', async function({page, server}) { + await page.goto(server.EMPTY_PAGE); + const result = await page.evaluate(() => { + const win = window.open('about:blank'); + return new win.Promise(f => f(42)); + }); + expect(result).toBe(42); + }); }); describe('Page.addInitScript', function() { diff --git a/test/popup.spec.js b/test/popup.spec.js index f14e63a1e3..419b61858f 100644 --- a/test/popup.spec.js +++ b/test/popup.spec.js @@ -86,6 +86,18 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE await context.close(); expect(injected).toBe(123); }); + it.fail(CHROMIUM || FFOX)('should expose function from browser context', async function({browser, server}) { + const context = await browser.newContext(); + await context.exposeFunction('add', (a, b) => a + b); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + const added = await page.evaluate(async () => { + const win = window.open('about:blank'); + return win.add(9, 4); + }); + await context.close(); + expect(added).toBe(13); + }); }); describe('Page.Events.Popup', function() {