feat(exposeBinding): a more powerful exposeFunction with source attribution (#2263)
This commit is contained in:
parent
40ea0dd23b
commit
2bd427ad1d
87
docs/api.md
87
docs/api.md
|
|
@ -297,6 +297,7 @@ await context.close();
|
|||
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
|
||||
- [browserContext.close()](#browsercontextclose)
|
||||
- [browserContext.cookies([urls])](#browsercontextcookiesurls)
|
||||
- [browserContext.exposeBinding(name, playwrightBinding)](#browsercontextexposebindingname-playwrightbinding)
|
||||
- [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction)
|
||||
- [browserContext.grantPermissions(permissions[][, options])](#browsercontextgrantpermissionspermissions-options)
|
||||
- [browserContext.newPage()](#browsercontextnewpage)
|
||||
|
|
@ -421,20 +422,54 @@ 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)
|
||||
- `name` <[string]> Name of the function on the window object.
|
||||
- `playwrightBinding` <[function]> Callback function that will be called in the Playwright's context.
|
||||
- returns: <[Promise]>
|
||||
|
||||
The method adds a function called `name` on the `window` object of every frame in every page in the context.
|
||||
When called, the function executes `playwrightBinding` in Node.js and returns a [Promise] which resolves to the return value of `playwrightBinding`.
|
||||
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.
|
||||
|
||||
An example of exposing page URL to all frames in all pages in the context:
|
||||
```js
|
||||
const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
|
||||
|
||||
(async () => {
|
||||
const browser = await webkit.launch({ headless: false });
|
||||
const context = await browser.newContext();
|
||||
await context.exposeBinding('pageURL', ({ page }) => page.url());
|
||||
const page = await context.newPage();
|
||||
await page.setContent(`
|
||||
<script>
|
||||
async function onClick() {
|
||||
document.querySelector('div').textContent = await window.pageURL();
|
||||
}
|
||||
</script>
|
||||
<button onclick="onClick()">Click me</button>
|
||||
<div></div>
|
||||
`);
|
||||
await page.click('button');
|
||||
})();
|
||||
```
|
||||
|
||||
#### 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.
|
||||
- returns: <[Promise]>
|
||||
|
||||
The method adds a function called `name` on the `window` object of every frame in every page in the context.
|
||||
When called, the function executes `playwrightFunction` in node.js and returns a [Promise] which resolves to the return value of `playwrightFunction`.
|
||||
When called, the function executes `playwrightFunction` in Node.js and returns a [Promise] which resolves to the return value of `playwrightFunction`.
|
||||
|
||||
If the `playwrightFunction` returns a [Promise], it will be awaited.
|
||||
|
||||
See [page.exposeFunction(name, playwrightFunction)](#pageexposefunctionname-playwrightfunction) for page-only version.
|
||||
|
||||
> **NOTE** Functions installed via `page.exposeFunction` survive navigations.
|
||||
|
||||
An example of adding an `md5` function to all pages in the context:
|
||||
```js
|
||||
const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
|
||||
|
|
@ -678,6 +713,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.exposeFunction(name, playwrightFunction)](#pageexposefunctionname-playwrightfunction)
|
||||
- [page.fill(selector, value[, options])](#pagefillselector-value-options)
|
||||
- [page.focus(selector[, options])](#pagefocusselector-options)
|
||||
|
|
@ -1165,13 +1201,51 @@ console.log(await resultHandle.jsonValue());
|
|||
await resultHandle.dispose();
|
||||
```
|
||||
|
||||
#### page.exposeBinding(name, playwrightBinding)
|
||||
- `name` <[string]> Name of the function on the window object.
|
||||
- `playwrightBinding` <[function]> Callback function that will be called in the Playwright's context.
|
||||
- returns: <[Promise]>
|
||||
|
||||
The method adds a function called `name` on the `window` object of every frame in this page.
|
||||
When called, the function executes `playwrightBinding` in Node.js and returns a [Promise] which resolves to the return value of `playwrightBinding`.
|
||||
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.
|
||||
|
||||
> **NOTE** Functions installed via `page.exposeBinding` survive navigations.
|
||||
|
||||
An example of exposing page URL to all frames in a page:
|
||||
```js
|
||||
const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
|
||||
|
||||
(async () => {
|
||||
const browser = await webkit.launch({ headless: false });
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
await page.exposeBinding('pageURL', ({ page }) => page.url());
|
||||
await page.setContent(`
|
||||
<script>
|
||||
async function onClick() {
|
||||
document.querySelector('div').textContent = await window.pageURL();
|
||||
}
|
||||
</script>
|
||||
<button onclick="onClick()">Click me</button>
|
||||
<div></div>
|
||||
`);
|
||||
await page.click('button');
|
||||
})();
|
||||
```
|
||||
|
||||
#### 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.
|
||||
- returns: <[Promise]>
|
||||
|
||||
The method adds a function called `name` on the `window` object of every frame in the page.
|
||||
When called, the function executes `playwrightFunction` in node.js and returns a [Promise] which resolves to the return value of `playwrightFunction`.
|
||||
When called, the function executes `playwrightFunction` in Node.js and returns a [Promise] which resolves to the return value of `playwrightFunction`.
|
||||
|
||||
If the `playwrightFunction` returns a [Promise], it will be awaited.
|
||||
|
||||
|
|
@ -1720,7 +1794,7 @@ const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
|
|||
})();
|
||||
```
|
||||
|
||||
To pass an argument from node.js to the predicate of `page.waitForFunction` function:
|
||||
To pass an argument from Node.js to the predicate of `page.waitForFunction` function:
|
||||
|
||||
```js
|
||||
const selector = '.foo';
|
||||
|
|
@ -2389,7 +2463,7 @@ const { firefox } = require('playwright'); // Or 'chromium' or 'webkit'.
|
|||
})();
|
||||
```
|
||||
|
||||
To pass an argument from node.js to the predicate of `frame.waitForFunction` function:
|
||||
To pass an argument from Node.js to the predicate of `frame.waitForFunction` function:
|
||||
|
||||
```js
|
||||
const selector = '.foo';
|
||||
|
|
@ -4027,6 +4101,7 @@ const backgroundPage = await context.waitForEvent('backgroundpage');
|
|||
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
|
||||
- [browserContext.close()](#browsercontextclose)
|
||||
- [browserContext.cookies([urls])](#browsercontextcookiesurls)
|
||||
- [browserContext.exposeBinding(name, playwrightBinding)](#browsercontextexposebindingname-playwrightbinding)
|
||||
- [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction)
|
||||
- [browserContext.grantPermissions(permissions[][, options])](#browsercontextgrantpermissionspermissions-options)
|
||||
- [browserContext.newPage()](#browsercontextnewpage)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { ExtendedEventEmitter } from './extendedEventEmitter';
|
|||
import { Download } from './download';
|
||||
import { BrowserBase } from './browser';
|
||||
import { Log, InnerLogger, Logger, RootLogger } from './logger';
|
||||
import { FunctionWithSource } from './frames';
|
||||
|
||||
export type BrowserContextOptions = {
|
||||
viewport?: types.Size | null,
|
||||
|
|
@ -62,6 +63,7 @@ export interface BrowserContext extends InnerLogger {
|
|||
setOffline(offline: boolean): Promise<void>;
|
||||
setHTTPCredentials(httpCredentials: types.Credentials | null): Promise<void>;
|
||||
addInitScript(script: Function | string | { path?: string, content?: string }, arg?: any): Promise<void>;
|
||||
exposeBinding(name: string, playwrightBinding: FunctionWithSource): Promise<void>;
|
||||
exposeFunction(name: string, playwrightFunction: Function): Promise<void>;
|
||||
route(url: types.URLMatch, handler: network.RouteHandler): Promise<void>;
|
||||
unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise<void>;
|
||||
|
|
@ -126,11 +128,27 @@ export abstract class BrowserContextBase extends ExtendedEventEmitter implements
|
|||
abstract setExtraHTTPHeaders(headers: network.Headers): Promise<void>;
|
||||
abstract setOffline(offline: boolean): Promise<void>;
|
||||
abstract addInitScript(script: string | Function | { path?: string | undefined; content?: string | undefined; }, arg?: any): Promise<void>;
|
||||
abstract exposeFunction(name: string, playwrightFunction: Function): Promise<void>;
|
||||
abstract _doExposeBinding(binding: PageBinding): Promise<void>;
|
||||
abstract route(url: types.URLMatch, handler: network.RouteHandler): Promise<void>;
|
||||
abstract unroute(url: types.URLMatch, handler?: network.RouteHandler): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
|
||||
async exposeFunction(name: string, playwrightFunction: Function): Promise<void> {
|
||||
await this.exposeBinding(name, (options, ...args: any) => playwrightFunction(...args));
|
||||
}
|
||||
|
||||
async exposeBinding(name: string, playwrightBinding: 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);
|
||||
this._pageBindings.set(name, binding);
|
||||
this._doExposeBinding(binding);
|
||||
}
|
||||
|
||||
async grantPermissions(permissions: string[], options?: { origin?: string }) {
|
||||
let origin = '*';
|
||||
if (options && options.origin) {
|
||||
|
|
|
|||
|
|
@ -405,15 +405,7 @@ export class CRBrowserContext extends BrowserContextBase {
|
|||
await (page._delegate as CRPage).evaluateOnNewDocument(source);
|
||||
}
|
||||
|
||||
async exposeFunction(name: string, playwrightFunction: Function): 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, playwrightFunction);
|
||||
this._pageBindings.set(name, binding);
|
||||
async _doExposeBinding(binding: PageBinding) {
|
||||
for (const page of this.pages())
|
||||
await (page._delegate as CRPage).exposeBinding(binding);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,16 +302,8 @@ export class FFBrowserContext extends BrowserContextBase {
|
|||
await this._browser._connection.send('Browser.addScriptToEvaluateOnNewDocument', { browserContextId: this._browserContextId || undefined, script: source });
|
||||
}
|
||||
|
||||
async exposeFunction(name: string, playwrightFunction: Function): 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, playwrightFunction);
|
||||
this._pageBindings.set(name, binding);
|
||||
await this._browser._connection.send('Browser.addBinding', { browserContextId: this._browserContextId || undefined, name, script: binding.source });
|
||||
async _doExposeBinding(binding: PageBinding) {
|
||||
await this._browser._connection.send('Browser.addBinding', { browserContextId: this._browserContextId || undefined, name: binding.name, script: binding.source });
|
||||
}
|
||||
|
||||
async route(url: types.URLMatch, handler: network.RouteHandler): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import { Page } from './page';
|
|||
import { selectors } from './selectors';
|
||||
import * as types from './types';
|
||||
import { waitForTimeoutWasUsed } from './hints';
|
||||
import { BrowserContext } from './browserContext';
|
||||
|
||||
type ContextType = 'main' | 'utility';
|
||||
type ContextData = {
|
||||
|
|
@ -46,6 +47,8 @@ export type GotoResult = {
|
|||
|
||||
type ConsoleTagHandler = () => void;
|
||||
|
||||
export type FunctionWithSource = (source: { context: BrowserContext, page: Page, frame: Frame}, ...args: any) => any;
|
||||
|
||||
export class FrameManager {
|
||||
private _page: Page;
|
||||
private _frames = new Map<string, Frame>();
|
||||
|
|
|
|||
16
src/page.ts
16
src/page.ts
|
|
@ -253,11 +253,15 @@ export class Page extends ExtendedEventEmitter implements InnerLogger {
|
|||
}
|
||||
|
||||
async exposeFunction(name: string, playwrightFunction: Function) {
|
||||
await this.exposeBinding(name, (options, ...args: any) => playwrightFunction(...args));
|
||||
}
|
||||
|
||||
async exposeBinding(name: string, 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, playwrightFunction);
|
||||
const binding = new PageBinding(name, playwrightBinding);
|
||||
this._pageBindings.set(name, binding);
|
||||
await this._delegate.exposeBinding(binding);
|
||||
}
|
||||
|
|
@ -267,7 +271,7 @@ export class Page extends ExtendedEventEmitter implements InnerLogger {
|
|||
return this._delegate.updateExtraHTTPHeaders();
|
||||
}
|
||||
|
||||
async _onBindingCalled(payload: string, context: js.ExecutionContext) {
|
||||
async _onBindingCalled(payload: string, context: dom.FrameExecutionContext) {
|
||||
await PageBinding.dispatch(this, payload, context);
|
||||
}
|
||||
|
||||
|
|
@ -580,23 +584,23 @@ export class Worker extends EventEmitter {
|
|||
|
||||
export class PageBinding {
|
||||
readonly name: string;
|
||||
readonly playwrightFunction: Function;
|
||||
readonly playwrightFunction: frames.FunctionWithSource;
|
||||
readonly source: string;
|
||||
|
||||
constructor(name: string, playwrightFunction: Function) {
|
||||
constructor(name: string, playwrightFunction: frames.FunctionWithSource) {
|
||||
this.name = name;
|
||||
this.playwrightFunction = playwrightFunction;
|
||||
this.source = helper.evaluationString(addPageBinding, name);
|
||||
}
|
||||
|
||||
static async dispatch(page: Page, payload: string, context: js.ExecutionContext) {
|
||||
static async dispatch(page: Page, payload: string, context: dom.FrameExecutionContext) {
|
||||
const {name, seq, args} = JSON.parse(payload);
|
||||
let expression = null;
|
||||
try {
|
||||
let binding = page._pageBindings.get(name);
|
||||
if (!binding)
|
||||
binding = page._browserContext._pageBindings.get(name);
|
||||
const result = await binding!.playwrightFunction(...args);
|
||||
const result = await binding!.playwrightFunction({ frame: context.frame, page, context: page._browserContext }, ...args);
|
||||
expression = helper.evaluationString(deliverResult, name, seq, result);
|
||||
} catch (error) {
|
||||
if (error instanceof Error)
|
||||
|
|
|
|||
|
|
@ -308,15 +308,7 @@ export class WKBrowserContext extends BrowserContextBase {
|
|||
await (page._delegate as WKPage)._updateBootstrapScript();
|
||||
}
|
||||
|
||||
async exposeFunction(name: string, playwrightFunction: Function): 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, playwrightFunction);
|
||||
this._pageBindings.set(name, binding);
|
||||
async _doExposeBinding(binding: PageBinding) {
|
||||
for (const page of this.pages())
|
||||
await (page._delegate as WKPage).exposeBinding(binding);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -326,6 +326,26 @@ describe('BrowserContext.pages()', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('BrowserContext.exposeBinding', () => {
|
||||
it('should work', async({browser}) => {
|
||||
const context = await browser.newContext();
|
||||
let bindingSource;
|
||||
await context.exposeBinding('add', (source, a, b) => {
|
||||
bindingSource = source;
|
||||
return a + b;
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const result = await page.evaluate(async function() {
|
||||
return add(5, 6);
|
||||
});
|
||||
expect(bindingSource.context).toBe(context);
|
||||
expect(bindingSource.page).toBe(page);
|
||||
expect(bindingSource.frame).toBe(page.mainFrame());
|
||||
expect(result).toEqual(11);
|
||||
await context.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('BrowserContext.exposeFunction', () => {
|
||||
it('should work', async({browser, server}) => {
|
||||
const context = await browser.newContext();
|
||||
|
|
|
|||
|
|
@ -405,6 +405,26 @@ describe('Page.waitForResponse', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Page.exposeBinding', () => {
|
||||
it('should work', async({browser}) => {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
let bindingSource;
|
||||
await page.exposeBinding('add', (source, a, b) => {
|
||||
bindingSource = source;
|
||||
return a + b;
|
||||
});
|
||||
const result = await page.evaluate(async function() {
|
||||
return add(5, 6);
|
||||
});
|
||||
expect(bindingSource.context).toBe(context);
|
||||
expect(bindingSource.page).toBe(page);
|
||||
expect(bindingSource.frame).toBe(page.mainFrame());
|
||||
expect(result).toEqual(11);
|
||||
await context.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Page.exposeFunction', function() {
|
||||
it('should work', async({page, server}) => {
|
||||
await page.exposeFunction('compute', function(a, b) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue