api(popup): introduce BrowserContext.exposeFunction (#1176)

This commit is contained in:
Dmitry Gozman 2020-03-03 16:46:06 -08:00 committed by GitHub
parent 1b863c2300
commit 6c6cdc033b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 311 additions and 89 deletions

View file

@ -271,6 +271,7 @@ await context.close();
- [browserContext.clearPermissions()](#browsercontextclearpermissions) - [browserContext.clearPermissions()](#browsercontextclearpermissions)
- [browserContext.close()](#browsercontextclose) - [browserContext.close()](#browsercontextclose)
- [browserContext.cookies([...urls])](#browsercontextcookiesurls) - [browserContext.cookies([...urls])](#browsercontextcookiesurls)
- [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction)
- [browserContext.newPage()](#browsercontextnewpage) - [browserContext.newPage()](#browsercontextnewpage)
- [browserContext.pages()](#browsercontextpages) - [browserContext.pages()](#browsercontextpages)
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies) - [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)
@ -361,6 +362,73 @@ will be closed.
If no URLs are specified, this method returns all cookies. If no URLs are specified, this method returns all cookies.
If URLs are specified, only cookies that affect those URLs are returned. 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() #### browserContext.newPage()
- returns: <[Promise]<[Page]>> - returns: <[Promise]<[Page]>>
@ -1007,36 +1075,38 @@ await resultHandle.dispose();
- `playwrightFunction` <[function]> Callback function which will be called in Playwright's context. - `playwrightFunction` <[function]> Callback function which will be called in Playwright's context.
- returns: <[Promise]> - 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`. 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. 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. > **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 ```js
const { firefox } = require('playwright'); // Or 'chromium' or 'webkit'. const { webkit } = require('playwright'); // Or 'chromium' or 'firefox'.
const crypto = require('crypto'); const crypto = require('crypto');
(async () => { (async () => {
const browser = await firefox.launch(); const browser = await webkit.launch({ headless: false });
const page = await browser.newPage(); 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.exposeFunction('md5', text => await page.setContent(`
crypto.createHash('md5').update(text).digest('hex') <script>
); async function onClick() {
await page.evaluate(async () => { document.querySelector('div').textContent = await window.md5('PLAYWRIGHT');
// use window.md5 to compute hashes }
const myString = 'PLAYWRIGHT'; </script>
const myHash = await window.md5(myString); <button onclick="onClick()">Click me</button>
console.log(`md5 of ${myString} is ${myHash}`); <div></div>
}); `);
await browser.close(); 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 ```js
const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'. const { chromium } = require('playwright'); // Or 'firefox' or 'webkit'.
@ -3624,6 +3694,7 @@ const backgroundPage = await backroundPageTarget.page();
- [browserContext.clearPermissions()](#browsercontextclearpermissions) - [browserContext.clearPermissions()](#browsercontextclearpermissions)
- [browserContext.close()](#browsercontextclose) - [browserContext.close()](#browsercontextclose)
- [browserContext.cookies([...urls])](#browsercontextcookiesurls) - [browserContext.cookies([...urls])](#browsercontextcookiesurls)
- [browserContext.exposeFunction(name, playwrightFunction)](#browsercontextexposefunctionname-playwrightfunction)
- [browserContext.newPage()](#browsercontextnewpage) - [browserContext.newPage()](#browsercontextnewpage)
- [browserContext.pages()](#browsercontextpages) - [browserContext.pages()](#browsercontextpages)
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies) - [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)

View file

@ -15,7 +15,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Page } from './page'; import { Page, PageBinding } from './page';
import * as network from './network'; import * as network from './network';
import * as types from './types'; import * as types from './types';
import { helper } from './helper'; import { helper } from './helper';
@ -47,11 +47,13 @@ export interface BrowserContext {
setGeolocation(geolocation: types.Geolocation | null): Promise<void>; setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
setExtraHTTPHeaders(headers: network.Headers): Promise<void>; setExtraHTTPHeaders(headers: network.Headers): Promise<void>;
addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]): Promise<void>; addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]): Promise<void>;
exposeFunction(name: string, playwrightFunction: Function): Promise<void>;
close(): Promise<void>; close(): Promise<void>;
_existingPages(): Page[]; _existingPages(): Page[];
readonly _timeoutSettings: TimeoutSettings; readonly _timeoutSettings: TimeoutSettings;
readonly _options: BrowserContextOptions; readonly _options: BrowserContextOptions;
readonly _pageBindings: Map<string, PageBinding>;
} }
export function assertBrowserContextIsNotOwned(context: BrowserContext) { export function assertBrowserContextIsNotOwned(context: BrowserContext) {

View file

@ -20,7 +20,7 @@ import { Events as CommonEvents } from '../events';
import { assert, helper, debugError } from '../helper'; import { assert, helper, debugError } from '../helper';
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext'; import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
import { CRConnection, ConnectionEvents, CRSession } from './crConnection'; import { CRConnection, ConnectionEvents, CRSession } from './crConnection';
import { Page, PageEvent } from '../page'; import { Page, PageEvent, PageBinding } from '../page';
import { CRTarget } from './crTarget'; import { CRTarget } from './crTarget';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { CRPage } from './crPage'; import { CRPage } from './crPage';
@ -204,6 +204,7 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
readonly _options: BrowserContextOptions; readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings; readonly _timeoutSettings: TimeoutSettings;
readonly _evaluateOnNewDocumentSources: string[]; readonly _evaluateOnNewDocumentSources: string[];
readonly _pageBindings = new Map<string, PageBinding>();
private _closed = false; private _closed = false;
constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) { constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) {
@ -325,6 +326,19 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
await (page._delegate as CRPage).evaluateOnNewDocument(source); 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() { async close() {
if (this._closed) if (this._closed)
return; return;

View file

@ -23,7 +23,7 @@ import * as network from '../network';
import { CRSession, CRConnection } from './crConnection'; import { CRSession, CRConnection } from './crConnection';
import { EVALUATION_SCRIPT_URL, CRExecutionContext } from './crExecutionContext'; import { EVALUATION_SCRIPT_URL, CRExecutionContext } from './crExecutionContext';
import { CRNetworkManager } from './crNetworkManager'; import { CRNetworkManager } from './crNetworkManager';
import { Page, Worker } from '../page'; import { Page, Worker, PageBinding } from '../page';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { Events } from '../events'; import { Events } from '../events';
import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crProtocolHelper'; import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crProtocolHelper';
@ -120,6 +120,8 @@ export class CRPage implements PageDelegate {
if (options.geolocation) if (options.geolocation)
promises.push(this._client.send('Emulation.setGeolocationOverride', options.geolocation)); promises.push(this._client.send('Emulation.setGeolocationOverride', options.geolocation));
promises.push(this.updateExtraHTTPHeaders()); promises.push(this.updateExtraHTTPHeaders());
for (const binding of this._browserContext._pageBindings.values())
promises.push(this._initBinding(binding));
for (const source of this._browserContext._evaluateOnNewDocumentSources) for (const source of this._browserContext._evaluateOnNewDocumentSources)
promises.push(this.evaluateOnNewDocument(source)); promises.push(this.evaluateOnNewDocument(source));
await Promise.all(promises); await Promise.all(promises);
@ -276,10 +278,16 @@ export class CRPage implements PageDelegate {
this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace)); this._page._addConsoleMessage(event.type, values, toConsoleMessageLocation(event.stackTrace));
} }
async exposeBinding(name: string, bindingFunction: string) { async exposeBinding(binding: PageBinding) {
await this._client.send('Runtime.addBinding', {name: name}); await this._initBinding(binding);
await this._client.send('Page.addScriptToEvaluateOnNewDocument', {source: bindingFunction}); await Promise.all(this._page.frames().map(frame => frame.evaluate(binding.source).catch(debugError)));
await Promise.all(this._page.frames().map(frame => frame.evaluate(bindingFunction).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) { _onBindingCalled(event: Protocol.Runtime.bindingCalledPayload) {

View file

@ -21,7 +21,7 @@ import { Events } from '../events';
import { assert, helper, RegisteredListener, debugError } from '../helper'; import { assert, helper, RegisteredListener, debugError } from '../helper';
import * as network from '../network'; import * as network from '../network';
import * as types from '../types'; import * as types from '../types';
import { Page, PageEvent } from '../page'; import { Page, PageEvent, PageBinding } from '../page';
import { ConnectionEvents, FFConnection, FFSessionEvents, FFSession } from './ffConnection'; import { ConnectionEvents, FFConnection, FFSessionEvents, FFSession } from './ffConnection';
import { FFPage } from './ffPage'; import { FFPage } from './ffPage';
import * as platform from '../platform'; import * as platform from '../platform';
@ -265,6 +265,7 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
readonly _timeoutSettings: TimeoutSettings; readonly _timeoutSettings: TimeoutSettings;
private _closed = false; private _closed = false;
private readonly _evaluateOnNewDocumentSources: string[]; private readonly _evaluateOnNewDocumentSources: string[];
readonly _pageBindings = new Map<string, PageBinding>();
constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) { constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) {
super(); super();
@ -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 }); 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() { async close() {
if (this._closed) if (this._closed)
return; return;

View file

@ -20,7 +20,7 @@ import { helper, RegisteredListener, debugError, assert } from '../helper';
import * as dom from '../dom'; import * as dom from '../dom';
import { FFSession } from './ffConnection'; import { FFSession } from './ffConnection';
import { FFExecutionContext } from './ffExecutionContext'; import { FFExecutionContext } from './ffExecutionContext';
import { Page, PageDelegate, Worker } from '../page'; import { Page, PageDelegate, Worker, PageBinding } from '../page';
import { FFNetworkManager, headersArray } from './ffNetworkManager'; import { FFNetworkManager, headersArray } from './ffNetworkManager';
import { Events } from '../events'; import { Events } from '../events';
import * as dialog from '../dialog'; import * as dialog from '../dialog';
@ -233,10 +233,10 @@ export class FFPage implements PageDelegate {
this._page._didCrash(); this._page._didCrash();
} }
async exposeBinding(name: string, bindingFunction: string): Promise<void> { async exposeBinding(binding: PageBinding) {
await this._session.send('Page.addBinding', {name: name}); await this._session.send('Page.addBinding', {name: binding.name});
await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: bindingFunction}); await this._session.send('Page.addScriptToEvaluateOnNewDocument', {script: binding.source});
await Promise.all(this._page.frames().map(frame => frame.evaluate(bindingFunction).catch(debugError))); await Promise.all(this._page.frames().map(frame => frame.evaluate(binding.source).catch(debugError)));
} }
didClose() { didClose() {

View file

@ -39,7 +39,7 @@ export interface PageDelegate {
reload(): Promise<void>; reload(): Promise<void>;
goBack(): Promise<boolean>; goBack(): Promise<boolean>;
goForward(): Promise<boolean>; goForward(): Promise<boolean>;
exposeBinding(name: string, bindingFunction: string): Promise<void>; exposeBinding(binding: PageBinding): Promise<void>;
evaluateOnNewDocument(source: string): Promise<void>; evaluateOnNewDocument(source: string): Promise<void>;
closePage(runBeforeUnload: boolean): Promise<void>; closePage(runBeforeUnload: boolean): Promise<void>;
@ -117,7 +117,7 @@ export class Page extends platform.EventEmitter {
readonly _timeoutSettings: TimeoutSettings; readonly _timeoutSettings: TimeoutSettings;
readonly _delegate: PageDelegate; readonly _delegate: PageDelegate;
readonly _state: PageState; readonly _state: PageState;
private _pageBindings = new Map<string, Function>(); readonly _pageBindings = new Map<string, PageBinding>();
readonly _screenshotter: Screenshotter; readonly _screenshotter: Screenshotter;
readonly _frameManager: frames.FrameManager; readonly _frameManager: frames.FrameManager;
readonly accessibility: accessibility.Accessibility; readonly accessibility: accessibility.Accessibility;
@ -256,26 +256,12 @@ export class Page extends platform.EventEmitter {
async exposeFunction(name: string, playwrightFunction: Function) { async exposeFunction(name: string, playwrightFunction: Function) {
if (this._pageBindings.has(name)) if (this._pageBindings.has(name))
throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`); throw new Error(`Function "${name}" has been already registered`);
this._pageBindings.set(name, playwrightFunction); if (this._browserContext._pageBindings.has(name))
await this._delegate.exposeBinding(name, helper.evaluationString(addPageBinding, name)); throw new Error(`Function "${name}" has been already registered in the browser context`);
const binding = new PageBinding(name, playwrightFunction);
function addPageBinding(bindingName: string) { this._pageBindings.set(name, binding);
const binding = (window as any)[bindingName]; await this._delegate.exposeBinding(binding);
(window as any)[bindingName] = (...args: any[]) => {
const me = (window as any)[bindingName];
let callbacks = me['callbacks'];
if (!callbacks) {
callbacks = new Map();
me['callbacks'] = callbacks;
}
const seq = (me['lastSeq'] || 0) + 1;
me['lastSeq'] = seq;
const promise = new Promise((resolve, reject) => callbacks.set(seq, {resolve, reject}));
binding(JSON.stringify({name: bindingName, seq, args}));
return promise;
};
}
} }
setExtraHTTPHeaders(headers: network.Headers) { setExtraHTTPHeaders(headers: network.Headers) {
@ -284,35 +270,7 @@ export class Page extends platform.EventEmitter {
} }
async _onBindingCalled(payload: string, context: js.ExecutionContext) { async _onBindingCalled(payload: string, context: js.ExecutionContext) {
const {name, seq, args} = JSON.parse(payload); await PageBinding.dispatch(this, payload, context);
let expression = null;
try {
const result = await this._pageBindings.get(name)!(...args);
expression = helper.evaluationString(deliverResult, name, seq, result);
} catch (error) {
if (error instanceof Error)
expression = helper.evaluationString(deliverError, name, seq, error.message, error.stack);
else
expression = helper.evaluationString(deliverErrorValue, name, seq, error);
}
context.evaluate(expression).catch(debugError);
function deliverResult(name: string, seq: number, result: any) {
(window as any)[name]['callbacks'].get(seq).resolve(result);
(window as any)[name]['callbacks'].delete(seq);
}
function deliverError(name: string, seq: number, message: string, stack: string) {
const error = new Error(message);
error.stack = stack;
(window as any)[name]['callbacks'].get(seq).reject(error);
(window as any)[name]['callbacks'].delete(seq);
}
function deliverErrorValue(name: string, seq: number, value: any) {
(window as any)[name]['callbacks'].get(seq).reject(value);
(window as any)[name]['callbacks'].delete(seq);
}
} }
_addConsoleMessage(type: string, args: js.JSHandle[], location: ConsoleMessageLocation, text?: string) { _addConsoleMessage(type: string, args: js.JSHandle[], location: ConsoleMessageLocation, text?: string) {
@ -609,3 +567,67 @@ export class Worker extends platform.EventEmitter {
return (await this._executionContextPromise).evaluateHandle(pageFunction, ...args as any); return (await this._executionContextPromise).evaluateHandle(pageFunction, ...args as any);
} }
} }
export class PageBinding {
readonly name: string;
readonly playwrightFunction: Function;
readonly source: string;
constructor(name: string, playwrightFunction: Function) {
this.name = name;
this.playwrightFunction = playwrightFunction;
this.source = helper.evaluationString(addPageBinding, name);
}
static async dispatch(page: Page, payload: string, context: js.ExecutionContext) {
const {name, seq, args} = JSON.parse(payload);
let expression = null;
try {
let binding = page._pageBindings.get(name);
if (!binding)
binding = page.context()._pageBindings.get(name);
const result = await binding!.playwrightFunction(...args);
expression = helper.evaluationString(deliverResult, name, seq, result);
} catch (error) {
if (error instanceof Error)
expression = helper.evaluationString(deliverError, name, seq, error.message, error.stack);
else
expression = helper.evaluationString(deliverErrorValue, name, seq, error);
}
context.evaluate(expression).catch(debugError);
function deliverResult(name: string, seq: number, result: any) {
(window as any)[name]['callbacks'].get(seq).resolve(result);
(window as any)[name]['callbacks'].delete(seq);
}
function deliverError(name: string, seq: number, message: string, stack: string) {
const error = new Error(message);
error.stack = stack;
(window as any)[name]['callbacks'].get(seq).reject(error);
(window as any)[name]['callbacks'].delete(seq);
}
function deliverErrorValue(name: string, seq: number, value: any) {
(window as any)[name]['callbacks'].get(seq).reject(value);
(window as any)[name]['callbacks'].delete(seq);
}
}
}
function addPageBinding(bindingName: string) {
const binding = (window as any)[bindingName];
(window as any)[bindingName] = (...args: any[]) => {
const me = (window as any)[bindingName];
let callbacks = me['callbacks'];
if (!callbacks) {
callbacks = new Map();
me['callbacks'] = callbacks;
}
const seq = (me['lastSeq'] || 0) + 1;
me['lastSeq'] = seq;
const promise = new Promise((resolve, reject) => callbacks.set(seq, {resolve, reject}));
binding(JSON.stringify({name: bindingName, seq, args}));
return promise;
};
}

View file

@ -19,7 +19,7 @@ import { Browser, createPageInNewContext } from '../browser';
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext'; import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
import { assert, helper, RegisteredListener, debugError } from '../helper'; import { assert, helper, RegisteredListener, debugError } from '../helper';
import * as network from '../network'; import * as network from '../network';
import { Page, PageEvent } from '../page'; import { Page, PageBinding, PageEvent } from '../page';
import { ConnectionTransport, SlowMoTransport } from '../transport'; import { ConnectionTransport, SlowMoTransport } from '../transport';
import * as types from '../types'; import * as types from '../types';
import { Events } from '../events'; import { Events } from '../events';
@ -187,6 +187,7 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
readonly _timeoutSettings: TimeoutSettings; readonly _timeoutSettings: TimeoutSettings;
private _closed = false; private _closed = false;
readonly _evaluateOnNewDocumentSources: string[]; readonly _evaluateOnNewDocumentSources: string[];
readonly _pageBindings = new Map<string, PageBinding>();
constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) { constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) {
super(); super();
@ -297,6 +298,19 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
await (page._delegate as WKPage)._updateBootstrapScript(); await (page._delegate as WKPage)._updateBootstrapScript();
} }
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 WKPage).exposeBinding(binding);
}
async close() { async close() {
if (this._closed) if (this._closed)
return; return;

View file

@ -24,7 +24,7 @@ import { Events } from '../events';
import { WKExecutionContext } from './wkExecutionContext'; import { WKExecutionContext } from './wkExecutionContext';
import { WKInterceptableRequest } from './wkInterceptableRequest'; import { WKInterceptableRequest } from './wkInterceptableRequest';
import { WKWorkers } from './wkWorkers'; import { WKWorkers } from './wkWorkers';
import { Page, PageDelegate } from '../page'; import { Page, PageDelegate, PageBinding } from '../page';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import * as dialog from '../dialog'; import * as dialog from '../dialog';
import { RawMouseImpl, RawKeyboardImpl } from './wkInput'; import { RawMouseImpl, RawKeyboardImpl } from './wkInput';
@ -52,7 +52,7 @@ export class WKPage implements PageDelegate {
private readonly _contextIdToContext: Map<number, dom.FrameExecutionContext>; private readonly _contextIdToContext: Map<number, dom.FrameExecutionContext>;
private _mainFrameContextId?: number; private _mainFrameContextId?: number;
private _sessionListeners: RegisteredListener[] = []; private _sessionListeners: RegisteredListener[] = [];
private readonly _bootstrapScripts: string[] = []; private readonly _evaluateOnNewDocumentSources: string[] = [];
private readonly _browserContext: WKBrowserContext; private readonly _browserContext: WKBrowserContext;
constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPageProxy | null) { constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPageProxy | null) {
@ -140,6 +140,8 @@ export class WKPage implements PageDelegate {
if (this._page._state.mediaType || this._page._state.colorScheme) if (this._page._state.mediaType || this._page._state.colorScheme)
promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme)); promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme));
promises.push(session.send('Page.setBootstrapScript', { source: this._calculateBootstrapScript() })); promises.push(session.send('Page.setBootstrapScript', { source: this._calculateBootstrapScript() }));
for (const binding of this._browserContext._pageBindings.values())
promises.push(this._evaluateBindingScript(binding));
if (contextOptions.bypassCSP) if (contextOptions.bypassCSP)
promises.push(session.send('Page.setBypassCSP', { enabled: true })); promises.push(session.send('Page.setBypassCSP', { enabled: true }));
promises.push(session.send('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() })); promises.push(session.send('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() }));
@ -461,20 +463,34 @@ export class WKPage implements PageDelegate {
}); });
} }
async exposeBinding(name: string, bindingFunction: string): Promise<void> { async exposeBinding(binding: PageBinding): Promise<void> {
const script = `self.${name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${bindingFunction}`;
this._bootstrapScripts.unshift(script);
await this._updateBootstrapScript(); await this._updateBootstrapScript();
await this._evaluateBindingScript(binding);
}
private async _evaluateBindingScript(binding: PageBinding): Promise<void> {
const script = this._bindingToScript(binding);
await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(debugError))); await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(debugError)));
} }
async evaluateOnNewDocument(script: string): Promise<void> { async evaluateOnNewDocument(script: string): Promise<void> {
this._bootstrapScripts.push(script); this._evaluateOnNewDocumentSources.push(script);
await this._updateBootstrapScript(); await this._updateBootstrapScript();
} }
private _bindingToScript(binding: PageBinding): string {
return `self.${binding.name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${binding.source}`;
}
private _calculateBootstrapScript(): string { private _calculateBootstrapScript(): string {
return [...this._browserContext._evaluateOnNewDocumentSources, ...this._bootstrapScripts].join(';'); const scripts: string[] = [];
for (const binding of this._browserContext._pageBindings.values())
scripts.push(this._bindingToScript(binding));
for (const binding of this._page._pageBindings.values())
scripts.push(this._bindingToScript(binding));
scripts.push(...this._browserContext._evaluateOnNewDocumentSources);
scripts.push(...this._evaluateOnNewDocumentSources);
return scripts.join(';');
} }
async _updateBootstrapScript(): Promise<void> { async _updateBootstrapScript(): Promise<void> {

View file

@ -306,6 +306,48 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, FF
}); });
}); });
describe('BrowserContext.exposeFunction', () => {
it.fail(CHROMIUM || FFOX)('should work', async({browser, server}) => {
const context = await browser.newContext();
await context.exposeFunction('add', (a, b) => a + b);
const page = await context.newPage();
await page.exposeFunction('mul', (a, b) => a * b);
const result = await page.evaluate(async function() {
return { mul: await mul(9, 4), add: await add(9, 4) };
});
expect(result).toEqual({ mul: 36, add: 13 });
await context.close();
});
it.fail(FFOX)('should throw for duplicate registrations', async({browser, server}) => {
const context = await browser.newContext();
await context.exposeFunction('foo', () => {});
await context.exposeFunction('bar', () => {});
let error = await context.exposeFunction('foo', () => {}).catch(e => e);
expect(error.message).toBe('Function "foo" has been already registered');
const page = await context.newPage();
error = await page.exposeFunction('foo', () => {}).catch(e => e);
expect(error.message).toBe('Function "foo" has been already registered in the browser context');
await page.exposeFunction('baz', () => {});
error = await context.exposeFunction('baz', () => {}).catch(e => e);
expect(error.message).toBe('Function "baz" has been already registered in one of the pages');
await context.close();
});
it.skip(FFOX)('should be callable from-inside addInitScript', async({browser, server}) => {
const context = await browser.newContext();
let args = [];
await context.exposeFunction('woof', function(arg) {
args.push(arg);
});
await context.addInitScript(() => woof('context'));
const page = await context.newPage();
await page.addInitScript(() => woof('page'));
args = [];
await page.reload();
expect(args).toEqual(['context', 'page']);
await context.close();
});
});
describe('Events.BrowserContext.Page', function() { describe('Events.BrowserContext.Page', function() {
it('should report when a new page is created and closed', async({browser, server}) => { it('should report when a new page is created and closed', async({browser, server}) => {
const context = await browser.newContext(); const context = await browser.newContext();

View file

@ -274,7 +274,15 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
await page.evaluate(() => { window.JSON.stringify = null; window.JSON = null; }); await page.evaluate(() => { window.JSON.stringify = null; window.JSON = null; });
const result = await page.evaluate(() => ({abc: 123})); const result = await page.evaluate(() => ({abc: 123}));
expect(result).toEqual({abc: 123}); expect(result).toEqual({abc: 123});
}) });
it.fail(WEBKIT)('should await promise from popup', async function({page, server}) {
await page.goto(server.EMPTY_PAGE);
const result = await page.evaluate(() => {
const win = window.open('about:blank');
return new win.Promise(f => f(42));
});
expect(result).toBe(42);
});
}); });
describe('Page.addInitScript', function() { describe('Page.addInitScript', function() {

View file

@ -86,6 +86,18 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
await context.close(); await context.close();
expect(injected).toBe(123); expect(injected).toBe(123);
}); });
it.fail(CHROMIUM || FFOX)('should expose function from browser context', async function({browser, server}) {
const context = await browser.newContext();
await context.exposeFunction('add', (a, b) => a + b);
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
const added = await page.evaluate(async () => {
const win = window.open('about:blank');
return win.add(9, 4);
});
await context.close();
expect(added).toBe(13);
});
}); });
describe('Page.Events.Popup', function() { describe('Page.Events.Popup', function() {