Skip to content

Commit 5e42029

Browse files
authored
api: allow exposeBinding to pass handles (#4030)
This adds an option `{ handle: true }` to pass a single handle instead of arbitrary json values.
1 parent c217121 commit 5e42029

File tree

15 files changed

+203
-51
lines changed

15 files changed

+203
-51
lines changed

docs/api.md

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ await context.close();
314314
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
315315
- [browserContext.close()](#browsercontextclose)
316316
- [browserContext.cookies([urls])](#browsercontextcookiesurls)
317-
- [browserContext.exposeBinding(name, playwrightBinding)](#browsercontextexposebindingname-playwrightbinding)
317+
- [browserContext.exposeBinding(name, playwrightBinding[, options])](#browsercontextexposebindingname-playwrightbinding-options)
318318
- [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction)
319319
- [browserContext.grantPermissions(permissions[][, options])](#browsercontextgrantpermissionspermissions-options)
320320
- [browserContext.newPage()](#browsercontextnewpage)
@@ -443,9 +443,11 @@ will be closed.
443443
If no URLs are specified, this method returns all cookies.
444444
If URLs are specified, only cookies that affect those URLs are returned.
445445

446-
#### browserContext.exposeBinding(name, playwrightBinding)
446+
#### browserContext.exposeBinding(name, playwrightBinding[, options])
447447
- `name` <[string]> Name of the function on the window object.
448448
- `playwrightBinding` <[function]> Callback function that will be called in the Playwright's context.
449+
- `options` <[Object]>
450+
- `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.
449451
- returns: <[Promise]>
450452

451453
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.
455457
The first argument of the `playwrightBinding` function contains information about the caller:
456458
`{ browserContext: BrowserContext, page: Page, frame: Frame }`.
457459

458-
See [page.exposeBinding(name, playwrightBinding)](#pageexposebindingname-playwrightbinding) for page-only version.
460+
See [page.exposeBinding(name, playwrightBinding)](#pageexposebindingname-playwrightbinding-options) for page-only version.
459461

460462
An example of exposing page URL to all frames in all pages in the context:
461463
```js
@@ -479,6 +481,20 @@ const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
479481
})();
480482
```
481483

484+
An example of passing an element handle:
485+
```js
486+
await context.exposeBinding('clicked', async (source, element) => {
487+
console.log(await element.textContent());
488+
}, { handle: true });
489+
await page.setContent(`
490+
<script>
491+
document.addEventListener('click', event => window.clicked(event.target));
492+
</script>
493+
<div>Click me</div>
494+
<div>Or click me</div>
495+
`);
496+
```
497+
482498
#### browserContext.exposeFunction(name, playwrightFunction)
483499
- `name` <[string]> Name of the function on the window object.
484500
- `playwrightFunction` <[function]> Callback function that will be called in the Playwright's context.
@@ -735,7 +751,7 @@ page.removeListener('request', logRequest);
735751
- [page.emulateMedia(options)](#pageemulatemediaoptions)
736752
- [page.evaluate(pageFunction[, arg])](#pageevaluatepagefunction-arg)
737753
- [page.evaluateHandle(pageFunction[, arg])](#pageevaluatehandlepagefunction-arg)
738-
- [page.exposeBinding(name, playwrightBinding)](#pageexposebindingname-playwrightbinding)
754+
- [page.exposeBinding(name, playwrightBinding[, options])](#pageexposebindingname-playwrightbinding-options)
739755
- [page.exposeFunction(name, playwrightFunction)](#pageexposefunctionname-playwrightfunction)
740756
- [page.fill(selector, value[, options])](#pagefillselector-value-options)
741757
- [page.focus(selector[, options])](#pagefocusselector-options)
@@ -1264,9 +1280,11 @@ console.log(await resultHandle.jsonValue());
12641280
await resultHandle.dispose();
12651281
```
12661282

1267-
#### page.exposeBinding(name, playwrightBinding)
1283+
#### page.exposeBinding(name, playwrightBinding[, options])
12681284
- `name` <[string]> Name of the function on the window object.
12691285
- `playwrightBinding` <[function]> Callback function that will be called in the Playwright's context.
1286+
- `options` <[Object]>
1287+
- `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.
12701288
- returns: <[Promise]>
12711289

12721290
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.
12761294
The first argument of the `playwrightBinding` function contains information about the caller:
12771295
`{ browserContext: BrowserContext, page: Page, frame: Frame }`.
12781296

1279-
See [browserContext.exposeBinding(name, playwrightBinding)](#browsercontextexposebindingname-playwrightbinding) for the context-wide version.
1297+
See [browserContext.exposeBinding(name, playwrightBinding)](#browsercontextexposebindingname-playwrightbinding-options) for the context-wide version.
12801298

12811299
> **NOTE** Functions installed via `page.exposeBinding` survive navigations.
12821300
@@ -1302,6 +1320,20 @@ const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
13021320
})();
13031321
```
13041322

1323+
An example of passing an element handle:
1324+
```js
1325+
await page.exposeBinding('clicked', async (source, element) => {
1326+
console.log(await element.textContent());
1327+
}, { handle: true });
1328+
await page.setContent(`
1329+
<script>
1330+
document.addEventListener('click', event => window.clicked(event.target));
1331+
</script>
1332+
<div>Click me</div>
1333+
<div>Or click me</div>
1334+
`);
1335+
```
1336+
13051337
#### page.exposeFunction(name, playwrightFunction)
13061338
- `name` <[string]> Name of the function on the window object
13071339
- `playwrightFunction` <[function]> Callback function which will be called in Playwright's context.
@@ -4409,7 +4441,7 @@ const backgroundPage = await context.waitForEvent('backgroundpage');
44094441
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
44104442
- [browserContext.close()](#browsercontextclose)
44114443
- [browserContext.cookies([urls])](#browsercontextcookiesurls)
4412-
- [browserContext.exposeBinding(name, playwrightBinding)](#browsercontextexposebindingname-playwrightbinding)
4444+
- [browserContext.exposeBinding(name, playwrightBinding[, options])](#browsercontextexposebindingname-playwrightbinding-options)
44134445
- [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction)
44144446
- [browserContext.grantPermissions(permissions[][, options])](#browsercontextgrantpermissionspermissions-options)
44154447
- [browserContext.newPage()](#browsercontextnewpage)

src/client/browserContext.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@
1515
* limitations under the License.
1616
*/
1717

18-
import * as frames from './frame';
19-
import { Page, BindingCall } from './page';
18+
import { Page, BindingCall, FunctionWithSource } from './page';
2019
import * as network from './network';
2120
import * as channels from '../protocol/channels';
2221
import { ChannelOwner } from './channelOwner';
@@ -34,7 +33,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
3433
private _routes: { url: URLMatch, handler: network.RouteHandler }[] = [];
3534
readonly _browser: Browser | null = null;
3635
readonly _browserName: string;
37-
readonly _bindings = new Map<string, frames.FunctionWithSource>();
36+
readonly _bindings = new Map<string, FunctionWithSource>();
3837
_timeoutSettings = new TimeoutSettings();
3938
_ownerPage: Page | undefined;
4039
private _closedPromise: Promise<void>;
@@ -176,21 +175,19 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
176175
});
177176
}
178177

179-
async exposeBinding(name: string, playwrightBinding: frames.FunctionWithSource): Promise<void> {
178+
async exposeBinding(name: string, playwrightBinding: FunctionWithSource, options: { handle?: boolean } = {}): Promise<void> {
180179
return this._wrapApiCall('browserContext.exposeBinding', async () => {
181-
for (const page of this.pages()) {
182-
if (page._bindings.has(name))
183-
throw new Error(`Function "${name}" has been already registered in one of the pages`);
184-
}
185-
if (this._bindings.has(name))
186-
throw new Error(`Function "${name}" has been already registered`);
180+
await this._channel.exposeBinding({ name, needsHandle: options.handle });
187181
this._bindings.set(name, playwrightBinding);
188-
await this._channel.exposeBinding({ name });
189182
});
190183
}
191184

192185
async exposeFunction(name: string, playwrightFunction: Function): Promise<void> {
193-
await this.exposeBinding(name, (source, ...args) => playwrightFunction(...args));
186+
return this._wrapApiCall('browserContext.exposeFunction', async () => {
187+
await this._channel.exposeBinding({ name });
188+
const binding: FunctionWithSource = (source, ...args) => playwrightFunction(...args);
189+
this._bindings.set(name, binding);
190+
});
194191
}
195192

196193
async route(url: URLMatch, handler: network.RouteHandler): Promise<void> {

src/client/frame.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import { assert } from '../utils/utils';
1919
import * as channels from '../protocol/channels';
20-
import { BrowserContext } from './browserContext';
2120
import { ChannelOwner } from './channelOwner';
2221
import { ElementHandle, convertSelectOptionValues, convertInputFiles } from './elementHandle';
2322
import { assertMaxArguments, JSHandle, Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle';
@@ -33,7 +32,6 @@ import { urlMatches } from './clientHelper';
3332

3433
const fsReadFileAsync = util.promisify(fs.readFile.bind(fs));
3534

36-
export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame }, ...args: any) => any;
3735
export type WaitForNavigationOptions = {
3836
timeout?: number,
3937
waitUntil?: LifecycleEvent,

src/client/page.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ import { Dialog } from './dialog';
2828
import { Download } from './download';
2929
import { ElementHandle, determineScreenshotType } from './elementHandle';
3030
import { Worker } from './worker';
31-
import { Frame, FunctionWithSource, verifyLoadState, WaitForNavigationOptions } from './frame';
31+
import { Frame, verifyLoadState, WaitForNavigationOptions } from './frame';
3232
import { Keyboard, Mouse } from './input';
33-
import { assertMaxArguments, Func1, FuncOn, SmartHandle, serializeArgument, parseResult } from './jsHandle';
33+
import { assertMaxArguments, Func1, FuncOn, SmartHandle, serializeArgument, parseResult, JSHandle } from './jsHandle';
3434
import { Request, Response, Route, RouteHandler, validateHeaders } from './network';
3535
import { FileChooser } from './fileChooser';
3636
import { Buffer } from 'buffer';
@@ -60,6 +60,7 @@ type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> &
6060
path?: string,
6161
};
6262
type Listener = (...args: any[]) => void;
63+
export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame }, ...args: any) => any;
6364

6465
export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitializer> {
6566
private _browserContext: BrowserContext;
@@ -280,17 +281,17 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
280281
}
281282

282283
async exposeFunction(name: string, playwrightFunction: Function) {
283-
await this.exposeBinding(name, (options, ...args: any) => playwrightFunction(...args));
284+
return this._wrapApiCall('page.exposeFunction', async () => {
285+
await this._channel.exposeBinding({ name });
286+
const binding: FunctionWithSource = (source, ...args) => playwrightFunction(...args);
287+
this._bindings.set(name, binding);
288+
});
284289
}
285290

286-
async exposeBinding(name: string, playwrightBinding: FunctionWithSource) {
291+
async exposeBinding(name: string, playwrightBinding: FunctionWithSource, options: { handle?: boolean } = {}) {
287292
return this._wrapApiCall('page.exposeBinding', async () => {
288-
if (this._bindings.has(name))
289-
throw new Error(`Function "${name}" has been already registered`);
290-
if (this._browserContext._bindings.has(name))
291-
throw new Error(`Function "${name}" has been already registered in the browser context`);
293+
await this._channel.exposeBinding({ name, needsHandle: options.handle });
292294
this._bindings.set(name, playwrightBinding);
293-
await this._channel.exposeBinding({ name });
294295
});
295296
}
296297

@@ -615,7 +616,11 @@ export class BindingCall extends ChannelOwner<channels.BindingCallChannel, chann
615616
page: frame._page!,
616617
frame
617618
};
618-
const result = await func(source, ...this._initializer.args.map(parseResult));
619+
let result: any;
620+
if (this._initializer.handle)
621+
result = await func(source, JSHandle.from(this._initializer.handle));
622+
else
623+
result = await func(source, ...this._initializer.args!.map(parseResult));
619624
this._channel.resolve({ result: serializeArgument(result) });
620625
} catch (e) {
621626
this._channel.reject({ error: serializeError(e) });

src/dispatchers/browserContextDispatcher.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
5656
}
5757

5858
async exposeBinding(params: channels.BrowserContextExposeBindingParams): Promise<void> {
59-
await this._context.exposeBinding(params.name, (source, ...args) => {
60-
const binding = new BindingCallDispatcher(this._scope, params.name, source, args);
59+
await this._context.exposeBinding(params.name, !!params.needsHandle, (source, ...args) => {
60+
const binding = new BindingCallDispatcher(this._scope, params.name, !!params.needsHandle, source, args);
6161
this._dispatchEvent('bindingCall', { binding });
6262
return binding.promise();
6363
});

src/dispatchers/pageDispatcher.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ import { DialogDispatcher } from './dialogDispatcher';
2626
import { DownloadDispatcher } from './downloadDispatcher';
2727
import { FrameDispatcher } from './frameDispatcher';
2828
import { RequestDispatcher, ResponseDispatcher, RouteDispatcher } from './networkDispatchers';
29-
import { serializeResult, parseArgument } from './jsHandleDispatcher';
29+
import { serializeResult, parseArgument, JSHandleDispatcher } from './jsHandleDispatcher';
3030
import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher';
3131
import { FileChooser } from '../server/fileChooser';
3232
import { CRCoverage } from '../server/chromium/crCoverage';
33+
import { JSHandle } from '../server/javascript';
3334

3435
export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel {
3536
private _page: Page;
@@ -81,8 +82,8 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
8182
}
8283

8384
async exposeBinding(params: channels.PageExposeBindingParams): Promise<void> {
84-
await this._page.exposeBinding(params.name, (source, ...args) => {
85-
const binding = new BindingCallDispatcher(this._scope, params.name, source, args);
85+
await this._page.exposeBinding(params.name, !!params.needsHandle, (source, ...args) => {
86+
const binding = new BindingCallDispatcher(this._scope, params.name, !!params.needsHandle, source, args);
8687
this._dispatchEvent('bindingCall', { binding });
8788
return binding.promise();
8889
});
@@ -254,11 +255,12 @@ export class BindingCallDispatcher extends Dispatcher<{}, channels.BindingCallIn
254255
private _reject: ((error: any) => void) | undefined;
255256
private _promise: Promise<any>;
256257

257-
constructor(scope: DispatcherScope, name: string, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) {
258+
constructor(scope: DispatcherScope, name: string, needsHandle: boolean, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) {
258259
super(scope, {}, 'BindingCall', {
259260
frame: lookupDispatcher<FrameDispatcher>(source.frame),
260261
name,
261-
args: args.map(serializeResult),
262+
args: needsHandle ? undefined : args.map(serializeResult),
263+
handle: needsHandle ? new JSHandleDispatcher(scope, args[0] as JSHandle) : undefined,
262264
});
263265
this._promise = new Promise((resolve, reject) => {
264266
this._resolve = resolve;

src/protocol/channels.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -555,9 +555,10 @@ export type BrowserContextCookiesResult = {
555555
};
556556
export type BrowserContextExposeBindingParams = {
557557
name: string,
558+
needsHandle?: boolean,
558559
};
559560
export type BrowserContextExposeBindingOptions = {
560-
561+
needsHandle?: boolean,
561562
};
562563
export type BrowserContextExposeBindingResult = void;
563564
export type BrowserContextGrantPermissionsParams = {
@@ -808,9 +809,10 @@ export type PageEmulateMediaOptions = {
808809
export type PageEmulateMediaResult = void;
809810
export type PageExposeBindingParams = {
810811
name: string,
812+
needsHandle?: boolean,
811813
};
812814
export type PageExposeBindingOptions = {
813-
815+
needsHandle?: boolean,
814816
};
815817
export type PageExposeBindingResult = void;
816818
export type PageGoBackParams = {
@@ -2110,7 +2112,8 @@ export interface ConsoleMessageChannel extends Channel {
21102112
export type BindingCallInitializer = {
21112113
frame: FrameChannel,
21122114
name: string,
2113-
args: SerializedValue[],
2115+
args?: SerializedValue[],
2116+
handle?: JSHandleChannel,
21142117
};
21152118
export interface BindingCallChannel extends Channel {
21162119
reject(params: BindingCallRejectParams, metadata?: Metadata): Promise<BindingCallRejectResult>;

src/protocol/protocol.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ BrowserContext:
475475
exposeBinding:
476476
parameters:
477477
name: string
478+
needsHandle: boolean?
478479

479480
grantPermissions:
480481
parameters:
@@ -618,6 +619,7 @@ Page:
618619
exposeBinding:
619620
parameters:
620621
name: string
622+
needsHandle: boolean?
621623

622624
goBack:
623625
parameters:
@@ -1780,8 +1782,9 @@ BindingCall:
17801782
frame: Frame
17811783
name: string
17821784
args:
1783-
type: array
1785+
type: array?
17841786
items: SerializedValue
1787+
handle: JSHandle?
17851788

17861789
commands:
17871790

src/protocol/validator.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
262262
});
263263
scheme.BrowserContextExposeBindingParams = tObject({
264264
name: tString,
265+
needsHandle: tOptional(tBoolean),
265266
});
266267
scheme.BrowserContextGrantPermissionsParams = tObject({
267268
permissions: tArray(tString),
@@ -323,6 +324,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
323324
});
324325
scheme.PageExposeBindingParams = tObject({
325326
name: tString,
327+
needsHandle: tOptional(tBoolean),
326328
});
327329
scheme.PageGoBackParams = tObject({
328330
timeout: tOptional(tNumber),

src/server/browserContext.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,14 @@ export abstract class BrowserContext extends EventEmitter {
167167
return this._doSetHTTPCredentials(httpCredentials);
168168
}
169169

170-
async exposeBinding(name: string, playwrightBinding: frames.FunctionWithSource): Promise<void> {
170+
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.FunctionWithSource): Promise<void> {
171171
for (const page of this.pages()) {
172172
if (page._pageBindings.has(name))
173173
throw new Error(`Function "${name}" has been already registered in one of the pages`);
174174
}
175175
if (this._pageBindings.has(name))
176176
throw new Error(`Function "${name}" has been already registered`);
177-
const binding = new PageBinding(name, playwrightBinding);
177+
const binding = new PageBinding(name, playwrightBinding, needsHandle);
178178
this._pageBindings.set(name, binding);
179179
this._doExposeBinding(binding);
180180
}

0 commit comments

Comments
 (0)