diff --git a/docs/api.md b/docs/api.md
index effa99919ab7c..d39c9c053fb94 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 b13a62b75524d..807eece17ef10 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 8b1f7403b2c5e..a823af149039b 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 5e6e102a71842..99cedd5498053 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 832585f59432d..b70fb00d96bf4 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 979a33a842685..9a25b58551262 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 a2eaef2e1454a..602ea6d8e6b47 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 7b58002ca8534..a7de0e639f53a 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 732603134a691..2dbc069a12d4b 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 9837e443a6ae8..fbbb92a63e951 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 eb16990b60db6..7d02623608d46 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 82eaf56fe49bd..5be48dc77ba09 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 7f9f7b85843e8..07c03931bbf1e 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 470b454fa6916..e57ca9b518e82 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' });