api: allow exposeBinding to pass handles (#4030)

This adds an option `{ handle: true }` to pass a single handle instead of arbitrary json values.
This commit is contained in:
Dmitry Gozman 2020-10-01 22:47:31 -07:00 committed by GitHub
parent c2171218fa
commit 5e42029fce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 203 additions and 51 deletions

View File

@ -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(`
<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.
@ -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(`
<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.
@ -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)

View File

@ -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<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>;
@ -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> {

View File

@ -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,

View File

@ -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<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;
@ -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 });
});
}
@ -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) });

View File

@ -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();
});

View File

@ -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;
@ -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();
});
@ -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;

View File

@ -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<BindingCallRejectResult>;

View File

@ -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:

View File

@ -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),

View File

@ -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);
}

View File

@ -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;

View File

@ -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();
});

View File

@ -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');
});

View File

@ -51,6 +51,8 @@ type ElementHandleWaitForSelectorOptionsNotHidden = ElementHandleWaitForSelector
state: 'visible'|'attached';
};
type BindingSource = { context: BrowserContext, page: Page, frame: Frame };
export interface Page {
evaluate<R, Arg>(pageFunction: PageFunction<Arg, R>, arg: Arg): Promise<R>;
evaluate<R>(pageFunction: PageFunction<void, R>, arg?: any): Promise<R>;
@ -81,6 +83,9 @@ export interface Page {
waitForSelector(selector: string, options?: PageWaitForSelectorOptionsNotHidden): Promise<ElementHandle<SVGElement | HTMLElement>>;
waitForSelector<K extends keyof HTMLElementTagNameMap>(selector: K, options: PageWaitForSelectorOptions): Promise<ElementHandleForTag<K> | null>;
waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise<null|ElementHandle<SVGElement | HTMLElement>>;
exposeBinding(name: string, playwrightBinding: (source: BindingSource, arg: JSHandle) => any, options: { handle: true }): Promise<void>;
exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise<void>;
}
export interface Frame {
@ -115,6 +120,11 @@ export interface Frame {
waitForSelector(selector: string, options: PageWaitForSelectorOptions): Promise<null|ElementHandle<SVGElement | HTMLElement>>;
}
export interface BrowserContext {
exposeBinding(name: string, playwrightBinding: (source: BindingSource, arg: JSHandle) => any, options: { handle: true }): Promise<void>;
exposeBinding(name: string, playwrightBinding: (source: BindingSource, ...args: any[]) => any, options?: { handle?: boolean }): Promise<void>;
}
export interface Worker {
evaluate<R, Arg>(pageFunction: PageFunction<Arg, R>, arg: Arg): Promise<R>;
evaluate<R>(pageFunction: PageFunction<void, R>, arg?: any): Promise<R>;

View File

@ -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' });