diff --git a/docs/api.md b/docs/api.md index 46b1f967..24ae5f2f 100644 --- a/docs/api.md +++ b/docs/api.md @@ -2939,8 +2939,11 @@ _To output coverage in a form consumable by [Istanbul](https://github.com/istanb #### coverage.startJSCoverage(options) - `options` <[Object]> Set of configurable options for coverage - `resetOnNavigation` <[boolean]> Whether to reset coverage on every navigation. Defaults to `true`. + - `reportAnonymousScripts` <[boolean]> Whether anonymous scripts generated by the page should be reported. Defaults to `false`. - returns: <[Promise]> Promise that resolves when coverage is started +> **NOTE** Anonymous scripts are ones that don't have an associated url. These are scripts that are dynamically created on the page using `eval` or `new Function`. If `reportAnonymousScripts` is set to `true`, anonymous scripts will have `__puppeteer_evaluation_script__` as their URL. + #### coverage.stopCSSCoverage() - returns: <[Promise]<[Array]<[Object]>>> Promise that resolves to the array of coverage reports for all stylesheets - `url` <[string]> StyleSheet URL @@ -2952,14 +2955,14 @@ _To output coverage in a form consumable by [Istanbul](https://github.com/istanb > **NOTE** CSS Coverage doesn't include dynamically injected style tags without sourceURLs. #### coverage.stopJSCoverage() -- returns: <[Promise]<[Array]<[Object]>>> Promise that resolves to the array of coverage reports for all non-anonymous scripts +- returns: <[Promise]<[Array]<[Object]>>> Promise that resolves to the array of coverage reports for all scripts - `url` <[string]> Script URL - `text` <[string]> Script content - `ranges` <[Array]<[Object]>> Script ranges that were executed. Ranges are sorted and non-overlapping. - `start` <[number]> A start offset in text, inclusive - `end` <[number]> An end offset in text, exclusive -> **NOTE** JavaScript Coverage doesn't include anonymous scripts. However, scripts with sourceURLs are +> **NOTE** JavaScript Coverage doesn't include anonymous scripts by default. However, scripts with sourceURLs are reported. [Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array "Array" diff --git a/lib/Coverage.js b/lib/Coverage.js index 288e0e51..756e8c53 100644 --- a/lib/Coverage.js +++ b/lib/Coverage.js @@ -16,6 +16,8 @@ const {helper, debugError, assert} = require('./helper'); +const {EVALUATION_SCRIPT_URL} = require('./ExecutionContext'); + /** * @typedef {Object} CoverageEntry * @property {string} url @@ -83,6 +85,7 @@ class JSCoverage { async start(options = {}) { assert(!this._enabled, 'JSCoverage is already enabled'); this._resetOnNavigation = options.resetOnNavigation === undefined ? true : !!options.resetOnNavigation; + this._reportAnonymousScripts = !!options.reportAnonymousScripts; this._enabled = true; this._scriptURLs.clear(); this._scriptSources.clear(); @@ -109,8 +112,11 @@ class JSCoverage { * @param {!Protocol.Debugger.scriptParsedPayload} event */ async _onScriptParsed(event) { - // Ignore anonymous scripts - if (!event.url) + // Ignore puppeteer-injected scripts + if (event.url === EVALUATION_SCRIPT_URL) + return; + // Ignore other anonymous scripts unless the reportAnonymousScripts option is true. + if (!event.url && !this._reportAnonymousScripts) return; try { const response = await this._client.send('Debugger.getScriptSource', {scriptId: event.scriptId}); diff --git a/lib/ExecutionContext.js b/lib/ExecutionContext.js index e1867bd1..72b765c8 100644 --- a/lib/ExecutionContext.js +++ b/lib/ExecutionContext.js @@ -16,6 +16,9 @@ const {helper, assert} = require('./helper'); +const EVALUATION_SCRIPT_URL = '__puppeteer_evaluation_script__'; +const SOURCE_URL_REGEX = /^[\040\t]*\/\/[@#] sourceURL=\s*(\S*?)\s*$/m; + class ExecutionContext { /** * @param {!Puppeteer.CDPSession} client @@ -61,11 +64,14 @@ class ExecutionContext { * @return {!Promise} */ async evaluateHandle(pageFunction, ...args) { + const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`; + if (helper.isString(pageFunction)) { const contextId = this._contextId; const expression = /** @type {string} */ (pageFunction); + const expressionWithSourceUrl = SOURCE_URL_REGEX.test(expression) ? expression : expression + '\n' + suffix; const {exceptionDetails, result: remoteObject} = await this._client.send('Runtime.evaluate', { - expression, + expression: expressionWithSourceUrl, contextId, returnByValue: false, awaitPromise: true, @@ -76,8 +82,11 @@ class ExecutionContext { return this._objectHandleFactory(remoteObject); } + if (typeof pageFunction !== 'function') + throw new Error('The following is not a function: ' + pageFunction); + const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', { - functionDeclaration: pageFunction.toString(), + functionDeclaration: pageFunction.toString() + '\n' + suffix + '\n', executionContextId: this._contextId, arguments: args.map(convertArgument.bind(this)), returnByValue: false, @@ -239,4 +248,4 @@ class JSHandle { } helper.tracePublicAPI(JSHandle); -module.exports = {ExecutionContext, JSHandle}; +module.exports = {ExecutionContext, JSHandle, EVALUATION_SCRIPT_URL}; diff --git a/test/assets/jscoverage/eval.html b/test/assets/jscoverage/eval.html new file mode 100644 index 00000000..838ae287 --- /dev/null +++ b/test/assets/jscoverage/eval.html @@ -0,0 +1 @@ + diff --git a/test/coverage.spec.js b/test/coverage.spec.js index c5875090..ede30769 100644 --- a/test/coverage.spec.js +++ b/test/coverage.spec.js @@ -38,12 +38,27 @@ module.exports.addTests = function({testRunner, expect}) { expect(coverage.length).toBe(1); expect(coverage[0].url).toBe('nicename.js'); }); - it('should ignore anonymous scripts', async function({page, server}) { - await page.coverage.startJSCoverage(); - await page.goto(server.EMPTY_PAGE); - await page.evaluate(() => console.log(1)); - const coverage = await page.coverage.stopJSCoverage(); - expect(coverage.length).toBe(0); + describe('anonymous scripts coverage', function() { + it('should ignore eval() scripts by default', async function({page, server}) { + await page.coverage.startJSCoverage(); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(1); + }); + it('shouldn\'t ignore eval() scripts if reportAnonymousScripts is true', async function({page, server}) { + await page.coverage.startJSCoverage({reportAnonymousScripts: true}); + await page.goto(server.PREFIX + '/jscoverage/eval.html'); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(2); + }); + it('should ignore injected scripts regardless of reportAnonymousScripts', async function({page, server}) { + await page.coverage.startJSCoverage({reportAnonymousScripts: true}); + await page.goto(server.EMPTY_PAGE); + await page.evaluate('console.log("foo")'); + await page.evaluate(() => console.log('bar')); + const coverage = await page.coverage.stopJSCoverage(); + expect(coverage.length).toBe(0); + }); }); it('should report multiple scripts', async function({page, server}) { await page.coverage.startJSCoverage();