diff --git a/docs/api.md b/docs/api.md index 150c142e2d4c0..da90ed69e7df9 100644 --- a/docs/api.md +++ b/docs/api.md @@ -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]>> @@ -950,7 +973,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]> @@ -960,23 +983,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. @@ -3588,6 +3607,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) diff --git a/package.json b/package.json index bb81d38d8a229..288a7b75abea4 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "main": "index.js", "playwright": { "chromium_revision": "744254", - "firefox_revision": "1031", + "firefox_revision": "1032", "webkit_revision": "1162" }, "scripts": { diff --git a/src/browserContext.ts b/src/browserContext.ts index e7a735eaeec21..47e3bf98634c2 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -46,6 +46,7 @@ export interface BrowserContext { clearPermissions(): Promise; setGeolocation(geolocation: types.Geolocation | null): Promise; setExtraHTTPHeaders(headers: network.Headers): Promise; + evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]): Promise; close(): Promise; _existingPages(): Page[]; diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 13d6ad14c4193..1c564d44e7475 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -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; diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index 8d0aaddf03925..ed0a8633165f6 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -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); } diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 1e21c139196ab..b04ccdc0014cb 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -33,7 +33,7 @@ import { headersArray } from './ffNetworkManager'; export class FFBrowser extends platform.EventEmitter implements Browser { _connection: FFConnection; _targets: Map; - readonly _defaultContext: BrowserContext; + readonly _defaultContext: FFBrowserContext; readonly _contexts: Map; 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; diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index dd0f35dd08392..5f567a352fbb7 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -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(); readonly _pageProxies = new Map(); 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; diff --git a/src/webkit/wkPage.ts b/src/webkit/wkPage.ts index bd43eebcdb3e8..ee0b7f1f3a998 100644 --- a/src/webkit/wkPage.ts +++ b/src/webkit/wkPage.ts @@ -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 { 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 { this._bootstrapScripts.push(script); - await this._setBootstrapScripts(); + await this._updateBootstrapScript(); + } + + private _calculateBootstrapScript(): string { + return [...this._browserContext._evaluateOnNewDocumentSources, ...this._bootstrapScripts].join(';'); } - private async _setBootstrapScripts() { - const source = this._bootstrapScripts.join(';'); - await this._updateState('Page.setBootstrapScript', { source }); + async _updateBootstrapScript(): Promise { + await this._updateState('Page.setBootstrapScript', { source: this._calculateBootstrapScript() }); } async closePage(runBeforeUnload: boolean): Promise { diff --git a/src/webkit/wkPageProxy.ts b/src/webkit/wkPageProxy.ts index ec7e718a06044..0956f2584a2f6 100644 --- a/src/webkit/wkPageProxy.ts +++ b/src/webkit/wkPageProxy.ts @@ -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; private _pagePromiseFulfill: (page: Page | null) => void = () => {}; @@ -36,7 +36,7 @@ export class WKPageProxy { private readonly _sessions = new Map(); 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; diff --git a/test/evaluation.spec.js b/test/evaluation.spec.js index d50d092584b8f..991e5a82da386 100644 --- a/test/evaluation.spec.js +++ b/test/evaluation.spec.js @@ -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; diff --git a/test/popup.spec.js b/test/popup.spec.js index 7563c4606bf0a..3cea767e2471b 100644 --- a/test/popup.spec.js +++ b/test/popup.spec.js @@ -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() { diff --git a/test/waittask.spec.js b/test/waittask.spec.js index 68e6fb0f2f303..582c2709f11fc 100644 --- a/test/waittask.spec.js +++ b/test/waittask.spec.js @@ -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)