Skip to content

Commit

Permalink
api(popup): introduce BrowserContext.exposeFunction (#1176)
Browse files Browse the repository at this point in the history
  • Loading branch information
dgozman committed Mar 4, 2020
1 parent 1b863c2 commit 6c6cdc0
Show file tree
Hide file tree
Showing 12 changed files with 311 additions and 89 deletions.
103 changes: 87 additions & 16 deletions docs/api.md
Expand Up @@ -271,6 +271,7 @@ await context.close();
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
- [browserContext.close()](#browsercontextclose)
- [browserContext.cookies([...urls])](#browsercontextcookiesurls)
- [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction)
- [browserContext.newPage()](#browsercontextnewpage)
- [browserContext.pages()](#browsercontextpages)
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)
Expand Down Expand Up @@ -361,6 +362,73 @@ 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.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 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`.

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'.
const crypto = require('crypto');

(async () => {
const browser = await webkit.launch({ headless: false });
const context = await browser.newContext();
await context.exposeFunction('md5', text => crypto.createHash('md5').update(text).digest('hex'));
const page = await context.newPage();
await page.setContent(`
<script>
async function onClick() {
document.querySelector('div').textContent = await window.md5('PLAYWRIGHT');
}
</script>
<button onclick="onClick()">Click me</button>
<div></div>
`);
await page.click('button');
})();
```

An example of adding a `window.readfile` function to all pages in the context:

```js
const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
const fs = require('fs');

(async () => {
const browser = await chromium.launch();
const context = await browser.newContext();
await context.exposeFunction('readfile', async filePath => {
return new Promise((resolve, reject) => {
fs.readFile(filePath, 'utf8', (err, text) => {
if (err)
reject(err);
else
resolve(text);
});
});
});
const page = await context.newPage();
page.on('console', msg => console.log(msg.text()));
await page.evaluate(async () => {
// use window.readfile to read contents of a file
const content = await window.readfile('/etc/hosts');
console.log(content);
});
await browser.close();
})();
```

#### browserContext.newPage()
- returns: <[Promise]<[Page]>>

Expand Down Expand Up @@ -1007,36 +1075,38 @@ await resultHandle.dispose();
- `playwrightFunction` <[function]> Callback function which will be called in Playwright's context.
- returns: <[Promise]>

The method adds a function called `name` on the page's `window` object.
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`.

If the `playwrightFunction` returns a [Promise], it will be awaited.

See [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction) for context-wide exposed function.

> **NOTE** Functions installed via `page.exposeFunction` survive navigations.
An example of adding an `md5` function into the page:
An example of adding an `md5` function to the page:
```js
const { firefox } = require('playwright'); // Or 'chromium' or 'webkit'.
const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
const crypto = require('crypto');

(async () => {
const browser = await firefox.launch();
const browser = await webkit.launch({ headless: false });
const page = await browser.newPage();
page.on('console', msg => console.log(msg.text()));
await page.exposeFunction('md5', text =>
crypto.createHash('md5').update(text).digest('hex')
);
await page.evaluate(async () => {
// use window.md5 to compute hashes
const myString = 'PLAYWRIGHT';
const myHash = await window.md5(myString);
console.log(`md5 of ${myString} is ${myHash}`);
});
await browser.close();
await page.exposeFunction('md5', text => crypto.createHash('md5').update(text).digest('hex'));
await page.setContent(`
<script>
async function onClick() {
document.querySelector('div').textContent = await window.md5('PLAYWRIGHT');
}
</script>
<button onclick="onClick()">Click me</button>
<div></div>
`);
await page.click('button');
})();
```

An example of adding a `window.readfile` function into the page:
An example of adding a `window.readfile` function to the page:

```js
const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
Expand Down Expand Up @@ -3624,6 +3694,7 @@ const backgroundPage = await backroundPageTarget.page();
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
- [browserContext.close()](#browsercontextclose)
- [browserContext.cookies([...urls])](#browsercontextcookiesurls)
- [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction)
- [browserContext.newPage()](#browsercontextnewpage)
- [browserContext.pages()](#browsercontextpages)
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)
Expand Down
4 changes: 3 additions & 1 deletion src/browserContext.ts
Expand Up @@ -15,7 +15,7 @@
* limitations under the License.
*/

import { Page } from './page';
import { Page, PageBinding } from './page';
import * as network from './network';
import * as types from './types';
import { helper } from './helper';
Expand Down Expand Up @@ -47,11 +47,13 @@ export interface BrowserContext {
setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
setExtraHTTPHeaders(headers: network.Headers): Promise<void>;
addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]): Promise<void>;
exposeFunction(name: string, playwrightFunction: Function): Promise<void>;
close(): Promise<void>;

_existingPages(): Page[];
readonly _timeoutSettings: TimeoutSettings;
readonly _options: BrowserContextOptions;
readonly _pageBindings: Map<string, PageBinding>;
}

export function assertBrowserContextIsNotOwned(context: BrowserContext) {
Expand Down
16 changes: 15 additions & 1 deletion src/chromium/crBrowser.ts
Expand Up @@ -20,7 +20,7 @@ import { Events as CommonEvents } from '../events';
import { assert, helper, debugError } from '../helper';
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
import { CRConnection, ConnectionEvents, CRSession } from './crConnection';
import { Page, PageEvent } from '../page';
import { Page, PageEvent, PageBinding } from '../page';
import { CRTarget } from './crTarget';
import { Protocol } from './protocol';
import { CRPage } from './crPage';
Expand Down Expand Up @@ -204,6 +204,7 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings;
readonly _evaluateOnNewDocumentSources: string[];
readonly _pageBindings = new Map<string, PageBinding>();
private _closed = false;

constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) {
Expand Down Expand Up @@ -325,6 +326,19 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
await (page._delegate as CRPage).evaluateOnNewDocument(source);
}

async exposeFunction(name: string, playwrightFunction: Function): Promise<void> {
for (const page of this._existingPages()) {
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);
for (const page of this._existingPages())
await (page._delegate as CRPage).exposeBinding(binding);
}

async close() {
if (this._closed)
return;
Expand Down
18 changes: 13 additions & 5 deletions src/chromium/crPage.ts
Expand Up @@ -23,7 +23,7 @@ import * as network from '../network';
import { CRSession, CRConnection } from './crConnection';
import { EVALUATION_SCRIPT_URL, CRExecutionContext } from './crExecutionContext';
import { CRNetworkManager } from './crNetworkManager';
import { Page, Worker } from '../page';
import { Page, Worker, PageBinding } from '../page';
import { Protocol } from './protocol';
import { Events } from '../events';
import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crProtocolHelper';
Expand Down Expand Up @@ -120,6 +120,8 @@ export class CRPage implements PageDelegate {
if (options.geolocation)
promises.push(this._client.send('Emulation.setGeolocationOverride', options.geolocation));
promises.push(this.updateExtraHTTPHeaders());
for (const binding of this._browserContext._pageBindings.values())
promises.push(this._initBinding(binding));
for (const source of this._browserContext._evaluateOnNewDocumentSources)
promises.push(this.evaluateOnNewDocument(source));
await Promise.all(promises);
Expand Down Expand Up @@ -276,10 +278,16 @@ export class CRPage implements PageDelegate {
this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace));
}

async exposeBinding(name: string, bindingFunction: string) {
await this._client.send('Runtime.addBinding', {name: name});
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: bindingFunction});
await Promise.all(this._page.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError)));
async exposeBinding(binding: PageBinding) {
await this._initBinding(binding);
await Promise.all(this._page.frames().map(frame => frame.evaluate(binding.source).catch(debugError)));
}

async _initBinding(binding: PageBinding) {
await Promise.all([
this._client.send('Runtime.addBinding', { name: binding.name }),
this._client.send('Page.addScriptToEvaluateOnNewDocument', { source: binding.source })
]);
}

_onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) {
Expand Down
15 changes: 14 additions & 1 deletion src/firefox/ffBrowser.ts
Expand Up @@ -21,7 +21,7 @@ import { Events } from '../events';
import { assert, helper, RegisteredListener, debugError } from '../helper';
import * as network from '../network';
import * as types from '../types';
import { Page, PageEvent } from '../page';
import { Page, PageEvent, PageBinding } from '../page';
import { ConnectionEvents, FFConnection, FFSessionEvents, FFSession } from './ffConnection';
import { FFPage } from './ffPage';
import * as platform from '../platform';
Expand Down Expand Up @@ -265,6 +265,7 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
readonly _timeoutSettings: TimeoutSettings;
private _closed = false;
private readonly _evaluateOnNewDocumentSources: string[];
readonly _pageBindings = new Map<string, PageBinding>();

constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) {
super();
Expand Down Expand Up @@ -368,6 +369,18 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
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._existingPages()) {
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);
throw new Error('Not implemented');
}

