feat(exposeBinding): a more powerful exposeFunction with source attribution (#2263)
This commit is contained in:
		
							parent
							
								
									40ea0dd23b
								
							
						
					
					
						commit
						2bd427ad1d
					
				
							
								
								
									
										87
									
								
								docs/api.md
								
								
								
								
							
							
						
						
									
										87
									
								
								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(` | ||||
|     <script> | ||||
|       async function onClick() { | ||||
|         document.querySelector('div').textContent = await window.pageURL(); | ||||
|       } | ||||
|     </script> | ||||
|     <button onclick="onClick()">Click me</button> | ||||
|     <div></div> | ||||
|   `); | ||||
|   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(` | ||||
|     <script> | ||||
|       async function onClick() { | ||||
|         document.querySelector('div').textContent = await window.pageURL(); | ||||
|       } | ||||
|     </script> | ||||
|     <button onclick="onClick()">Click me</button> | ||||
|     <div></div> | ||||
|   `); | ||||
|   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) | ||||
|  |  | |||
|  | @ -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<void>; | ||||
|   setHTTPCredentials(httpCredentials: types.Credentials | null): Promise<void>; | ||||
|   addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void>; | ||||
|   exposeBinding(name: string, playwrightBinding: FunctionWithSource): Promise<void>; | ||||
|   exposeFunction(name: string, playwrightFunction: Function): Promise<void>; | ||||
|   route(url: types.URLMatch, handler: network.RouteHandler): Promise<void>; | ||||
|   unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise<void>; | ||||
|  | @ -126,11 +128,27 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements | |||
|   abstract setExtraHTTPHeaders(headers: network.Headers): Promise<void>; | ||||
|   abstract setOffline(offline: boolean): Promise<void>; | ||||
|   abstract addInitScript(script: string | Function | { path?: string | undefined; content?: string | undefined; }, arg?: any): Promise<void>; | ||||
|   abstract exposeFunction(name: string, playwrightFunction: Function): Promise<void>; | ||||
|   abstract _doExposeBinding(binding: PageBinding): Promise<void>; | ||||
|   abstract route(url: types.URLMatch, handler: network.RouteHandler): Promise<void>; | ||||
|   abstract unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise<void>; | ||||
|   abstract close(): Promise<void>; | ||||
| 
 | ||||
|   async exposeFunction(name: string, playwrightFunction: Function): Promise<void> { | ||||
|     await this.exposeBinding(name, (options, ...args: any) => playwrightFunction(...args)); | ||||
|   } | ||||
| 
 | ||||
|   async exposeBinding(name: string, playwrightBinding: FunctionWithSource): Promise<void> { | ||||
|     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) { | ||||
|  |  | |||
|  | @ -405,15 +405,7 @@ export class CRBrowserContext extends BrowserContextBase { | |||
|       await (page._delegate as CRPage).evaluateOnNewDocument(source); | ||||
|   } | ||||
| 
 | ||||
|   async exposeFunction(name: string, playwrightFunction: Function): Promise<void> { | ||||
|     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); | ||||
|   } | ||||
|  |  | |||
|  | @ -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<void> { | ||||
|     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<void> { | ||||
|  |  | |||
|  | @ -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<string, Frame>(); | ||||
|  |  | |||
							
								
								
									
										16
									
								
								src/page.ts
								
								
								
								
							
							
						
						
									
										16
									
								
								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) | ||||
|  |  | |||
|  | @ -308,15 +308,7 @@ export class WKBrowserContext extends BrowserContextBase { | |||
|       await (page._delegate as WKPage)._updateBootstrapScript(); | ||||
|   } | ||||
| 
 | ||||
|   async exposeFunction(name: string, playwrightFunction: Function): Promise<void> { | ||||
|     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); | ||||
|   } | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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) { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue