diff --git a/docs/api.md b/docs/api.md index 6306f2e961..18aec729ad 100644 --- a/docs/api.md +++ b/docs/api.md @@ -264,11 +264,11 @@ await context.close(); - [event: 'close'](#event-close) +- [browserContext.addInitScript(script[, ...args])](#browsercontextaddinitscriptscript-args) - [browserContext.clearCookies()](#browsercontextclearcookies) - [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) @@ -286,6 +286,32 @@ Emitted when Browser context gets closed. This might happen because of one of th - Browser application is closed or crashed. - The [`browser.close`](#browserclose) method was called. +#### browserContext.addInitScript(script[, ...args]) +- `script` <[function]|[string]|[Object]> Script to be evaluated in all pages in the browser context. + - `path` <[string]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). + - `content` <[string]> Raw script content. +- `...args` <...[Serializable]> Arguments to pass to `script` (only supported when passing a function). +- returns: <[Promise]> + +Adds a script which would be evaluated 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 script is evaluated in the context of the newly attached frame. + +The script is evaluated 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.addInitScript(preloadFile); +``` + +> **NOTE** The order of evaluation of multiple scripts installed via [browserContext.addInitScript(script[, ...args])](#browsercontextaddinitscriptscript-args) and [page.addInitScript(script[, ...args])](#pageaddinitscriptscript-args) is not defined. + #### browserContext.clearCookies() - returns: <[Promise]> @@ -328,30 +354,6 @@ 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.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]>> @@ -511,6 +513,7 @@ page.removeListener('request', logRequest); - [page.$eval(selector, pageFunction[, ...args])](#pageevalselector-pagefunction-args-1) - [page.$wait(selector[, options])](#pagewaitselector-options) - [page.accessibility](#pageaccessibility) +- [page.addInitScript(script[, ...args])](#pageaddinitscriptscript-args) - [page.addScriptTag(options)](#pageaddscripttagoptions) - [page.addStyleTag(options)](#pageaddstyletagoptions) - [page.authenticate(credentials)](#pageauthenticatecredentials) @@ -524,7 +527,6 @@ page.removeListener('request', logRequest); - [page.emulateMedia(options)](#pageemulatemediaoptions) - [page.evaluate(pageFunction[, ...args])](#pageevaluatepagefunction-args) - [page.evaluateHandle(pageFunction[, ...args])](#pageevaluatehandlepagefunction-args) -- [page.evaluateOnNewDocument(pageFunction[, ...args])](#pageevaluateonnewdocumentpagefunction-args) - [page.exposeFunction(name, playwrightFunction)](#pageexposefunctionname-playwrightfunction) - [page.fill(selector, value, options)](#pagefillselector-value-options) - [page.focus(selector, options)](#pagefocusselector-options) @@ -752,6 +754,32 @@ This is a shortcut to [page.waitForSelector(selector[, options])](#pagewaitforse #### page.accessibility - returns: <[Accessibility]> +#### page.addInitScript(script[, ...args]) +- `script` <[function]|[string]|[Object]> Script to be evaluated in the page. + - `path` <[string]> Path to the JavaScript file. If `path` is a relative path, then it is resolved relative to [current working directory](https://nodejs.org/api/process.html#process_process_cwd). + - `content` <[string]> Raw script content. +- `...args` <...[Serializable]> Arguments to pass to `script` (only supported when passing a function). +- returns: <[Promise]> + +Adds a script which would be evaluated in one of the following scenarios: +- Whenever the page is navigated. +- Whenever the child frame is attached or navigated. In this case, the scritp is evaluated in the context of the newly attached frame. + +The script is evaluated 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 page.addInitScript(preloadFile); +``` + +> **NOTE** The order of evaluation of multiple scripts installed via [browserContext.addInitScript(script[, ...args])](#browsercontextaddinitscriptscript-args) and [page.addInitScript(script[, ...args])](#pageaddinitscriptscript-args) is not defined. + #### page.addScriptTag(options) - `options` <[Object]> - `url` <[string]> URL of a script to be added. @@ -966,30 +994,6 @@ console.log(await resultHandle.jsonValue()); await resultHandle.dispose(); ``` -#### page.evaluateOnNewDocument(pageFunction[, ...args]) -- `pageFunction` <[function]|[string]> Function to be evaluated in the page -- `...args` <...[Serializable]> Arguments to pass to `pageFunction` -- returns: <[Promise]> - -Adds a function which would be invoked in one of the following scenarios: -- whenever the page is navigated -- whenever the child frame is attached or navigated. 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 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. @@ -3604,11 +3608,11 @@ const backgroundPage = await backroundPageTarget.page(); - [event: 'close'](#event-close) +- [browserContext.addInitScript(script[, ...args])](#browsercontextaddinitscriptscript-args) - [browserContext.clearCookies()](#browsercontextclearcookies) - [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/src/browserContext.ts b/src/browserContext.ts index 47e3bf9863..85c51dc90f 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -46,7 +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; + addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]): Promise; close(): Promise; _existingPages(): Page[]; diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index 1c564d44e7..93ed23ab5b 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -302,8 +302,8 @@ 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); + async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) { + const source = await helper.evaluationScript(script, ...args); this._evaluateOnNewDocumentSources.push(source); for (const page of this._existingPages()) await (page._delegate as CRPage).evaluateOnNewDocument(source); diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index b04ccdc001..15e8c92b17 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -359,8 +359,8 @@ 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); + async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) { + const source = await helper.evaluationScript(script, ...args); this._evaluateOnNewDocumentSources.push(source); await this._browser._connection.send('Browser.addScriptToEvaluateOnNewDocument', { browserContextId: this._browserContextId || undefined, script: source }); } diff --git a/src/helper.ts b/src/helper.ts index 712089d78a..fb6746ca85 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -41,6 +41,21 @@ class Helper { } } + static async evaluationScript(fun: Function | string | { path?: string, content?: string }, ...args: any[]): Promise { + if (!helper.isString(fun) && typeof fun !== 'function') { + if (fun.content !== undefined) { + fun = fun.content; + } else if (fun.path !== undefined) { + let contents = await platform.readFileAsync(fun.path, 'utf8'); + contents += '//# sourceURL=' + fun.path.replace(/\n/g, ''); + fun = contents; + } else { + throw new Error('Either path or content property must be present'); + } + } + return helper.evaluationString(fun, ...args); + } + static installApiHooks(className: string, classType: any) { const log = platform.debug('pw:api'); for (const methodName of Reflect.ownKeys(classType.prototype)) { diff --git a/src/page.ts b/src/page.ts index bb20a38276..57617ebbfb 100644 --- a/src/page.ts +++ b/src/page.ts @@ -411,9 +411,8 @@ export class Page extends platform.EventEmitter { return this.mainFrame().evaluate(pageFunction, ...args as any); } - async evaluateOnNewDocument(pageFunction: Function | string, ...args: any[]) { - const source = helper.evaluationString(pageFunction, ...args); - await this._delegate.evaluateOnNewDocument(source); + async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) { + await this._delegate.evaluateOnNewDocument(await helper.evaluationScript(script, ...args)); } async setCacheEnabled(enabled: boolean = true) { diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 5f567a352f..47c8c2ec8e 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -278,8 +278,8 @@ 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); + async addInitScript(script: Function | string | { path?: string, content?: string }, ...args: any[]) { + const source = await helper.evaluationScript(script, ...args); this._evaluateOnNewDocumentSources.push(source); for (const page of this._existingPages()) await (page._delegate as WKPage)._updateBootstrapScript(); diff --git a/test/assets/injectedfile.js b/test/assets/injectedfile.js index 6cb04f1bba..784d1776bc 100644 --- a/test/assets/injectedfile.js +++ b/test/assets/injectedfile.js @@ -1,2 +1,3 @@ window.__injected = 42; +window.injected = 123; window.__injectedError = new Error('hi'); \ No newline at end of file diff --git a/test/evaluation.spec.js b/test/evaluation.spec.js index 991e5a82da..22766d14cc 100644 --- a/test/evaluation.spec.js +++ b/test/evaluation.spec.js @@ -16,7 +16,7 @@ */ const utils = require('./utils'); - +const path = require('path'); const bigint = typeof BigInt !== 'undefined'; /** @@ -277,19 +277,41 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) }) }); - describe('Page.evaluateOnNewDocument', function() { + describe('Page.addInitScript', function() { it('should evaluate before anything else on the page', async({page, server}) => { - await page.evaluateOnNewDocument(function(){ + await page.addInitScript(function(){ window.injected = 123; }); await page.goto(server.PREFIX + '/tamperable.html'); expect(await page.evaluate(() => window.result)).toBe(123); }); + it('should work with a path', async({page, server}) => { + await page.addInitScript({ path: path.join(__dirname, 'assets/injectedfile.js') }); + await page.goto(server.PREFIX + '/tamperable.html'); + expect(await page.evaluate(() => window.result)).toBe(123); + }); + it('should work with content', async({page, server}) => { + await page.addInitScript({ content: 'window.injected = 123' }); + await page.goto(server.PREFIX + '/tamperable.html'); + expect(await page.evaluate(() => window.result)).toBe(123); + }); + it('should throw without path and content', async({page, server}) => { + const error = await page.addInitScript({ foo: 'bar' }).catch(e => e); + expect(error.message).toBe('Either path or content property must be present'); + }); it('should work with browser context scripts', async({browser, server}) => { const context = await browser.newContext(); - await context.evaluateOnNewDocument(() => window.temp = 123); + await context.addInitScript(() => window.temp = 123); + const page = await context.newPage(); + await page.addInitScript(() => 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 with a path', async({browser, server}) => { + const context = await browser.newContext(); + await context.addInitScript({ path: path.join(__dirname, 'assets/injectedfile.js') }); 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(); @@ -297,17 +319,17 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) 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 context.addInitScript(() => window.temp = 123); + await page.addInitScript(() => 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(){ + await page.addInitScript(function(){ window.script1 = 1; }); - await page.evaluateOnNewDocument(function(){ + await page.addInitScript(function(){ window.script2 = 2; }); await page.goto(server.PREFIX + '/tamperable.html'); @@ -316,7 +338,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) }); it('should work with CSP', async({page, server}) => { server.setCSP('/empty.html', 'script-src ' + server.PREFIX); - await page.evaluateOnNewDocument(function(){ + await page.addInitScript(function(){ window.injected = 123; }); await page.goto(server.PREFIX + '/empty.html'); @@ -328,7 +350,7 @@ module.exports.describe = function({testRunner, expect, FFOX, CHROMIUM, WEBKIT}) }); it('should work after a cross origin navigation', async({page, server}) => { await page.goto(server.CROSS_PROCESS_PREFIX); - await page.evaluateOnNewDocument(function(){ + await page.addInitScript(function(){ window.injected = 123; }); await page.goto(server.PREFIX + '/tamperable.html'); diff --git a/test/page.spec.js b/test/page.spec.js index 5036a9061a..1e4bce6058 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -412,12 +412,12 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF }); expect(thrown).toBe(null); }); - it('should be callable from-inside evaluateOnNewDocument', async({page, server}) => { + it('should be callable from-inside addInitScript', async({page, server}) => { let called = false; await page.exposeFunction('woof', function() { called = true; }); - await page.evaluateOnNewDocument(() => woof()); + await page.addInitScript(() => woof()); await page.reload(); expect(called).toBe(true); }); diff --git a/test/popup.spec.js b/test/popup.spec.js index 3cea767e24..cf58b6fec7 100644 --- a/test/popup.spec.js +++ b/test/popup.spec.js @@ -74,9 +74,9 @@ 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}) { + it.skip(CHROMIUM)('should apply addInitScript from browser context', async function({browser, server}) { const context = await browser.newContext(); - await context.evaluateOnNewDocument(() => window.injected = 123); + await context.addInitScript(() => window.injected = 123); const page = await context.newPage(); await page.goto(server.EMPTY_PAGE); const injected = await page.evaluate(() => { diff --git a/test/waittask.spec.js b/test/waittask.spec.js index 582c2709f1..6a6589303f 100644 --- a/test/waittask.spec.js +++ b/test/waittask.spec.js @@ -85,7 +85,7 @@ module.exports.describe = function({testRunner, expect, product, playwright, FFO await watchdog; }); it('should work when resolved right before execution context disposal', async({page, server}) => { - await page.evaluateOnNewDocument(() => window.__RELOADED = true); + await page.addInitScript(() => window.__RELOADED = true); await page.waitForFunction(() => { if (!window.__RELOADED) window.location.reload();