mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 13:50:25 +03:00
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:
parent
c2171218fa
commit
5e42029fce
46
docs/api.md
46
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(`
|
||||
<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)
|
||||
|
@ -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> {
|
||||
|
@ -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,
|
||||
|
@ -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) });
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -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>;
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
|
10
utils/generate_types/overrides.d.ts
vendored
10
utils/generate_types/overrides.d.ts
vendored
@ -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>;
|
||||
|
@ -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' });
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user