async close() {
if (this._closed)
return;
Expand Down
10 changes: 5 additions & 5 deletions src/firefox/ffPage.ts
Expand Up @@ -20,7 +20,7 @@ import { helper, RegisteredListener, debugError, assert } from '../helper';
import * as dom from '../dom';
import { FFSession } from './ffConnection';
import { FFExecutionContext } from './ffExecutionContext';
import { Page, PageDelegate, Worker } from '../page';
import { Page, PageDelegate, Worker, PageBinding } from '../page';
import { FFNetworkManager, headersArray } from './ffNetworkManager';
import { Events } from '../events';
import * as dialog from '../dialog';
Expand Down Expand Up @@ -233,10 +233,10 @@ export class FFPage implements PageDelegate {
this._page._didCrash();
}

async exposeBinding(name: string, bindingFunction: string): Promise<void> {
await this._session.send('Page.addBinding', {name: name});
await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: bindingFunction});
await Promise.all(this._page.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError)));
async exposeBinding(binding: PageBinding) {
await this._session.send('Page.addBinding', {name: binding.name});
await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: binding.source});
await Promise.all(this._page.frames().map(frame => frame.evaluate(binding.source).catch(debugError)));
}

didClose() {
Expand Down

0 comments on commit 6c6cdc0

Please sign in to comment.