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