Skip to content

Commit 7682865

Browse files
authored
feat(popups): add BrowserContext.evaluateOnNewDocument (#1136)
1 parent dc161df commit 7682865

12 files changed

+114
-33
lines changed

docs/api.md

+32-12
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ await context.close();
268268
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
269269
- [browserContext.close()](#browsercontextclose)
270270
- [browserContext.cookies([...urls])](#browsercontextcookiesurls)
271+
- [browserContext.evaluateOnNewDocument(pageFunction[, ...args])](#browsercontextevaluateonnewdocumentpagefunction-args)
271272
- [browserContext.newPage()](#browsercontextnewpage)
272273
- [browserContext.pages()](#browsercontextpages)
273274
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)
@@ -308,7 +309,7 @@ context.clearPermissions();
308309
Closes the browser context. All the targets that belong to the browser context
309310
will be closed.
310311

311-
> **NOTE** only incognito browser contexts can be closed.
312+
> **NOTE** the default browser context cannot be closed.
312313
313314
#### browserContext.cookies([...urls])
314315
- `...urls` <...[string]>
@@ -327,7 +328,29 @@ will be closed.
327328
If no URLs are specified, this method returns all cookies.
328329
If URLs are specified, only cookies that affect those URLs are returned.
329330

330-
> **NOTE** the default browser context cannot be closed.
331+
#### browserContext.evaluateOnNewDocument(pageFunction[, ...args])
332+
- `pageFunction` <[function]|[string]> Function to be evaluated in all pages in the browser context
333+
- `...args` <...[Serializable]> Arguments to pass to `pageFunction`
334+
- returns: <[Promise]>
335+
336+
Adds a function which would be invoked in one of the following scenarios:
337+
- Whenever a page is created in the browser context or is navigated.
338+
- 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.
339+
340+
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`.
341+
342+
An example of overriding `Math.random` before the page loads:
343+
344+
```js
345+
// preload.js
346+
Math.random = () => 42;
347+
348+
// In your playwright script, assuming the preload.js file is in same folder
349+
const preloadFile = fs.readFileSync('./preload.js', 'utf8');
350+
await browserContext.evaluateOnNewDocument(preloadFile);
351+
```
352+
353+
> **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.
331354
332355
#### browserContext.newPage()
333356
- returns: <[Promise]<[Page]>>
@@ -944,7 +967,7 @@ await resultHandle.dispose();
944967
```
945968

946969
#### page.evaluateOnNewDocument(pageFunction[, ...args])
947-
- `pageFunction` <[function]|[string]> Function to be evaluated in browser context
970+
- `pageFunction` <[function]|[string]> Function to be evaluated in the page
948971
- `...args` <...[Serializable]> Arguments to pass to `pageFunction`
949972
- returns: <[Promise]>
950973

@@ -954,23 +977,19 @@ Adds a function which would be invoked in one of the following scenarios:
954977

955978
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`.
956979

957-
An example of overriding the navigator.languages property before the page loads:
980+
An example of overriding `Math.random` before the page loads:
958981

959982
```js
960983
// preload.js
984+
Math.random = () => 42;
961985

962-
// overwrite the `languages` property to use a custom getter
963-
Object.defineProperty(navigator, "languages", {
964-
get: function() {
965-
return ["en-US", "en", "bn"];
966-
}
967-
});
968-
969-
// In your playwright script, assuming the preload.js file is in same folder of our script
986+
// In your playwright script, assuming the preload.js file is in same folder
970987
const preloadFile = fs.readFileSync('./preload.js', 'utf8');
971988
await page.evaluateOnNewDocument(preloadFile);
972989
```
973990

991+
> **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.
992+
974993
#### page.exposeFunction(name, playwrightFunction)
975994
- `name` <[string]> Name of the function on the window object
976995
- `playwrightFunction` <[function]> Callback function which will be called in Playwright's context.
@@ -3591,6 +3610,7 @@ const backgroundPage = await backroundPageTarget.page();
35913610
- [browserContext.clearPermissions()](#browsercontextclearpermissions)
35923611
- [browserContext.close()](#browsercontextclose)
35933612
- [browserContext.cookies([...urls])](#browsercontextcookiesurls)
3613+
- [browserContext.evaluateOnNewDocument(pageFunction[, ...args])](#browsercontextevaluateonnewdocumentpagefunction-args)
35943614
- [browserContext.newPage()](#browsercontextnewpage)
35953615
- [browserContext.pages()](#browsercontextpages)
35963616
- [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies)

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"main": "index.js",
1010
"playwright": {
1111
"chromium_revision": "744254",
12-
"firefox_revision": "1031",
12+
"firefox_revision": "1032",
1313
"webkit_revision": "1162"
1414
},
1515
"scripts": {

src/browserContext.ts

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface BrowserContext {
4646
clearPermissions(): Promise<void>;
4747
setGeolocation(geolocation: types.Geolocation | null): Promise<void>;
4848
setExtraHTTPHeaders(headers: network.Headers): Promise<void>;
49+
evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]): Promise<void>;
4950
close(): Promise<void>;
5051

5152
_existingPages(): Page[];

src/chromium/crBrowser.ts

+9
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
187187
readonly _browserContextId: string | null;
188188
readonly _options: BrowserContextOptions;
189189
readonly _timeoutSettings: TimeoutSettings;
190+
readonly _evaluateOnNewDocumentSources: string[];
190191
private _closed = false;
191192

192193
constructor(browser: CRBrowser, browserContextId: string | null, options: BrowserContextOptions) {
@@ -195,6 +196,7 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
195196
this._browserContextId = browserContextId;
196197
this._timeoutSettings = new TimeoutSettings();
197198
this._options = options;
199+
this._evaluateOnNewDocumentSources = [];
198200
}
199201

200202
async _initialize() {
@@ -300,6 +302,13 @@ export class CRBrowserContext extends platform.EventEmitter implements BrowserCo
300302
await (page._delegate as CRPage).updateExtraHTTPHeaders();
301303
}
302304

305+
async evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]) {
306+
const source = helper.evaluationString(pageFunction, ...args);
307+
this._evaluateOnNewDocumentSources.push(source);
308+
for (const page of this._existingPages())
309+
await (page._delegate as CRPage).evaluateOnNewDocument(source);
310+
}
311+
303312
async close() {
304313
if (this._closed)
305314
return;

src/chromium/crPage.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ import { RawMouseImpl, RawKeyboardImpl } from './crInput';
3333
import { getAccessibilityTree } from './crAccessibility';
3434
import { CRCoverage } from './crCoverage';
3535
import { CRPDF } from './crPdf';
36-
import { CRBrowser } from './crBrowser';
37-
import { BrowserContext } from '../browserContext';
36+
import { CRBrowser, CRBrowserContext } from './crBrowser';
3837
import * as types from '../types';
3938
import { ConsoleMessage } from '../console';
4039
import * as platform from '../platform';
@@ -54,14 +53,16 @@ export class CRPage implements PageDelegate {
5453
private _browser: CRBrowser;
5554
private _pdf: CRPDF;
5655
private _coverage: CRCoverage;
56+
private readonly _browserContext: CRBrowserContext;
5757

58-
constructor(client: CRSession, browser: CRBrowser, browserContext: BrowserContext) {
58+
constructor(client: CRSession, browser: CRBrowser, browserContext: CRBrowserContext) {
5959
this._client = client;
6060
this._browser = browser;
6161
this.rawKeyboard = new RawKeyboardImpl(client);
6262
this.rawMouse = new RawMouseImpl(client);
6363
this._pdf = new CRPDF(client);
6464
this._coverage = new CRCoverage(client);
65+
this._browserContext = browserContext;
6566
this._page = new Page(this, browserContext);
6667
this._networkManager = new CRNetworkManager(client, this._page);
6768

@@ -119,6 +120,8 @@ export class CRPage implements PageDelegate {
119120
if (options.geolocation)
120121
promises.push(this._client.send('Emulation.setGeolocationOverride', options.geolocation));
121122
promises.push(this.updateExtraHTTPHeaders());
123+
for (const source of this._browserContext._evaluateOnNewDocumentSources)
124+
promises.push(this.evaluateOnNewDocument(source));
122125
await Promise.all(promises);
123126
}
124127

src/firefox/ffBrowser.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { headersArray } from './ffNetworkManager';
3333
export class FFBrowser extends platform.EventEmitter implements Browser {
3434
_connection: FFConnection;
3535
_targets: Map<string, Target>;
36-
readonly _defaultContext: BrowserContext;
36+
readonly _defaultContext: FFBrowserContext;
3737
readonly _contexts: Map<string, FFBrowserContext>;
3838
private _eventListeners: RegisteredListener[];
3939

@@ -261,13 +261,15 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
261261
readonly _options: BrowserContextOptions;
262262
readonly _timeoutSettings: TimeoutSettings;
263263
private _closed = false;
264+
private readonly _evaluateOnNewDocumentSources: string[];
264265

265266
constructor(browser: FFBrowser, browserContextId: string | null, options: BrowserContextOptions) {
266267
super();
267268
this._browser = browser;
268269
this._browserContextId = browserContextId;
269270
this._timeoutSettings = new TimeoutSettings();
270271
this._options = options;
272+
this._evaluateOnNewDocumentSources = [];
271273
}
272274

273275
async _initialize() {
@@ -357,6 +359,12 @@ export class FFBrowserContext extends platform.EventEmitter implements BrowserCo
357359
await this._browser._connection.send('Browser.setExtraHTTPHeaders', { browserContextId: this._browserContextId || undefined, headers: headersArray(this._options.extraHTTPHeaders) });
358360
}
359361

362+
async evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]) {
363+
const source = helper.evaluationString(pageFunction, ...args);
364+
this._evaluateOnNewDocumentSources.push(source);
365+
await this._browser._connection.send('Browser.addScriptToEvaluateOnNewDocument', { browserContextId: this._browserContextId || undefined, script: source });
366+
}
367+
360368
async close() {
361369
if (this._closed)
362370
return;

src/webkit/wkBrowser.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export class WKBrowser extends platform.EventEmitter implements Browser {
3636
private readonly _connection: WKConnection;
3737
private readonly _attachToDefaultContext: boolean;
3838
readonly _browserSession: WKSession;
39-
readonly _defaultContext: BrowserContext;
39+
readonly _defaultContext: WKBrowserContext;
4040
readonly _contexts = new Map<string, WKBrowserContext>();
4141
readonly _pageProxies = new Map<string, WKPageProxy>();
4242
private readonly _eventListeners: RegisteredListener[];
@@ -174,13 +174,15 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
174174
readonly _options: BrowserContextOptions;
175175
readonly _timeoutSettings: TimeoutSettings;
176176
private _closed = false;
177+
readonly _evaluateOnNewDocumentSources: string[];
177178

178179
constructor(browser: WKBrowser, browserContextId: string | undefined, options: BrowserContextOptions) {
179180
super();
180181
this._browser = browser;
181182
this._browserContextId = browserContextId;
182183
this._timeoutSettings = new TimeoutSettings();
183184
this._options = options;
185+
this._evaluateOnNewDocumentSources = [];
184186
}
185187

186188
async _initialize() {
@@ -276,6 +278,13 @@ export class WKBrowserContext extends platform.EventEmitter implements BrowserCo
276278
await (page._delegate as WKPage).updateExtraHTTPHeaders();
277279
}
278280

281+
async evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]) {
282+
const source = helper.evaluationString(pageFunction, ...args);
283+
this._evaluateOnNewDocumentSources.push(source);
284+
for (const page of this._existingPages())
285+
await (page._delegate as WKPage)._updateBootstrapScript();
286+
}
287+
279288
async close() {
280289
if (this._closed)
281290
return;

src/webkit/wkPage.ts

+13-11
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ import { WKWorkers } from './wkWorkers';
2727
import { Page, PageDelegate } from '../page';
2828
import { Protocol } from './protocol';
2929
import * as dialog from '../dialog';
30-
import { BrowserContext } from '../browserContext';
3130
import { RawMouseImpl, RawKeyboardImpl } from './wkInput';
3231
import * as types from '../types';
3332
import * as accessibility from '../accessibility';
3433
import * as platform from '../platform';
3534
import { getAccessibilityTree } from './wkAccessibility';
3635
import { WKProvisionalPage } from './wkProvisionalPage';
3736
import { WKPageProxy } from './wkPageProxy';
37+
import { WKBrowserContext } from './wkBrowser';
3838

3939
const UTILITY_WORLD_NAME = '__playwright_utility_world__';
4040
const BINDING_CALL_MESSAGE = '__playwright_binding_call__';
@@ -53,8 +53,9 @@ export class WKPage implements PageDelegate {
5353
private _mainFrameContextId?: number;
5454
private _sessionListeners: RegisteredListener[] = [];
5555
private readonly _bootstrapScripts: string[] = [];
56+
private readonly _browserContext: WKBrowserContext;
5657

57-
constructor(browserContext: BrowserContext, pageProxySession: WKSession, opener: WKPageProxy | null) {
58+
constructor(browserContext: WKBrowserContext, pageProxySession: WKSession, opener: WKPageProxy | null) {
5859
this._pageProxySession = pageProxySession;
5960
this._opener = opener;
6061
this.rawKeyboard = new RawKeyboardImpl(pageProxySession);
@@ -63,6 +64,7 @@ export class WKPage implements PageDelegate {
6364
this._page = new Page(this, browserContext);
6465
this._workers = new WKWorkers(this._page);
6566
this._session = undefined as any as WKSession;
67+
this._browserContext = browserContext;
6668
this._page.on(Events.Page.FrameDetached, frame => this._removeContextsForFrame(frame, false));
6769
}
6870

@@ -137,10 +139,7 @@ export class WKPage implements PageDelegate {
137139
promises.push(session.send('Page.overrideUserAgent', { value: contextOptions.userAgent }));
138140
if (this._page._state.mediaType || this._page._state.colorScheme)
139141
promises.push(WKPage._setEmulateMedia(session, this._page._state.mediaType, this._page._state.colorScheme));
140-
if (this._bootstrapScripts.length) {
141-
const source = this._bootstrapScripts.join(';');
142-
promises.push(session.send('Page.setBootstrapScript', { source }));
143-
}
142+
promises.push(session.send('Page.setBootstrapScript', { source: this._calculateBootstrapScript() }));
144143
if (contextOptions.bypassCSP)
145144
promises.push(session.send('Page.setBypassCSP', { enabled: true }));
146145
promises.push(session.send('Network.setExtraHTTPHeaders', { headers: this._calculateExtraHTTPHeaders() }));
@@ -465,18 +464,21 @@ export class WKPage implements PageDelegate {
465464
async exposeBinding(name: string, bindingFunction: string): Promise<void> {
466465
const script = `self.${name} = (param) => console.debug('${BINDING_CALL_MESSAGE}', {}, param); ${bindingFunction}`;
467466
this._bootstrapScripts.unshift(script);
468-
await this._setBootstrapScripts();
467+
await this._updateBootstrapScript();
469468
await Promise.all(this._page.frames().map(frame => frame.evaluate(script).catch(debugError)));
470469
}
471470

472471
async evaluateOnNewDocument(script: string): Promise<void> {
473472
this._bootstrapScripts.push(script);
474-
await this._setBootstrapScripts();
473+
await this._updateBootstrapScript();
474+
}
475+
476+
private _calculateBootstrapScript(): string {
477+
return [...this._browserContext._evaluateOnNewDocumentSources, ...this._bootstrapScripts].join(';');
475478
}
476479

477-
private async _setBootstrapScripts() {
478-
const source = this._bootstrapScripts.join(';');
479-
await this._updateState('Page.setBootstrapScript', { source });
480+
async _updateBootstrapScript(): Promise<void> {
481+
await this._updateState('Page.setBootstrapScript', { source: this._calculateBootstrapScript() });
480482
}
481483

482484
async closePage(runBeforeUnload: boolean): Promise<void> {

src/webkit/wkPageProxy.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,19 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { BrowserContext } from '../browserContext';
1817
import { Page } from '../page';
1918
import { Protocol } from './protocol';
2019
import { WKSession } from './wkConnection';
2120
import { WKPage } from './wkPage';
2221
import { RegisteredListener, helper, assert, debugError } from '../helper';
2322
import { Events } from '../events';
23+
import { WKBrowserContext } from './wkBrowser';
2424

2525
const isPovisionalSymbol = Symbol('isPovisional');
2626

2727
export class WKPageProxy {
2828
private readonly _pageProxySession: WKSession;
29-
readonly _browserContext: BrowserContext;
29+
readonly _browserContext: WKBrowserContext;
3030
private readonly _opener: WKPageProxy | null;
3131
private readonly _pagePromise: Promise<Page | null>;
3232
private _pagePromiseFulfill: (page: Page | null) => void = () => {};
@@ -36,7 +36,7 @@ export class WKPageProxy {
3636
private readonly _sessions = new Map<string, WKSession>();
3737
private readonly _eventListeners: RegisteredListener[];
3838

39-
constructor(pageProxySession: WKSession, browserContext: BrowserContext, opener: WKPageProxy | null) {
39+
constructor(pageProxySession: WKSession, browserContext: WKBrowserContext, opener: WKPageProxy | null) {
4040
this._pageProxySession = pageProxySession;
4141
this._browserContext = browserContext;
4242
this._opener = opener;

test/evaluation.spec.js

+18
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,24 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT})
285285
await page.goto(server.PREFIX + '/tamperable.html');
286286
expect(await page.evaluate(() => window.result)).toBe(123);
287287
});
288+
it('should work with browser context scripts', async({browser, server}) => {
289+
const context = await browser.newContext();
290+
await context.evaluateOnNewDocument(() => window.temp = 123);
291+
const page = await context.newPage();
292+
await page.evaluateOnNewDocument(() => window.injected = window.temp);
293+
await page.goto(server.PREFIX + '/tamperable.html');
294+
expect(await page.evaluate(() => window.result)).toBe(123);
295+
await context.close();
296+
});
297+
it('should work with browser context scripts for already created pages', async({browser, server}) => {
298+
const context = await browser.newContext();
299+
const page = await context.newPage();
300+
await context.evaluateOnNewDocument(() => window.temp = 123);
301+
await page.evaluateOnNewDocument(() => window.injected = window.temp);
302+
await page.goto(server.PREFIX + '/tamperable.html');
303+
expect(await page.evaluate(() => window.result)).toBe(123);
304+
await context.close();
305+
});
288306
it('should support multiple scripts', async({page, server}) => {
289307
await page.evaluateOnNewDocument(function(){
290308
window.script1 = 1;

0 commit comments

Comments
 (0)