diff --git a/docs/api.md b/docs/api.md
index 7d46f6b538..bc71f59036 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -297,6 +297,7 @@ await context.close();
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
- [browserContext.close()](#browsercontextclose)
- [browserContext.cookies([urls])](#browsercontextcookiesurls)
+- [browserContext.exposeBinding(name, playwrightBinding)](#browsercontextexposebindingname-playwrightbinding)
- [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction)
- [browserContext.grantPermissions(permissions[][, options])](#browsercontextgrantpermissionspermissions-options)
- [browserContext.newPage()](#browsercontextnewpage)
@@ -421,20 +422,54 @@ 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.exposeBinding(name, playwrightBinding)
+- `name` <[string]> Name of the function on the window object.
+- `playwrightBinding` <[function]> Callback function that will be called in the 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 `playwrightBinding` in Node.js and returns a [Promise] which resolves to the return value of `playwrightBinding`.
+If the `playwrightBinding` returns a [Promise], it will be awaited.
+
+The first argument of the `playwrightBinding` function contains information about the caller:
+`{ browserContext: BrowserContext, page: Page, frame: Frame }`.
+
+See [page.exposeBinding(name, playwrightBinding)](#pageexposebindingname-playwrightbinding) for page-only version.
+
+An example of exposing page URL to all frames in all pages in the context:
+```js
+const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
+
+(async () => {
+ const browser = await webkit.launch({ headless: false });
+ const context = await browser.newContext();
+ await context.exposeBinding('pageURL', ({ page }) => page.url());
+ const page = await context.newPage();
+ await page.setContent(`
+
+
+
+ `);
+ await page.click('button');
+})();
+```
+
#### browserContext.exposeFunction(name, playwrightFunction)
- `name` <[string]> Name of the function on the window object.
- `playwrightFunction` <[function]> Callback function that will be called in the 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`.
+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'.
@@ -678,6 +713,7 @@ page.removeListener('request', logRequest);
- [page.emulateMedia(options)](#pageemulatemediaoptions)
- [page.evaluate(pageFunction[, arg])](#pageevaluatepagefunction-arg)
- [page.evaluateHandle(pageFunction[, arg])](#pageevaluatehandlepagefunction-arg)
+- [page.exposeBinding(name, playwrightBinding)](#pageexposebindingname-playwrightbinding)
- [page.exposeFunction(name, playwrightFunction)](#pageexposefunctionname-playwrightfunction)
- [page.fill(selector, value[, options])](#pagefillselector-value-options)
- [page.focus(selector[, options])](#pagefocusselector-options)
@@ -1165,13 +1201,51 @@ console.log(await resultHandle.jsonValue());
await resultHandle.dispose();
```
+#### page.exposeBinding(name, playwrightBinding)
+- `name` <[string]> Name of the function on the window object.
+- `playwrightBinding` <[function]> Callback function that will be called in the Playwright's context.
+- returns: <[Promise]>
+
+The method adds a function called `name` on the `window` object of every frame in this page.
+When called, the function executes `playwrightBinding` in Node.js and returns a [Promise] which resolves to the return value of `playwrightBinding`.
+If the `playwrightBinding` returns a [Promise], it will be awaited.
+
+The first argument of the `playwrightBinding` function contains information about the caller:
+`{ browserContext: BrowserContext, page: Page, frame: Frame }`.
+
+See [browserContext.exposeBinding(name, playwrightBinding)](#browsercontextexposebindingname-playwrightbinding) for the context-wide version.
+
+> **NOTE** Functions installed via `page.exposeBinding` survive navigations.
+
+An example of exposing page URL to all frames in a page:
+```js
+const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
+
+(async () => {
+ const browser = await webkit.launch({ headless: false });
+ const context = await browser.newContext();
+ const page = await context.newPage();
+ await page.exposeBinding('pageURL', ({ page }) => page.url());
+ await page.setContent(`
+
+
+
+ `);
+ await page.click('button');
+})();
+```
+
#### page.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 the page.
-When called, the function executes `playwrightFunction` in node.js and returns a [Promise] which resolves to the return value of `playwrightFunction`.
+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.
@@ -1720,7 +1794,7 @@ const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
})();
```
-To pass an argument from node.js to the predicate of `page.waitForFunction` function:
+To pass an argument from Node.js to the predicate of `page.waitForFunction` function:
```js
const selector = '.foo';
@@ -2389,7 +2463,7 @@ const { firefox } = require('playwright'); // Or 'chromium' or 'webkit'.
})();
```
-To pass an argument from node.js to the predicate of `frame.waitForFunction` function:
+To pass an argument from Node.js to the predicate of `frame.waitForFunction` function:
```js
const selector = '.foo';
@@ -4027,6 +4101,7 @@ const backgroundPage = await context.waitForEvent('backgroundpage');
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
- [browserContext.close()](#browsercontextclose)
- [browserContext.cookies([urls])](#browsercontextcookiesurls)
+- [browserContext.exposeBinding(name, playwrightBinding)](#browsercontextexposebindingname-playwrightbinding)
- [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction)
- [browserContext.grantPermissions(permissions[][, options])](#browsercontextgrantpermissionspermissions-options)
- [browserContext.newPage()](#browsercontextnewpage)
diff --git a/src/browserContext.ts b/src/browserContext.ts
index d2d69223b5..234cf0d96c 100644
--- a/src/browserContext.ts
+++ b/src/browserContext.ts
@@ -25,6 +25,7 @@ import { ExtendedEventEmitter } from './extendedEventEmitter';
import { Download } from './download';
import { BrowserBase } from './browser';
import { Log, InnerLogger, Logger, RootLogger } from './logger';
+import { FunctionWithSource } from './frames';
export type BrowserContextOptions = {
viewport?: types.Size | null,
@@ -62,6 +63,7 @@ export interface BrowserContext extends InnerLogger {
setOffline(offline: boolean): Promise;
setHTTPCredentials(httpCredentials: types.Credentials | null): Promise;
addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise;
+ exposeBinding(name: string, playwrightBinding: FunctionWithSource): Promise;
exposeFunction(name: string, playwrightFunction: Function): Promise;
route(url: types.URLMatch, handler: network.RouteHandler): Promise;
unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise;
@@ -126,11 +128,27 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
abstract setExtraHTTPHeaders(headers: network.Headers): Promise;
abstract setOffline(offline: boolean): Promise;
abstract addInitScript(script: string | Function | { path?: string | undefined; content?: string | undefined; }, arg?: any): Promise;
- abstract exposeFunction(name: string, playwrightFunction: Function): Promise;
+ abstract _doExposeBinding(binding: PageBinding): Promise;
abstract route(url: types.URLMatch, handler: network.RouteHandler): Promise;
abstract unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise;
abstract close(): Promise;
+ async exposeFunction(name: string, playwrightFunction: Function): Promise {
+ await this.exposeBinding(name, (options, ...args: any) => playwrightFunction(...args));
+ }
+
+ async exposeBinding(name: string, playwrightBinding: FunctionWithSource): Promise {
+ for (const page of this.pages()) {
+ 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, playwrightBinding);
+ this._pageBindings.set(name, binding);
+ this._doExposeBinding(binding);
+ }
+
async grantPermissions(permissions: string[], options?: { origin?: string }) {
let origin = '*';
if (options && options.origin) {
diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts
index f2f12397f4..cdc90d0a53 100644
--- a/src/chromium/crBrowser.ts
+++ b/src/chromium/crBrowser.ts
@@ -405,15 +405,7 @@ export class CRBrowserContext extends BrowserContextBase {
await (page._delegate as CRPage).evaluateOnNewDocument(source);
}
- async exposeFunction(name: string, playwrightFunction: Function): Promise {
- for (const page of this.pages()) {
- 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);
+ async _doExposeBinding(binding: PageBinding) {
for (const page of this.pages())
await (page._delegate as CRPage).exposeBinding(binding);
}
diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts
index a9559be410..bf5f3fd4fb 100644
--- a/src/firefox/ffBrowser.ts
+++ b/src/firefox/ffBrowser.ts
@@ -302,16 +302,8 @@ export class FFBrowserContext extends BrowserContextBase {
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.pages()) {
- 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);
- await this._browser._connection.send('Browser.addBinding', { browserContextId: this._browserContextId || undefined, name, script: binding.source });
+ async _doExposeBinding(binding: PageBinding) {
+ await this._browser._connection.send('Browser.addBinding', { browserContextId: this._browserContextId || undefined, name: binding.name, script: binding.source });
}
async route(url: types.URLMatch, handler: network.RouteHandler): Promise {
diff --git a/src/frames.ts b/src/frames.ts
index 5dcac12074..6dc08fda0e 100644
--- a/src/frames.ts
+++ b/src/frames.ts
@@ -28,6 +28,7 @@ import { Page } from './page';
import { selectors } from './selectors';
import * as types from './types';
import { waitForTimeoutWasUsed } from './hints';
+import { BrowserContext } from './browserContext';
type ContextType = 'main' | 'utility';
type ContextData = {
@@ -46,6 +47,8 @@ export type GotoResult = {
type ConsoleTagHandler = () => void;
+export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame}, ...args: any) => any;
+
export class FrameManager {
private _page: Page;
private _frames = new Map();
diff --git a/src/page.ts b/src/page.ts
index 3004593db2..50cd7e1b98 100644
--- a/src/page.ts
+++ b/src/page.ts
@@ -253,11 +253,15 @@ export class Page extends ExtendedEventEmitter implements InnerLogger {
}
async exposeFunction(name: string, playwrightFunction: Function) {
+ await this.exposeBinding(name, (options, ...args: any) => playwrightFunction(...args));
+ }
+
+ async exposeBinding(name: string, playwrightBinding: frames.FunctionWithSource) {
if (this._pageBindings.has(name))
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);
+ const binding = new PageBinding(name, playwrightBinding);
this._pageBindings.set(name, binding);
await this._delegate.exposeBinding(binding);
}
@@ -267,7 +271,7 @@ export class Page extends ExtendedEventEmitter implements InnerLogger {
return this._delegate.updateExtraHTTPHeaders();
}
- async _onBindingCalled(payload: string, context: js.ExecutionContext) {
+ async _onBindingCalled(payload: string, context: dom.FrameExecutionContext) {
await PageBinding.dispatch(this, payload, context);
}
@@ -580,23 +584,23 @@ export class Worker extends EventEmitter {
export class PageBinding {
readonly name: string;
- readonly playwrightFunction: Function;
+ readonly playwrightFunction: frames.FunctionWithSource;
readonly source: string;
- constructor(name: string, playwrightFunction: Function) {
+ constructor(name: string, playwrightFunction: frames.FunctionWithSource) {
this.name = name;
this.playwrightFunction = playwrightFunction;
this.source = helper.evaluationString(addPageBinding, name);
}
- static async dispatch(page: Page, payload: string, context: js.ExecutionContext) {
+ static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
const {name, seq, args} = JSON.parse(payload);
let expression = null;
try {
let binding = page._pageBindings.get(name);
if (!binding)
binding = page._browserContext._pageBindings.get(name);
- const result = await binding!.playwrightFunction(...args);
+ const result = await binding!.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args);
expression = helper.evaluationString(deliverResult, name, seq, result);
} catch (error) {
if (error instanceof Error)
diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts
index 87f977cc2f..ac8e274956 100644
--- a/src/webkit/wkBrowser.ts
+++ b/src/webkit/wkBrowser.ts
@@ -308,15 +308,7 @@ export class WKBrowserContext extends BrowserContextBase {
await (page._delegate as WKPage)._updateBootstrapScript();
}
- async exposeFunction(name: string, playwrightFunction: Function): Promise {
- for (const page of this.pages()) {
- 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);
+ async _doExposeBinding(binding: PageBinding) {
for (const page of this.pages())
await (page._delegate as WKPage).exposeBinding(binding);
}
diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js
index 9d9eea4334..e124cb28a7 100644
--- a/test/browsercontext.spec.js
+++ b/test/browsercontext.spec.js
@@ -326,6 +326,26 @@ describe('BrowserContext.pages()', function() {
});
});
+describe('BrowserContext.exposeBinding', () => {
+ it('should work', async({browser}) => {
+ const context = await browser.newContext();
+ let bindingSource;
+ await context.exposeBinding('add', (source, a, b) => {
+ bindingSource = source;
+ return a + b;
+ });
+ const page = await context.newPage();
+ const result = await page.evaluate(async function() {
+ return add(5, 6);
+ });
+ expect(bindingSource.context).toBe(context);
+ expect(bindingSource.page).toBe(page);
+ expect(bindingSource.frame).toBe(page.mainFrame());
+ expect(result).toEqual(11);
+ await context.close();
+ });
+});
+
describe('BrowserContext.exposeFunction', () => {
it('should work', async({browser, server}) => {
const context = await browser.newContext();
diff --git a/test/page.spec.js b/test/page.spec.js
index b90585f7c5..63a1068a23 100644
--- a/test/page.spec.js
+++ b/test/page.spec.js
@@ -405,6 +405,26 @@ describe('Page.waitForResponse', function() {
});
});
+describe('Page.exposeBinding', () => {
+ it('should work', async({browser}) => {
+ const context = await browser.newContext();
+ const page = await context.newPage();
+ let bindingSource;
+ await page.exposeBinding('add', (source, a, b) => {
+ bindingSource = source;
+ return a + b;
+ });
+ const result = await page.evaluate(async function() {
+ return add(5, 6);
+ });
+ expect(bindingSource.context).toBe(context);
+ expect(bindingSource.page).toBe(page);
+ expect(bindingSource.frame).toBe(page.mainFrame());
+ expect(result).toEqual(11);
+ await context.close();
+ });
+});
+
describe('Page.exposeFunction', function() {
it('should work', async({page, server}) => {
await page.exposeFunction('compute', function(a, b) {