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.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)

View file

@ -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) {

View file

@ -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;

View file

@ -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) {

View file

@ -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;

View file

@ -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() {

View file

@ -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;
};
}

View file

@ -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;

View file

@ -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> {

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() {
it('should report when a new page is created and closed', async({browser, server}) => {
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; });
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() {

View file

@ -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() {