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() {