feat(coverage): add an option to collect coverage of anonymous scripts (#2796)

This patch adds `reportAnonymousScripts` option to the `coverage.startJSCoverage` method. With this option, anonymous scripts are reported as well.

Fixes #2777
This commit is contained in:
Pavel Pomerantsev 2018-07-11 21:38:34 -04:00 committed by Andrey Lushnikov
parent 96c558d544
commit 12bc1e1a62
5 changed files with 47 additions and 13 deletions

View File

@ -2939,8 +2939,11 @@ _To output coverage in a form consumable by [Istanbul](https://github.com/istanb
#### coverage.startJSCoverage(options) #### coverage.startJSCoverage(options)
- `options` <[Object]> Set of configurable options for coverage - `options` <[Object]> Set of configurable options for coverage
- `resetOnNavigation` <[boolean]> Whether to reset coverage on every navigation. Defaults to `true`. - `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 - 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() #### coverage.stopCSSCoverage()
- returns: <[Promise]<[Array]<[Object]>>> Promise that resolves to the array of coverage reports for all stylesheets - returns: <[Promise]<[Array]<[Object]>>> Promise that resolves to the array of coverage reports for all stylesheets
- `url` <[string]> StyleSheet URL - `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. > **NOTE** CSS Coverage doesn't include dynamically injected style tags without sourceURLs.
#### coverage.stopJSCoverage() #### 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 - `url` <[string]> Script URL
- `text` <[string]> Script content - `text` <[string]> Script content
- `ranges` <[Array]<[Object]>> Script ranges that were executed. Ranges are sorted and non-overlapping. - `ranges` <[Array]<[Object]>> Script ranges that were executed. Ranges are sorted and non-overlapping.
- `start` <[number]> A start offset in text, inclusive - `start` <[number]> A start offset in text, inclusive
- `end` <[number]> An end offset in text, exclusive - `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. reported.
[Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array "Array" [Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array "Array"

View File

@ -16,6 +16,8 @@
const {helper, debugError, assert} = require('./helper'); const {helper, debugError, assert} = require('./helper');
const {EVALUATION_SCRIPT_URL} = require('./ExecutionContext');
/** /**
* @typedef {Object} CoverageEntry * @typedef {Object} CoverageEntry
* @property {string} url * @property {string} url
@ -83,6 +85,7 @@ class JSCoverage {
async start(options = {}) { async start(options = {}) {
assert(!this._enabled, 'JSCoverage is already enabled'); assert(!this._enabled, 'JSCoverage is already enabled');
this._resetOnNavigation = options.resetOnNavigation === undefined ? true : !!options.resetOnNavigation; this._resetOnNavigation = options.resetOnNavigation === undefined ? true : !!options.resetOnNavigation;
this._reportAnonymousScripts = !!options.reportAnonymousScripts;
this._enabled = true; this._enabled = true;
this._scriptURLs.clear(); this._scriptURLs.clear();
this._scriptSources.clear(); this._scriptSources.clear();
@ -109,8 +112,11 @@ class JSCoverage {
* @param {!Protocol.Debugger.scriptParsedPayload} event * @param {!Protocol.Debugger.scriptParsedPayload} event
*/ */
async _onScriptParsed(event) { async _onScriptParsed(event) {
// Ignore anonymous scripts // Ignore puppeteer-injected scripts
if (!event.url) if (event.url === EVALUATION_SCRIPT_URL)
return;
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
if (!event.url && !this._reportAnonymousScripts)
return; return;
try { try {
const response = await this._client.send('Debugger.getScriptSource', {scriptId: event.scriptId}); const response = await this._client.send('Debugger.getScriptSource', {scriptId: event.scriptId});

View File

@ -16,6 +16,9 @@
const {helper, assert} = require('./helper'); 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 { class ExecutionContext {
/** /**
* @param {!Puppeteer.CDPSession} client * @param {!Puppeteer.CDPSession} client
@ -61,11 +64,14 @@ class ExecutionContext {
* @return {!Promise<!JSHandle>} * @return {!Promise<!JSHandle>}
*/ */
async evaluateHandle(pageFunction, ...args) { async evaluateHandle(pageFunction, ...args) {
const suffix = `//# sourceURL=${EVALUATION_SCRIPT_URL}`;
if (helper.isString(pageFunction)) { if (helper.isString(pageFunction)) {
const contextId = this._contextId; const contextId = this._contextId;
const expression = /** @type {string} */ (pageFunction); 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', { const {exceptionDetails, result: remoteObject} = await this._client.send('Runtime.evaluate', {
expression, expression: expressionWithSourceUrl,
contextId, contextId,
returnByValue: false, returnByValue: false,
awaitPromise: true, awaitPromise: true,
@ -76,8 +82,11 @@ class ExecutionContext {
return this._objectHandleFactory(remoteObject); 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', { const { exceptionDetails, result: remoteObject } = await this._client.send('Runtime.callFunctionOn', {
functionDeclaration: pageFunction.toString(), functionDeclaration: pageFunction.toString() + '\n' + suffix + '\n',
executionContextId: this._contextId, executionContextId: this._contextId,
arguments: args.map(convertArgument.bind(this)), arguments: args.map(convertArgument.bind(this)),
returnByValue: false, returnByValue: false,
@ -239,4 +248,4 @@ class JSHandle {
} }
helper.tracePublicAPI(JSHandle); helper.tracePublicAPI(JSHandle);
module.exports = {ExecutionContext, JSHandle}; module.exports = {ExecutionContext, JSHandle, EVALUATION_SCRIPT_URL};

View File

@ -0,0 +1 @@
<script>eval('console.log("foo")')</script>

View File

@ -38,12 +38,27 @@ module.exports.addTests = function({testRunner, expect}) {
expect(coverage.length).toBe(1); expect(coverage.length).toBe(1);
expect(coverage[0].url).toBe('nicename.js'); expect(coverage[0].url).toBe('nicename.js');
}); });
it('should ignore anonymous scripts', async function({page, server}) { describe('anonymous scripts coverage', function() {
await page.coverage.startJSCoverage(); it('should ignore eval() scripts by default', async function({page, server}) {
await page.goto(server.EMPTY_PAGE); await page.coverage.startJSCoverage();
await page.evaluate(() => console.log(1)); await page.goto(server.PREFIX + '/jscoverage/eval.html');
const coverage = await page.coverage.stopJSCoverage(); const coverage = await page.coverage.stopJSCoverage();
expect(coverage.length).toBe(0); 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}) { it('should report multiple scripts', async function({page, server}) {
await page.coverage.startJSCoverage(); await page.coverage.startJSCoverage();