From 5e42029fceb57da79cacb53a97af62ecd6b95293 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 1 Oct 2020 22:47:31 -0700 Subject: [PATCH] api: allow exposeBinding to pass handles (#4030) This adds an option `{ handle: true }` to pass a single handle instead of arbitrary json values. --- docs/api.md | 46 +++++++++++++++--- src/client/browserContext.ts | 21 ++++---- src/client/frame.ts | 2 - src/client/page.ts | 25 ++++++---- src/dispatchers/browserContextDispatcher.ts | 4 +- src/dispatchers/pageDispatcher.ts | 12 +++-- src/protocol/channels.ts | 9 ++-- src/protocol/protocol.yml | 5 +- src/protocol/validator.ts | 2 + src/server/browserContext.ts | 4 +- src/server/page.ts | 40 +++++++++++++--- test/browsercontext-expose-function.spec.ts | 16 +++++++ test/page-expose-function.spec.ts | 53 +++++++++++++++++++++ utils/generate_types/overrides.d.ts | 10 ++++ utils/generate_types/test/test.ts | 5 ++ 15 files changed, 203 insertions(+), 51 deletions(-) diff --git a/docs/api.md b/docs/api.md index effa99919a..d39c9c053f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -314,7 +314,7 @@ await context.close(); - [browserContext.clearPermissions()](#browsercontextclearpermissions) - [browserContext.close()](#browsercontextclose) - [browserContext.cookies([urls])](#browsercontextcookiesurls) -- [browserContext.exposeBinding(name, playwrightBinding)](#browsercontextexposebindingname-playwrightbinding) +- [browserContext.exposeBinding(name, playwrightBinding[, options])](#browsercontextexposebindingname-playwrightbinding-options) - [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction) - [browserContext.grantPermissions(permissions[][, options])](#browsercontextgrantpermissionspermissions-options) - [browserContext.newPage()](#browsercontextnewpage) @@ -443,9 +443,11 @@ 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) +#### browserContext.exposeBinding(name, playwrightBinding[, options]) - `name` <[string]> Name of the function on the window object. - `playwrightBinding` <[function]> Callback function that will be called in the Playwright's context. +- `options` <[Object]> + - `handle` <[boolean]> Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is supported. When passing by value, multiple arguments are supported. - returns: <[Promise]> The method adds a function called `name` on the `window` object of every frame in every page in the context. @@ -455,7 +457,7 @@ 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. +See [page.exposeBinding(name, playwrightBinding)](#pageexposebindingname-playwrightbinding-options) for page-only version. An example of exposing page URL to all frames in all pages in the context: ```js @@ -479,6 +481,20 @@ const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'. })(); ``` +An example of passing an element handle: +```js +await context.exposeBinding('clicked', async (source, element) => { + console.log(await element.textContent()); +}, { handle: true }); +await page.setContent(` + +
Click me
+
Or click me
+`); +``` + #### 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. @@ -735,7 +751,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.exposeBinding(name, playwrightBinding[, options])](#pageexposebindingname-playwrightbinding-options) - [page.exposeFunction(name, playwrightFunction)](#pageexposefunctionname-playwrightfunction) - [page.fill(selector, value[, options])](#pagefillselector-value-options) - [page.focus(selector[, options])](#pagefocusselector-options) @@ -1264,9 +1280,11 @@ console.log(await resultHandle.jsonValue()); await resultHandle.dispose(); ``` -#### page.exposeBinding(name, playwrightBinding) +#### page.exposeBinding(name, playwrightBinding[, options]) - `name` <[string]> Name of the function on the window object. - `playwrightBinding` <[function]> Callback function that will be called in the Playwright's context. +- `options` <[Object]> + - `handle` <[boolean]> Whether to pass the argument as a handle, instead of passing by value. When passing a handle, only one argument is supported. When passing by value, multiple arguments are supported. - returns: <[Promise]> The method adds a function called `name` on the `window` object of every frame in this page. @@ -1276,7 +1294,7 @@ 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. +See [browserContext.exposeBinding(name, playwrightBinding)](#browsercontextexposebindingname-playwrightbinding-options) for the context-wide version. > **NOTE** Functions installed via `page.exposeBinding` survive navigations. @@ -1302,6 +1320,20 @@ const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'. })(); ``` +An example of passing an element handle: +```js +await page.exposeBinding('clicked', async (source, element) => { + console.log(await element.textContent()); +}, { handle: true }); +await page.setContent(` + +
Click me
+
Or click me
+`); +``` + #### 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. @@ -4409,7 +4441,7 @@ const backgroundPage = await context.waitForEvent('backgroundpage'); - [browserContext.clearPermissions()](#browsercontextclearpermissions) - [browserContext.close()](#browsercontextclose) - [browserContext.cookies([urls])](#browsercontextcookiesurls) -- [browserContext.exposeBinding(name, playwrightBinding)](#browsercontextexposebindingname-playwrightbinding) +- [browserContext.exposeBinding(name, playwrightBinding[, options])](#browsercontextexposebindingname-playwrightbinding-options) - [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction) - [browserContext.grantPermissions(permissions[][, options])](#browsercontextgrantpermissionspermissions-options) - [browserContext.newPage()](#browsercontextnewpage) diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index b13a62b755..807eece17e 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -15,8 +15,7 @@ * limitations under the License. */ -import * as frames from './frame'; -import { Page, BindingCall } from './page'; +import { Page, BindingCall, FunctionWithSource } from './page'; import * as network from './network'; import * as channels from '../protocol/channels'; import { ChannelOwner } from './channelOwner'; @@ -34,7 +33,7 @@ export class BrowserContext extends ChannelOwner(); + readonly _bindings = new Map(); _timeoutSettings = new TimeoutSettings(); _ownerPage: Page | undefined; private _closedPromise: Promise; @@ -176,21 +175,19 @@ export class BrowserContext extends ChannelOwner { + async exposeBinding(name: string, playwrightBinding: FunctionWithSource, options: { handle?: boolean } = {}): Promise { return this._wrapApiCall('browserContext.exposeBinding', async () => { - for (const page of this.pages()) { - if (page._bindings.has(name)) - throw new Error(`Function "${name}" has been already registered in one of the pages`); - } - if (this._bindings.has(name)) - throw new Error(`Function "${name}" has been already registered`); + await this._channel.exposeBinding({ name, needsHandle: options.handle }); this._bindings.set(name, playwrightBinding); - await this._channel.exposeBinding({ name }); }); } async exposeFunction(name: string, playwrightFunction: Function): Promise { - await this.exposeBinding(name, (source, ...args) => playwrightFunction(...args)); + return this._wrapApiCall('browserContext.exposeFunction', async () => { + await this._channel.exposeBinding({ name }); + const binding: FunctionWithSource = (source, ...args) => playwrightFunction(...args); + this._bindings.set(name, binding); + }); } async route(url: URLMatch, handler: network.RouteHandler): Promise { diff --git a/src/client/frame.ts b/src/client/frame.ts index 8b1f7403b2..a823af1490 100644 --- a/src/client/frame.ts +++ b/src/client/frame.ts @@ -17,7 +17,6 @@ import { assert } from '../utils/utils'; import * as channels from '../protocol/channels'; -import { BrowserContext } from './browserContext'; import { ChannelOwner } from './channelOwner'; import { ElementHandle, convertSelectOptionValues, convertInputFiles } from './elementHandle'; import { assertMaxArguments, JSHandle, Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle'; @@ -33,7 +32,6 @@ import { urlMatches } from './clientHelper'; const fsReadFileAsync = util.promisify(fs.readFile.bind(fs)); -export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame }, ...args: any) => any; export type WaitForNavigationOptions = { timeout?: number, waitUntil?: LifecycleEvent, diff --git a/src/client/page.ts b/src/client/page.ts index 5e6e102a71..99cedd5498 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -28,9 +28,9 @@ import { Dialog } from './dialog'; import { Download } from './download'; import { ElementHandle, determineScreenshotType } from './elementHandle'; import { Worker } from './worker'; -import { Frame, FunctionWithSource, verifyLoadState, WaitForNavigationOptions } from './frame'; +import { Frame, verifyLoadState, WaitForNavigationOptions } from './frame'; import { Keyboard, Mouse } from './input'; -import { assertMaxArguments, Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle'; +import { assertMaxArguments, Func1, FuncOn, SmartHandle, serializeArgument, parseResult, JSHandle } from './jsHandle'; import { Request, Response, Route, RouteHandler, validateHeaders } from './network'; import { FileChooser } from './fileChooser'; import { Buffer } from 'buffer'; @@ -60,6 +60,7 @@ type PDFOptions = Omit & path?: string, }; type Listener = (...args: any[]) => void; +export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame }, ...args: any) => any; export class Page extends ChannelOwner { private _browserContext: BrowserContext; @@ -280,17 +281,17 @@ export class Page extends ChannelOwner playwrightFunction(...args)); + return this._wrapApiCall('page.exposeFunction', async () => { + await this._channel.exposeBinding({ name }); + const binding: FunctionWithSource = (source, ...args) => playwrightFunction(...args); + this._bindings.set(name, binding); + }); } - async exposeBinding(name: string, playwrightBinding: FunctionWithSource) { + async exposeBinding(name: string, playwrightBinding: FunctionWithSource, options: { handle?: boolean } = {}) { return this._wrapApiCall('page.exposeBinding', async () => { - if (this._bindings.has(name)) - throw new Error(`Function "${name}" has been already registered`); - if (this._browserContext._bindings.has(name)) - throw new Error(`Function "${name}" has been already registered in the browser context`); + await this._channel.exposeBinding({ name, needsHandle: options.handle }); this._bindings.set(name, playwrightBinding); - await this._channel.exposeBinding({ name }); }); } @@ -615,7 +616,11 @@ export class BindingCall extends ChannelOwner { - await this._context.exposeBinding(params.name, (source, ...args) => { - const binding = new BindingCallDispatcher(this._scope, params.name, source, args); + await this._context.exposeBinding(params.name, !!params.needsHandle, (source, ...args) => { + const binding = new BindingCallDispatcher(this._scope, params.name, !!params.needsHandle, source, args); this._dispatchEvent('bindingCall', { binding }); return binding.promise(); }); diff --git a/src/dispatchers/pageDispatcher.ts b/src/dispatchers/pageDispatcher.ts index 832585f594..b70fb00d96 100644 --- a/src/dispatchers/pageDispatcher.ts +++ b/src/dispatchers/pageDispatcher.ts @@ -26,10 +26,11 @@ import { DialogDispatcher } from './dialogDispatcher'; import { DownloadDispatcher } from './downloadDispatcher'; import { FrameDispatcher } from './frameDispatcher'; import { RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers'; -import { serializeResult, parseArgument } from './jsHandleDispatcher'; +import { serializeResult, parseArgument, JSHandleDispatcher } from './jsHandleDispatcher'; import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher'; import { FileChooser } from '../server/fileChooser'; import { CRCoverage } from '../server/chromium/crCoverage'; +import { JSHandle } from '../server/javascript'; export class PageDispatcher extends Dispatcher implements channels.PageChannel { private _page: Page; @@ -81,8 +82,8 @@ export class PageDispatcher extends Dispatcher i } async exposeBinding(params: channels.PageExposeBindingParams): Promise { - await this._page.exposeBinding(params.name, (source, ...args) => { - const binding = new BindingCallDispatcher(this._scope, params.name, source, args); + await this._page.exposeBinding(params.name, !!params.needsHandle, (source, ...args) => { + const binding = new BindingCallDispatcher(this._scope, params.name, !!params.needsHandle, source, args); this._dispatchEvent('bindingCall', { binding }); return binding.promise(); }); @@ -254,11 +255,12 @@ export class BindingCallDispatcher extends Dispatcher<{}, channels.BindingCallIn private _reject: ((error: any) => void) | undefined; private _promise: Promise; - constructor(scope: DispatcherScope, name: string, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) { + constructor(scope: DispatcherScope, name: string, needsHandle: boolean, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) { super(scope, {}, 'BindingCall', { frame: lookupDispatcher(source.frame), name, - args: args.map(serializeResult), + args: needsHandle ? undefined : args.map(serializeResult), + handle: needsHandle ? new JSHandleDispatcher(scope, args[0] as JSHandle) : undefined, }); this._promise = new Promise((resolve, reject) => { this._resolve = resolve; diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index 979a33a842..9a25b58551 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -555,9 +555,10 @@ export type BrowserContextCookiesResult = { }; export type BrowserContextExposeBindingParams = { name: string, + needsHandle?: boolean, }; export type BrowserContextExposeBindingOptions = { - + needsHandle?: boolean, }; export type BrowserContextExposeBindingResult = void; export type BrowserContextGrantPermissionsParams = { @@ -808,9 +809,10 @@ export type PageEmulateMediaOptions = { export type PageEmulateMediaResult = void; export type PageExposeBindingParams = { name: string, + needsHandle?: boolean, }; export type PageExposeBindingOptions = { - + needsHandle?: boolean, }; export type PageExposeBindingResult = void; export type PageGoBackParams = { @@ -2110,7 +2112,8 @@ export interface ConsoleMessageChannel extends Channel { export type BindingCallInitializer = { frame: FrameChannel, name: string, - args: SerializedValue[], + args?: SerializedValue[], + handle?: JSHandleChannel, }; export interface BindingCallChannel extends Channel { reject(params: BindingCallRejectParams, metadata?: Metadata): Promise; diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index a2eaef2e14..602ea6d8e6 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -475,6 +475,7 @@ BrowserContext: exposeBinding: parameters: name: string + needsHandle: boolean? grantPermissions: parameters: @@ -618,6 +619,7 @@ Page: exposeBinding: parameters: name: string + needsHandle: boolean? goBack: parameters: @@ -1780,8 +1782,9 @@ BindingCall: frame: Frame name: string args: - type: array + type: array? items: SerializedValue + handle: JSHandle? commands: diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 7b58002ca8..a7de0e639f 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -262,6 +262,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { }); scheme.BrowserContextExposeBindingParams = tObject({ name: tString, + needsHandle: tOptional(tBoolean), }); scheme.BrowserContextGrantPermissionsParams = tObject({ permissions: tArray(tString), @@ -323,6 +324,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { }); scheme.PageExposeBindingParams = tObject({ name: tString, + needsHandle: tOptional(tBoolean), }); scheme.PageGoBackParams = tObject({ timeout: tOptional(tNumber), diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index 732603134a..2dbc069a12 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -167,14 +167,14 @@ export abstract class BrowserContext extends EventEmitter { return this._doSetHTTPCredentials(httpCredentials); } - async exposeBinding(name: string, playwrightBinding: frames.FunctionWithSource): Promise { + async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.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); + const binding = new PageBinding(name, playwrightBinding, needsHandle); this._pageBindings.set(name, binding); this._doExposeBinding(binding); } diff --git a/src/server/page.ts b/src/server/page.ts index 9837e443a6..fbbb92a63e 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -232,12 +232,12 @@ export class Page extends EventEmitter { this._timeoutSettings.setDefaultTimeout(timeout); } - async exposeBinding(name: string, playwrightBinding: frames.FunctionWithSource) { + async exposeBinding(name: string, needsHandle: boolean, 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, playwrightBinding); + const binding = new PageBinding(name, playwrightBinding, needsHandle); this._pageBindings.set(name, binding); await this._delegate.exposeBinding(binding); } @@ -454,11 +454,13 @@ export class PageBinding { readonly name: string; readonly playwrightFunction: frames.FunctionWithSource; readonly source: string; + readonly needsHandle: boolean; - constructor(name: string, playwrightFunction: frames.FunctionWithSource) { + constructor(name: string, playwrightFunction: frames.FunctionWithSource, needsHandle: boolean) { this.name = name; this.playwrightFunction = playwrightFunction; - this.source = `(${addPageBinding.toString()})(${JSON.stringify(name)})`; + this.source = `(${addPageBinding.toString()})(${JSON.stringify(name)}, ${needsHandle})`; + this.needsHandle = needsHandle; } static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) { @@ -467,7 +469,13 @@ export class PageBinding { let binding = page._pageBindings.get(name); if (!binding) binding = page._browserContext._pageBindings.get(name); - const result = await binding!.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args); + let result: any; + if (binding!.needsHandle) { + const handle = await context.evaluateHandleInternal(takeHandle, { name, seq }).catch(e => null); + result = await binding!.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, handle); + } else { + result = await binding!.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args); + } context.evaluateInternal(deliverResult, { name, seq, result }).catch(e => debugLogger.log('error', e)); } catch (error) { if (isError(error)) @@ -476,6 +484,12 @@ export class PageBinding { context.evaluateInternal(deliverErrorValue, { name, seq, error }).catch(e => debugLogger.log('error', e)); } + function takeHandle(arg: { name: string, seq: number }) { + const handle = (window as any)[arg.name]['handles'].get(arg.seq); + (window as any)[arg.name]['handles'].delete(arg.seq); + return handle; + } + function deliverResult(arg: { name: string, seq: number, result: any }) { (window as any)[arg.name]['callbacks'].get(arg.seq).resolve(arg.result); (window as any)[arg.name]['callbacks'].delete(arg.seq); @@ -495,12 +509,14 @@ export class PageBinding { } } -function addPageBinding(bindingName: string) { +function addPageBinding(bindingName: string, needsHandle: boolean) { const binding = (window as any)[bindingName]; if (binding.__installed) return; (window as any)[bindingName] = (...args: any[]) => { const me = (window as any)[bindingName]; + if (needsHandle && args.slice(1).some(arg => arg !== undefined)) + throw new Error(`exposeBindingHandle supports a single argument, ${args.length} received`); let callbacks = me['callbacks']; if (!callbacks) { callbacks = new Map(); @@ -508,8 +524,18 @@ function addPageBinding(bindingName: string) { } const seq = (me['lastSeq'] || 0) + 1; me['lastSeq'] = seq; + let handles = me['handles']; + if (!handles) { + handles = new Map(); + me['handles'] = handles; + } const promise = new Promise((resolve, reject) => callbacks.set(seq, {resolve, reject})); - binding(JSON.stringify({name: bindingName, seq, args})); + if (needsHandle) { + handles.set(seq, args[0]); + binding(JSON.stringify({name: bindingName, seq})); + } else { + binding(JSON.stringify({name: bindingName, seq, args})); + } return promise; }; (window as any)[bindingName].__installed = true; diff --git a/test/browsercontext-expose-function.spec.ts b/test/browsercontext-expose-function.spec.ts index eb16990b60..7d02623608 100644 --- a/test/browsercontext-expose-function.spec.ts +++ b/test/browsercontext-expose-function.spec.ts @@ -76,3 +76,19 @@ it('should be callable from-inside addInitScript', async ({browser, server}) => expect(args).toEqual(['context', 'page']); await context.close(); }); + +it('exposeBindingHandle should work', async ({browser}) => { + const context = await browser.newContext(); + let target; + await context.exposeBinding('logme', (source, t) => { + target = t; + return 17; + }, { handle: true }); + const page = await context.newPage(); + const result = await page.evaluate(async function() { + return window['logme']({ foo: 42 }); + }); + expect(await target.evaluate(x => x.foo)).toBe(42); + expect(result).toEqual(17); + await context.close(); +}); diff --git a/test/page-expose-function.spec.ts b/test/page-expose-function.spec.ts index 82eaf56fe4..5be48dc77b 100644 --- a/test/page-expose-function.spec.ts +++ b/test/page-expose-function.spec.ts @@ -169,3 +169,56 @@ it('should work with complex objects', async ({page, server}) => { const result = await page.evaluate(async () => window['complexObject']({x: 5}, {x: 2})); expect(result.x).toBe(7); }); + +it('exposeBindingHandle should work', async ({page}) => { + let target; + await page.exposeBinding('logme', (source, t) => { + target = t; + return 17; + }, { handle: true }); + const result = await page.evaluate(async function() { + return window['logme']({ foo: 42 }); + }); + expect(await target.evaluate(x => x.foo)).toBe(42); + expect(result).toEqual(17); +}); + +it('exposeBindingHandle should not throw during navigation', async ({page, server}) => { + await page.exposeBinding('logme', (source, t) => { + return 17; + }, { handle: true }); + await page.goto(server.EMPTY_PAGE); + await Promise.all([ + page.evaluate(async url => { + window['logme']({ foo: 42 }); + window.location.href = url; + }, server.PREFIX + '/one-style.html'), + page.waitForNavigation({ waitUntil: 'load' }), + ]); +}); + +it('should throw for duplicate registrations', async ({page}) => { + await page.exposeFunction('foo', () => {}); + const error = await page.exposeFunction('foo', () => {}).catch(e => e); + expect(error.message).toContain('page.exposeFunction: Function "foo" has been already registered'); +}); + +it('exposeBindingHandle should throw for multiple arguments', async ({page}) => { + await page.exposeBinding('logme', (source, t) => { + return 17; + }, { handle: true }); + expect(await page.evaluate(async function() { + return window['logme']({ foo: 42 }); + })).toBe(17); + expect(await page.evaluate(async function() { + return window['logme']({ foo: 42 }, undefined, undefined); + })).toBe(17); + expect(await page.evaluate(async function() { + return window['logme'](undefined, undefined, undefined); + })).toBe(17); + + const error = await page.evaluate(async function() { + return window['logme'](1, 2); + }).catch(e => e); + expect(error.message).toContain('exposeBindingHandle supports a single argument, 2 received'); +}); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index 7f9f7b8584..07c03931bb 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -51,6 +51,8 @@ type ElementHandleWaitForSelectorOptionsNotHidden = ElementHandleWaitForSelector state: 'visible'|'attached'; }; +type BindingSource = { context: BrowserContext, page: Page, frame: Frame }; + export interface Page { evaluate(pageFunction: PageFunction, arg: Arg): Promise; evaluate(pageFunction: PageFunction, arg?: any): Promise; @@ -81,6 +83,9 @@ export interface Page { waitForSelector(selector: string, options?: PageWaitForSelectorOptionsNotHidden): Promise>; waitForSelector(selector: K, options: PageWaitForSelectorOptions): Promise | null>; waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise>; + + exposeBinding(name: string, playwrightBinding: (source: BindingSource, arg: JSHandle) => any, options: { handle: true }): Promise; + exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise; } export interface Frame { @@ -115,6 +120,11 @@ export interface Frame { waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise>; } +export interface BrowserContext { + exposeBinding(name: string, playwrightBinding: (source: BindingSource, arg: JSHandle) => any, options: { handle: true }): Promise; + exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise; +} + export interface Worker { evaluate(pageFunction: PageFunction, arg: Arg): Promise; evaluate(pageFunction: PageFunction, arg?: any): Promise; diff --git a/utils/generate_types/test/test.ts b/utils/generate_types/test/test.ts index 470b454fa6..e57ca9b518 100644 --- a/utils/generate_types/test/test.ts +++ b/utils/generate_types/test/test.ts @@ -122,6 +122,11 @@ playwright.chromium.launch().then(async browser => { console.log(content); }); + await page.exposeBinding('clicked', async (source, handle) => { + await handle.asElement()!.textContent(); + await source.page.goto('http://example.com'); + }, { handle: true }); + await page.emulateMedia({media: 'screen'}); await page.pdf({ path: 'page.pdf' });