feat(popups): add BrowserContext.evaluateOnNewDocument (#1136)

This commit is contained in:
Dmitry Gozman 2020-02-27 16:18:33 -08:00 committed by GitHub
parent dc161df063
commit 7682865d73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 114 additions and 33 deletions

View File

@ -268,6 +268,7 @@ await context.close();
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
- [browserContext.close()](#browsercontextclose)
- [browserContext.cookies([...urls])](#browsercontextcookiesurls)
- [browserContext.evaluateOnNewDocument(pageFunction[, ...args])](#browsercontextevaluateonnewdocumentpagefunction-args)
- [browserContext.newPage()](#browsercontextnewpage)
- [browserContext.pages()](#browsercontextpages)
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)
@ -308,7 +309,7 @@ context.clearPermissions();
Closes the browser context. All the targets that belong to the browser context
will be closed.
> **NOTE** only incognito browser contexts can be closed.
> **NOTE** the default browser context cannot be closed.
#### browserContext.cookies([...urls])
- `...urls` <...[string]>
@ -327,7 +328,29 @@ 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.
> **NOTE** the default browser context cannot be closed.
#### browserContext.evaluateOnNewDocument(pageFunction[, ...args])
- `pageFunction` <[function]|[string]> Function to be evaluated in all pages in the browser context
- `...args` <...[Serializable]> Arguments to pass to `pageFunction`
- returns: <[Promise]>
Adds a function which would be invoked in one of the following scenarios:
- Whenever a page is created in the browser context or is navigated.
- Whenever a child frame is attached or navigated in any page in the browser context. In this case, the function is invoked in the context of the newly attached frame.
The function is invoked after the document was created but before any of its scripts were run. This is useful to amend the JavaScript environment, e.g. to seed `Math.random`.
An example of overriding `Math.random` before the page loads:
```js
// preload.js
Math.random = () => 42;
// In your playwright script, assuming the preload.js file is in same folder
const preloadFile = fs.readFileSync('./preload.js', 'utf8');
await browserContext.evaluateOnNewDocument(preloadFile);
```
> **NOTE** The order of evaluation of multiple scripts installed via [browserContext.evaluateOnNewDocument(pageFunction[, ...args])](#browsercontextevaluateonnewdocumentpagefunction-args) and [page.evaluateOnNewDocument(pageFunction[, ...args])](#pageevaluateonnewdocumentpagefunction-args) is not defined.
#### browserContext.newPage()
- returns: <[Promise]<[Page]>>
@ -944,7 +967,7 @@ await resultHandle.dispose();
```
#### page.evaluateOnNewDocument(pageFunction[, ...args])
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
- `pageFunction` <[function]|[string]> Function to be evaluated in the page
- `...args` <...[Serializable]> Arguments to pass to `pageFunction`
- returns: <[Promise]>
@ -954,23 +977,19 @@ Adds a function which would be invoked in one of the following scenarios:
The function is invoked after the document was created but before any of its scripts were run. This is useful to amend the JavaScript environment, e.g. to seed `Math.random`.
An example of overriding the navigator.languages property before the page loads:
An example of overriding `Math.random` before the page loads:
```js
// preload.js
Math.random = () => 42;
// overwrite the `languages` property to use a custom getter
Object.defineProperty(navigator, "languages", {
get: function() {
return ["en-US", "en", "bn"];
}
});
// In your playwright script, assuming the preload.js file is in same folder of our script
// In your playwright script, assuming the preload.js file is in same folder
const preloadFile = fs.readFileSync('./preload.js', 'utf8');
await page.evaluateOnNewDocument(preloadFile);
```
> **NOTE** The order of evaluation of multiple scripts installed via [browserContext.evaluateOnNewDocument(pageFunction[, ...args])](#browsercontextevaluateonnewdocumentpagefunction-args) and [page.evaluateOnNewDocument(pageFunction[, ...args])](#pageevaluateonnewdocumentpagefunction-args) is not defined.
#### page.exposeFunction(name, playwrightFunction)
- `name` <[string]> Name of the function on the window object
- `playwrightFunction` <[function]> Callback function which will be called in Playwright's context.
@ -3591,6 +3610,7 @@ const backgroundPage = await backroundPageTarget.page();
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
- [browserContext.close()](#browsercontextclose)
- [browserContext.cookies([...urls])](#browsercontextcookiesurls)
- [browserContext.evaluateOnNewDocument(pageFunction[, ...args])](#browsercontextevaluateonnewdocumentpagefunction-args)
- [browserContext.newPage()](#browsercontextnewpage)
- [browserContext.pages()](#browsercontextpages)
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)

View File

@ -9,7 +9,7 @@
"main": "index.js",
"playwright": {
"chromium_revision": "744254",
"firefox_revision": "1031",
"firefox_revision": "1032",
"webkit_revision": "1162"
},
"scripts": {

View File

@ -46,6 +46,7 @@ export interface BrowserContext {
clearPermissions(): Promise<void>;
setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
setExtraHTTPHeaders(headers: network.Headers): Promise<void>;
evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]): Promise<void>;
close(): Promise<void>;
_existingPages(): Page[];

View File

@ -187,6 +187,7 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
readonly _browserContextId: string | null;
readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings;
readonly _evaluateOnNewDocumentSources: string[];
private _closed = false;
constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) {
@ -195,6 +196,7 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
this._browserContextId = browserContextId;
this._timeoutSettings = new TimeoutSettings();
this._options = options;
this._evaluateOnNewDocumentSources = [];
}
async _initialize() {
@ -300,6 +302,13 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
await (page._delegate as CRPage).updateExtraHTTPHeaders();
}
async evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]) {
const source = helper.evaluationString(pageFunction, ...args);
this._evaluateOnNewDocumentSources.push(source);
for (const page of this._existingPages())
await (page._delegate as CRPage).evaluateOnNewDocument(source);
}
async close() {
if (this._closed)
return;

View File

@ -33,8 +33,7 @@ import { RawMouseImpl, RawKeyboardImpl } from './crInput';
import { getAccessibilityTree } from './crAccessibility';
import { CRCoverage } from './crCoverage';
import { CRPDF } from './crPdf';
import { CRBrowser } from './crBrowser';
import { BrowserContext } from '../browserContext';
import { CRBrowser, CRBrowserContext } from './crBrowser';
import * as types from '../types';
import { ConsoleMessage } from '../console';
import * as platform from '../platform';
@ -54,14 +53,16 @@ export class CRPage implements PageDelegate {
private _browser: CRBrowser;
private _pdf: CRPDF;
private _coverage: CRCoverage;
private readonly _browserContext: CRBrowserContext;
constructor(client: CRSession, browser: CRBrowser, browserContext: BrowserContext) {
constructor(client: CRSession, browser: CRBrowser, browserContext: CRBrowserContext) {
this._client = client;
this._browser = browser;
this.rawKeyboard = new RawKeyboardImpl(client);
this.rawMouse = new RawMouseImpl(client);
this._pdf = new CRPDF(client);
this._coverage = new CRCoverage(client);
this._browserContext = browserContext;
this._page = new Page(this, browserContext);
this._networkManager = new CRNetworkManager(client, this._page);
@ -119,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 source of this._browserContext._evaluateOnNewDocumentSources)
promises.push(this.evaluateOnNewDocument(source));
await Promise.all(promises);
}

View File

@ -33,7 +33,7 @@ import { headersArray } from './ffNetworkManager';
export class FFBrowser extends platform.EventEmitter implements Browser {
_connection: FFConnection;
_targets: Map<string, Target>;
readonly _defaultContext: BrowserContext;
readonly _defaultContext: FFBrowserContext;
readonly _contexts: Map<string, FFBrowserContext>;
private _eventListeners: RegisteredListener[];
@ -261,6 +261,7 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings;
private _closed = false;
private readonly _evaluateOnNewDocumentSources: string[];
constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) {
super();
@ -268,6 +269,7 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
this._browserContextId = browserContextId;
this._timeoutSettings = new TimeoutSettings();
this._options = options;
this._evaluateOnNewDocumentSources = [];
}
async _initialize() {
@ -357,6 +359,12 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
await this._browser._connection.send('Browser.setExtraHTTPHeaders', { browserContextId: this._browserContextId || undefined, headers: headersArray(this._options.extraHTTPHeaders) });
}
async evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]) {
const source = helper.evaluationString(pageFunction, ...args);
this._evaluateOnNewDocumentSources.push(source);
await this._browser._connection.send('Browser.addScriptToEvaluateOnNewDocument', { browserContextId: this._browserContextId || undefined, script: source });
}
async close() {
if (this._closed)
return;

View File

@ -36,7 +36,7 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
private readonly _connection: WKConnection;
private readonly _attachToDefaultContext: boolean;
readonly _browserSession: WKSession;
readonly _defaultContext: BrowserContext;
readonly _defaultContext: WKBrowserContext;
readonly _contexts = new Map<string, WKBrowserContext>();
readonly _pageProxies = new Map<string, WKPageProxy>();
private readonly _eventListeners: RegisteredListener[];
@ -174,6 +174,7 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
readonly _options: BrowserContextOptions;
readonly _timeoutSettings: TimeoutSettings;
private _closed = false;
readonly _evaluateOnNewDocumentSources: string[];
constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) {
super();
@ -181,6 +182,7 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
this._browserContextId = browserContextId;
this._timeoutSettings = new TimeoutSettings();
this._options = options;
this._evaluateOnNewDocumentSources = [];
}
async _initialize() {
@ -276,6 +278,13 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
await (page._delegate as WKPage).updateExtraHTTPHeaders();
}
async evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]) {
const source = helper.evaluationString(pageFunction, ...args);
this._evaluateOnNewDocumentSources.push(source);
for (const page of this._existingPages())
await (page._delegate as WKPage)._updateBootstrapScript();
}
async close() {
if (this._closed)
return;

View File

@ -27,7 +27,6 @@ import { WKWorkers } from './wkWorkers';
import { Page, PageDelegate } from '../page';
import { Protocol } from './protocol';
import * as dialog from '../dialog';
import { BrowserContext } from '../browserContext';
import { RawMouseImpl, RawKeyboardImpl } from './wkInput';
import * as types from '../types';
import * as accessibility from '../accessibility';
@ -35,6 +34,7 @@ import * as platform from '../platform';
import { getAccessibilityTree } from './wkAccessibility';
import { WKProvisionalPage } from './wkProvisionalPage';
import { WKPageProxy } from './wkPageProxy';
import { WKBrowserContext } from './wkBrowser';
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
@ -53,8 +53,9 @@ export class WKPage implements PageDelegate {
private _mainFrameContextId?: number;
private _sessionListeners: RegisteredListener[] = [];
private readonly _bootstrapScripts: string[] = [];
private readonly _browserContext: WKBrowserContext;
constructor(browserContext: BrowserContext, pageProxySession: WKSession, opener: WKPageProxy | null) {
constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPageProxy | null) {
this._pageProxySession = pageProxySession;
this._opener = opener;
this.rawKeyboard = new RawKeyboardImpl(pageProxySession);
@ -63,6 +64,7 @@ export class WKPage implements PageDelegate {
this._page = new Page(this, browserContext);
this._workers = new WKWorkers(this._page);
this._session = undefined as any as WKSession;
this._browserContext = browserContext;
this._page.on(Events.Page.FrameDetached, frame => this._removeContextsForFrame(frame, false));
}
@ -137,10 +139,7 @@ export class WKPage implements PageDelegate {
promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent }));
if (this._page._state.mediaType || this._page._state.colorScheme)
promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme));
if (this._bootstrapScripts.length) {
const source = this._bootstrapScripts.join(';');
promises.push(session.send('Page.setBootstrapScript', { source }));
}
promises.push(session.send('Page.setBootstrapScript', { source: this._calculateBootstrapScript() }));
if (contextOptions.bypassCSP)
promises.push(session.send('Page.setBypassCSP', { enabled: true }));
promises.push(session.send('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() }));
@ -465,18 +464,21 @@ 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);
await this._setBootstrapScripts();
await this._updateBootstrapScript();
await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(debugError)));
}
async evaluateOnNewDocument(script: string): Promise<void> {
this._bootstrapScripts.push(script);
await this._setBootstrapScripts();
await this._updateBootstrapScript();
}
private async _setBootstrapScripts() {
const source = this._bootstrapScripts.join(';');
await this._updateState('Page.setBootstrapScript', { source });
private _calculateBootstrapScript(): string {
return [...this._browserContext._evaluateOnNewDocumentSources, ...this._bootstrapScripts].join(';');
}
async _updateBootstrapScript(): Promise<void> {
await this._updateState('Page.setBootstrapScript', { source: this._calculateBootstrapScript() });
}
async closePage(runBeforeUnload: boolean): Promise<void> {

View File

@ -14,19 +14,19 @@
* limitations under the License.
*/
import { BrowserContext } from '../browserContext';
import { Page } from '../page';
import { Protocol } from './protocol';
import { WKSession } from './wkConnection';
import { WKPage } from './wkPage';
import { RegisteredListener, helper, assert, debugError } from '../helper';
import { Events } from '../events';
import { WKBrowserContext } from './wkBrowser';
const isPovisionalSymbol = Symbol('isPovisional');
export class WKPageProxy {
private readonly _pageProxySession: WKSession;
readonly _browserContext: BrowserContext;
readonly _browserContext: WKBrowserContext;
private readonly _opener: WKPageProxy | null;
private readonly _pagePromise: Promise<Page | null>;
private _pagePromiseFulfill: (page: Page | null) => void = () => {};
@ -36,7 +36,7 @@ export class WKPageProxy {
private readonly _sessions = new Map<string, WKSession>();
private readonly _eventListeners: RegisteredListener[];
constructor(pageProxySession: WKSession, browserContext: BrowserContext, opener: WKPageProxy | null) {
constructor(pageProxySession: WKSession, browserContext: WKBrowserContext, opener: WKPageProxy | null) {
this._pageProxySession = pageProxySession;
this._browserContext = browserContext;
this._opener = opener;

View File

@ -285,6 +285,24 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
await page.goto(server.PREFIX + '/tamperable.html');
expect(await page.evaluate(() => window.result)).toBe(123);
});
it('should work with browser context scripts', async({browser, server}) => {
const context = await browser.newContext();
await context.evaluateOnNewDocument(() => window.temp = 123);
const page = await context.newPage();
await page.evaluateOnNewDocument(() => window.injected = window.temp);
await page.goto(server.PREFIX + '/tamperable.html');
expect(await page.evaluate(() => window.result)).toBe(123);
await context.close();
});
it('should work with browser context scripts for already created pages', async({browser, server}) => {
const context = await browser.newContext();
const page = await context.newPage();
await context.evaluateOnNewDocument(() => window.temp = 123);
await page.evaluateOnNewDocument(() => window.injected = window.temp);
await page.goto(server.PREFIX + '/tamperable.html');
expect(await page.evaluate(() => window.result)).toBe(123);
await context.close();
});
it('should support multiple scripts', async({page, server}) => {
await page.evaluateOnNewDocument(function(){
window.script1 = 1;

View File

@ -74,6 +74,18 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
await context.close();
expect(size).toEqual({width: 400, height: 500});
});
it.skip(CHROMIUM || WEBKIT)('should apply evaluateOnNewDocument from browser context', async function({browser, server}) {
const context = await browser.newContext();
await context.evaluateOnNewDocument(() => window.injected = 123);
const page = await context.newPage();
await page.goto(server.EMPTY_PAGE);
const injected = await page.evaluate(() => {
const win = window.open('about:blank');
return win.injected;
});
await context.close();
expect(injected).toBe(123);
});
});
describe('Page.Events.Popup', function() {

View File

@ -85,7 +85,6 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO
await watchdog;
});
it('should work when resolved right before execution context disposal', async({page, server}) => {
// FIXME: implement Page.addScriptToEvaluateOnNewDocument in WebKit.
await page.evaluateOnNewDocument(() => window.__RELOADED = true);
await page.waitForFunction(() => {
if (!window.__RELOADED)