Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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(`
<script>
document.addEventListener('click', event => window.clicked(event.target));
</script>
<div>Click me</div>
<div>Or click me</div>
`);
```

#### 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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand All @@ -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.

Expand All @@ -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(`
<script>
document.addEventListener('click', event => window.clicked(event.target));
</script>
<div>Click me</div>
<div>Or click me</div>
`);
```

#### 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.
Expand Down Expand Up @@ -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)
Expand Down
21 changes: 9 additions & 12 deletions src/client/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -34,7 +33,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
private _routes: { url: URLMatch, handler: network.RouteHandler }[] = [];
readonly _browser: Browser | null = null;
readonly _browserName: string;
readonly _bindings = new Map<string, frames.FunctionWithSource>();
readonly _bindings = new Map<string, FunctionWithSource>();
_timeoutSettings = new TimeoutSettings();
_ownerPage: Page | undefined;
private _closedPromise: Promise<void>;
Expand Down Expand Up @@ -176,21 +175,19 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
});
}

async exposeBinding(name: string, playwrightBinding: frames.FunctionWithSource): Promise<void> {
async exposeBinding(name: string, playwrightBinding: FunctionWithSource, options: { handle?: boolean } = {}): Promise<void> {
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<void> {
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<void> {
Expand Down
2 changes: 0 additions & 2 deletions src/client/frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down
25 changes: 15 additions & 10 deletions src/client/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -60,6 +60,7 @@ type PDFOptions = Omit<channels.PagePdfParams, 'width' | 'height' | 'margin'> &
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<channels.PageChannel, channels.PageInitializer> {
private _browserContext: BrowserContext;
Expand Down Expand Up @@ -280,17 +281,17 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
}

async exposeFunction(name: string, playwrightFunction: Function) {
await this.exposeBinding(name, (options, ...args: any) => 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 });
});
}

Expand Down Expand Up @@ -615,7 +616,11 @@ export class BindingCall extends ChannelOwner<channels.BindingCallChannel, chann
page: frame._page!,
frame
};
const result = await func(source, ...this._initializer.args.map(parseResult));
let result: any;
if (this._initializer.handle)
result = await func(source, JSHandle.from(this._initializer.handle));
else
result = await func(source, ...this._initializer.args!.map(parseResult));
this._channel.resolve({ result: serializeArgument(result) });
} catch (e) {
this._channel.reject({ error: serializeError(e) });
Expand Down
4 changes: 2 additions & 2 deletions src/dispatchers/browserContextDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
}

async exposeBinding(params: channels.BrowserContextExposeBindingParams): Promise<void> {
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();
});
Expand Down
12 changes: 7 additions & 5 deletions src/dispatchers/pageDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Page, channels.PageInitializer> implements channels.PageChannel {
private _page: Page;
Expand Down Expand Up @@ -81,8 +82,8 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
}

async exposeBinding(params: channels.PageExposeBindingParams): Promise<void> {
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();
});
Expand Down Expand Up @@ -254,11 +255,12 @@ export class BindingCallDispatcher extends Dispatcher<{}, channels.BindingCallIn
private _reject: ((error: any) => void) | undefined;
private _promise: Promise<any>;

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<FrameDispatcher>(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;
Expand Down
9 changes: 6 additions & 3 deletions src/protocol/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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<BindingCallRejectResult>;
Expand Down
5 changes: 4 additions & 1 deletion src/protocol/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ BrowserContext:
exposeBinding:
parameters:
name: string
needsHandle: boolean?

grantPermissions:
parameters:
Expand Down Expand Up @@ -618,6 +619,7 @@ Page:
exposeBinding:
parameters:
name: string
needsHandle: boolean?

goBack:
parameters:
Expand Down Expand Up @@ -1780,8 +1782,9 @@ BindingCall:
frame: Frame
name: string
args:
type: array
type: array?
items: SerializedValue
handle: JSHandle?

commands:

Expand Down
2 changes: 2 additions & 0 deletions src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
4 changes: 2 additions & 2 deletions src/server/browserContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,14 @@ export abstract class BrowserContext extends EventEmitter {
return this._doSetHTTPCredentials(httpCredentials);
}

async exposeBinding(name: string, playwrightBinding: frames.FunctionWithSource): Promise<void> {
async exposeBinding(name: string, needsHandle: boolean, playwrightBinding: frames.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);
const binding = new PageBinding(name, playwrightBinding, needsHandle);
this._pageBindings.set(name, binding);
this._doExposeBinding(binding);
}
Expand Down
Loading