api(popup): introduce BrowserContext.exposeFunction (#1176)
This commit is contained in:
parent
1b863c2300
commit
6c6cdc033b
103
docs/api.md
103
docs/api.md
|
|
@ -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)
|
||||
|
|
@ -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]>>
|
||||
|
||||
|
|
@ -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'.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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);
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
124
src/page.ts
124
src/page.ts
|
|
@ -39,7 +39,7 @@ export interface PageDelegate {
|
|||
reload(): Promise<void>;
|
||||
goBack(): Promise<boolean>;
|
||||
goForward(): Promise<boolean>;
|
||||
exposeBinding(name: string, bindingFunction: string): Promise<void>;
|
||||
exposeBinding(binding: PageBinding): Promise<void>;
|
||||
evaluateOnNewDocument(source: string): Promise<void>;
|
||||
closePage(runBeforeUnload: boolean): Promise<void>;
|
||||
|
||||
|
|
@ -117,7 +117,7 @@ export class Page extends platform.EventEmitter {
|
|||
readonly _timeoutSettings: TimeoutSettings;
|
||||
readonly _delegate: PageDelegate;
|
||||
readonly _state: PageState;
|
||||
private _pageBindings = new Map<string, Function>();
|
||||
readonly _pageBindings = new Map<string, PageBinding>();
|
||||
readonly _screenshotter: Screenshotter;
|
||||
readonly _frameManager: frames.FrameManager;
|
||||
readonly accessibility: accessibility.Accessibility;
|
||||
|
|
@ -256,26 +256,12 @@ export class Page extends platform.EventEmitter {
|
|||
|
||||
async exposeFunction(name: string, playwrightFunction: Function) {
|
||||
if (this._pageBindings.has(name))
|
||||
throw new Error(`Failed to add page binding with name ${name}: window['${name}'] already exists!`);
|
||||
this._pageBindings.set(name, playwrightFunction);
|
||||
await this._delegate.exposeBinding(name, helper.evaluationString(addPageBinding, name));
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
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);
|
||||
this._pageBindings.set(name, binding);
|
||||
await this._delegate.exposeBinding(binding);
|
||||
}
|
||||
|
||||
setExtraHTTPHeaders(headers: network.Headers) {
|
||||
|
|
@ -284,35 +270,7 @@ export class Page extends platform.EventEmitter {
|
|||
}
|
||||
|
||||
async _onBindingCalled(payload: string, context: js.ExecutionContext) {
|
||||
const {name, seq, args} = JSON.parse(payload);
|
||||
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);
|
||||
}
|
||||
await PageBinding.dispatch(this, payload, context);
|
||||
}
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { Browser, createPageInNewContext } from '../browser';
|
|||
import { BrowserContext, BrowserContextOptions, validateBrowserContextOptions, assertBrowserContextIsNotOwned, verifyGeolocation } from '../browserContext';
|
||||
import { assert, helper, RegisteredListener, debugError } from '../helper';
|
||||
import * as network from '../network';
|
||||
import { Page, PageEvent } from '../page';
|
||||
import { Page, PageBinding, PageEvent } from '../page';
|
||||
import { ConnectionTransport, SlowMoTransport } from '../transport';
|
||||
import * as types from '../types';
|
||||
import { Events } from '../events';
|
||||
|
|
@ -187,6 +187,7 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
|
|||
readonly _timeoutSettings: TimeoutSettings;
|
||||
private _closed = false;
|
||||
readonly _evaluateOnNewDocumentSources: string[];
|
||||
readonly _pageBindings = new Map<string, PageBinding>();
|
||||
|
||||
constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) {
|
||||
super();
|
||||
|
|
@ -297,6 +298,19 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
|
|||
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() {
|
||||
if (this._closed)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { Events } from '../events';
|
|||
import { WKExecutionContext } from './wkExecutionContext';
|
||||
import { WKInterceptableRequest } from './wkInterceptableRequest';
|
||||
import { WKWorkers } from './wkWorkers';
|
||||
import { Page, PageDelegate } from '../page';
|
||||
import { Page, PageDelegate, PageBinding } from '../page';
|
||||
import { Protocol } from './protocol';
|
||||
import * as dialog from '../dialog';
|
||||
import { RawMouseImpl, RawKeyboardImpl } from './wkInput';
|
||||
|
|
@ -52,7 +52,7 @@ export class WKPage implements PageDelegate {
|
|||
private readonly _contextIdToContext: Map<number, dom.FrameExecutionContext>;
|
||||
private _mainFrameContextId?: number;
|
||||
private _sessionListeners: RegisteredListener[] = [];
|
||||
private readonly _bootstrapScripts: string[] = [];
|
||||
private readonly _evaluateOnNewDocumentSources: string[] = [];
|
||||
private readonly _browserContext: WKBrowserContext;
|
||||
|
||||
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)
|
||||
promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme));
|
||||
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)
|
||||
promises.push(session.send('Page.setBypassCSP', { enabled: true }));
|
||||
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> {
|
||||
const script = `self.${name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${bindingFunction}`;
|
||||
this._bootstrapScripts.unshift(script);
|
||||
async exposeBinding(binding: PageBinding): Promise<void> {
|
||||
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)));
|
||||
}
|
||||
|
||||
async evaluateOnNewDocument(script: string): Promise<void> {
|
||||
this._bootstrapScripts.push(script);
|
||||
this._evaluateOnNewDocumentSources.push(script);
|
||||
await this._updateBootstrapScript();
|
||||
}
|
||||
|
||||
private _bindingToScript(binding: PageBinding): string {
|
||||
return `self.${binding.name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${binding.source}`;
|
||||
}
|
||||
|
||||
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> {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
it('should report when a new page is created and closed', async({browser, server}) => {
|
||||
const context = await browser.newContext();
|
||||
|
|
|
|||
|
|
@ -274,7 +274,15 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
|
|||
await page.evaluate(() => { window.JSON.stringify = null; window.JSON = null; });
|
||||
const result = await page.evaluate(() => ({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() {
|
||||
|
|
|
|||
|
|
@ -86,6 +86,18 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
|
|||
await context.close();
|
||||
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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue