Skip to content

Commit cd4e9da

Browse files
authored
feat(coverage): export raw v8 coverage (#976)
Fixes #955
1 parent 7ec3bf4 commit cd4e9da

File tree

8 files changed

+121
-154
lines changed

8 files changed

+121
-154
lines changed

docs/api.md

Lines changed: 68 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121
- [class: Selectors](#class-selectors)
2222
- [class: TimeoutError](#class-timeouterror)
2323
- [class: Accessibility](#class-accessibility)
24-
- [class: Coverage](#class-coverage)
2524
- [class: Worker](#class-worker)
2625
- [class: BrowserServer](#class-browserserver)
2726
- [class: BrowserType](#class-browsertype)
2827
- [class: ChromiumBrowser](#class-chromiumbrowser)
28+
- [class: ChromiumCoverage](#class-chromiumcoverage)
2929
- [class: ChromiumSession](#class-chromiumsession)
3030
- [class: ChromiumTarget](#class-chromiumtarget)
3131
- [class: FirefoxBrowser](#class-firefoxbrowser)
@@ -821,9 +821,9 @@ Get the browser context that the page belongs to.
821821

822822
#### page.coverage
823823

824-
- returns: <[Coverage]>
824+
- returns: <?[any]>
825825

826-
> **NOTE** Code coverage is currently only supported in Chromium.
826+
Browser-specific Coverage implementation, only available for Chromium atm. See [ChromiumCoverage](#class-chromiumcoverage) for more details.
827827

828828
#### page.dblclick(selector[, options])
829829
- `selector` <[string]> A selector to search for element to double click. If there are multiple elements satisfying the selector, the first will be double clicked.
@@ -3261,78 +3261,6 @@ function findFocusedNode(node) {
32613261
}
32623262
```
32633263

3264-
### class: Coverage
3265-
3266-
Coverage gathers information about parts of JavaScript and CSS that were used by the page.
3267-
3268-
An example of using JavaScript and CSS coverage to get percentage of initially
3269-
executed code:
3270-
3271-
```js
3272-
// Enable both JavaScript and CSS coverage
3273-
await Promise.all([
3274-
page.coverage.startJSCoverage(),
3275-
page.coverage.startCSSCoverage()
3276-
]);
3277-
// Navigate to page
3278-
await page.goto('https://example.com');
3279-
// Disable both JavaScript and CSS coverage
3280-
const [jsCoverage, cssCoverage] = await Promise.all([
3281-
page.coverage.stopJSCoverage(),
3282-
page.coverage.stopCSSCoverage(),
3283-
]);
3284-
let totalBytes = 0;
3285-
let usedBytes = 0;
3286-
const coverage = [...jsCoverage, ...cssCoverage];
3287-
for (const entry of coverage) {
3288-
totalBytes += entry.text.length;
3289-
for (const range of entry.ranges)
3290-
usedBytes += range.end - range.start - 1;
3291-
}
3292-
console.log(`Bytes used: ${usedBytes / totalBytes * 100}%`);
3293-
```
3294-
3295-
<!-- GEN:toc -->
3296-
- [coverage.startCSSCoverage([options])](#coveragestartcsscoverageoptions)
3297-
- [coverage.startJSCoverage([options])](#coveragestartjscoverageoptions)
3298-
- [coverage.stopCSSCoverage()](#coveragestopcsscoverage)
3299-
- [coverage.stopJSCoverage()](#coveragestopjscoverage)
3300-
<!-- GEN:stop -->
3301-
3302-
#### coverage.startCSSCoverage([options])
3303-
- `options` <[Object]> Set of configurable options for coverage
3304-
- `resetOnNavigation` <[boolean]> Whether to reset coverage on every navigation. Defaults to `true`.
3305-
- returns: <[Promise]> Promise that resolves when coverage is started
3306-
3307-
#### coverage.startJSCoverage([options])
3308-
- `options` <[Object]> Set of configurable options for coverage
3309-
- `resetOnNavigation` <[boolean]> Whether to reset coverage on every navigation. Defaults to `true`.
3310-
- `reportAnonymousScripts` <[boolean]> Whether anonymous scripts generated by the page should be reported. Defaults to `false`.
3311-
- returns: <[Promise]> Promise that resolves when coverage is started
3312-
3313-
> **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 `__playwright_evaluation_script__` as their URL.
3314-
3315-
#### coverage.stopCSSCoverage()
3316-
- returns: <[Promise]<[Array]<[Object]>>> Promise that resolves to the array of coverage reports for all stylesheets
3317-
- `url` <[string]> StyleSheet URL
3318-
- `text` <[string]> StyleSheet content
3319-
- `ranges` <[Array]<[Object]>> StyleSheet ranges that were used. Ranges are sorted and non-overlapping.
3320-
- `start` <[number]> A start offset in text, inclusive
3321-
- `end` <[number]> An end offset in text, exclusive
3322-
3323-
> **NOTE** CSS Coverage doesn't include dynamically injected style tags without sourceURLs.
3324-
3325-
#### coverage.stopJSCoverage()
3326-
- returns: <[Promise]<[Array]<[Object]>>> Promise that resolves to the array of coverage reports for all scripts
3327-
- `url` <[string]> Script URL
3328-
- `text` <[string]> Script content
3329-
- `ranges` <[Array]<[Object]>> Script ranges that were executed. Ranges are sorted and non-overlapping.
3330-
- `start` <[number]> A start offset in text, inclusive
3331-
- `end` <[number]> An end offset in text, exclusive
3332-
3333-
> **NOTE** JavaScript Coverage doesn't include anonymous scripts by default. However, scripts with sourceURLs are
3334-
reported.
3335-
33363264
### class: Worker
33373265

33383266
The Worker class represents a [WebWorker](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API).
@@ -3687,6 +3615,71 @@ await page.evaluate(() => window.open('https://www.example.com/'));
36873615
const newWindowTarget = await browser.chromium.waitForTarget(target => target.url() === 'https://www.example.com/');
36883616
```
36893617

3618+
### class: ChromiumCoverage
3619+
3620+
Coverage gathers information about parts of JavaScript and CSS that were used by the page.
3621+
3622+
An example of using JavaScript coverage to produce Istambul report for page load:
3623+
3624+
```js
3625+
const { chromium } = require('.');
3626+
const v8toIstanbul = require('v8-to-istanbul');
3627+
3628+
(async() => {
3629+
const browser = await chromium.launch();
3630+
const page = await browser.newPage();
3631+
await page.coverage.startJSCoverage();
3632+
await page.goto('https://chromium.org');
3633+
const coverage = await page.coverage.stopJSCoverage();
3634+
for (const entry of coverage) {
3635+
const converter = new v8toIstanbul('', 0, { source: entry.source });
3636+
await converter.load();
3637+
converter.applyCoverage(entry.functions);
3638+
console.log(JSON.stringify(converter.toIstanbul()));
3639+
}
3640+
await browser.close();
3641+
})();
3642+
```
3643+
3644+
<!-- GEN:toc -->
3645+
- [chromiumCoverage.startCSSCoverage([options])](#chromiumcoveragestartcsscoverageoptions)
3646+
- [chromiumCoverage.startJSCoverage([options])](#chromiumcoveragestartjscoverageoptions)
3647+
- [chromiumCoverage.stopCSSCoverage()](#chromiumcoveragestopcsscoverage)
3648+
- [chromiumCoverage.stopJSCoverage()](#chromiumcoveragestopjscoverage)
3649+
<!-- GEN:stop -->
3650+
3651+
#### chromiumCoverage.startCSSCoverage([options])
3652+
- `options` <[Object]> Set of configurable options for coverage
3653+
- `resetOnNavigation` <[boolean]> Whether to reset coverage on every navigation. Defaults to `true`.
3654+
- returns: <[Promise]> Promise that resolves when coverage is started
3655+
3656+
#### chromiumCoverage.startJSCoverage([options])
3657+
- `options` <[Object]> Set of configurable options for coverage
3658+
- `resetOnNavigation` <[boolean]> Whether to reset coverage on every navigation. Defaults to `true`.
3659+
- `reportAnonymousScripts` <[boolean]> Whether anonymous scripts generated by the page should be reported. Defaults to `false`.
3660+
- returns: <[Promise]> Promise that resolves when coverage is started
3661+
3662+
> **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 `__playwright_evaluation_script__` as their URL.
3663+
3664+
#### chromiumCoverage.stopCSSCoverage()
3665+
- returns: <[Promise]<[Array]<[Object]>>> Promise that resolves to the array of coverage reports for all stylesheets
3666+
- `url` <[string]> StyleSheet URL
3667+
- `text` <[string]> StyleSheet content
3668+
- `ranges` <[Array]<[Object]>> StyleSheet ranges that were used. Ranges are sorted and non-overlapping.
3669+
- `start` <[number]> A start offset in text, inclusive
3670+
- `end` <[number]> An end offset in text, exclusive
3671+
3672+
> **NOTE** CSS Coverage doesn't include dynamically injected style tags without sourceURLs.
3673+
3674+
#### chromiumCoverage.stopJSCoverage()
3675+
- returns: <[Promise]<[Array]<[Object]>>> Promise that resolves to the array of coverage reports for all scripts
3676+
- `url` <[string]> Script URL
3677+
- `source` <[string]> Script content
3678+
- `functions` <[Array]<[Object]>> V8-specific coverage format.
3679+
3680+
> **NOTE** JavaScript Coverage doesn't include anonymous scripts by default. However, scripts with sourceURLs are
3681+
reported.
3682+
36903683
### class: ChromiumSession
36913684

36923685
* extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)

src/api.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ export { Frame } from './frames';
2525
export { Keyboard, Mouse } from './input';
2626
export { JSHandle } from './javascript';
2727
export { Request, Response } from './network';
28-
export { Coverage, FileChooser, Page, Worker } from './page';
28+
export { FileChooser, Page, Worker } from './page';
2929
export { Selectors } from './selectors';
3030

3131
export { CRBrowser as ChromiumBrowser } from './chromium/crBrowser';
32+
export { CRCoverage as ChromiumCoverage } from './chromium/crCoverage';
3233
export { CRSession as ChromiumSession } from './chromium/crConnection';
3334
export { CRTarget as ChromiumTarget } from './chromium/crTarget';
3435

src/chromium/crCoverage.ts

Lines changed: 42 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,33 @@ import { assert, debugError, helper, RegisteredListener } from '../helper';
2020
import { Protocol } from './protocol';
2121

2222
import { EVALUATION_SCRIPT_URL } from './crExecutionContext';
23-
import { Coverage } from '../page';
2423
import * as types from '../types';
2524

26-
type CoverageEntry = {
25+
type JSRange = {
26+
startOffset: number,
27+
endOffset: number,
28+
count: number
29+
}
30+
31+
type CSSCoverageEntry = {
2732
url: string,
28-
text: string,
29-
ranges: {start: number, end: number}[]
33+
text?: string,
34+
ranges: {
35+
start: number,
36+
end: number
37+
}[]
3038
};
3139

32-
export class CRCoverage implements Coverage {
40+
type JSCoverageEntry = {
41+
url: string,
42+
source?: string,
43+
functions: {
44+
functionName: string,
45+
ranges: JSRange[]
46+
}[]
47+
};
48+
49+
export class CRCoverage {
3350
private _jsCoverage: JSCoverage;
3451
private _cssCoverage: CSSCoverage;
3552

@@ -42,23 +59,23 @@ export class CRCoverage implements Coverage {
4259
return await this._jsCoverage.start(options);
4360
}
4461

45-
async stopJSCoverage(): Promise<CoverageEntry[]> {
62+
async stopJSCoverage(): Promise<JSCoverageEntry[]> {
4663
return await this._jsCoverage.stop();
4764
}
4865

4966
async startCSSCoverage(options?: types.CSSCoverageOptions) {
5067
return await this._cssCoverage.start(options);
5168
}
5269

53-
async stopCSSCoverage(): Promise<CoverageEntry[]> {
70+
async stopCSSCoverage(): Promise<CSSCoverageEntry[]> {
5471
return await this._cssCoverage.stop();
5572
}
5673
}
5774

5875
class JSCoverage {
5976
_client: CRSession;
6077
_enabled: boolean;
61-
_scriptURLs: Map<string, string>;
78+
_scriptIds: Set<string>;
6279
_scriptSources: Map<string, string>;
6380
_eventListeners: RegisteredListener[];
6481
_resetOnNavigation: boolean;
@@ -67,7 +84,7 @@ class JSCoverage {
6784
constructor(client: CRSession) {
6885
this._client = client;
6986
this._enabled = false;
70-
this._scriptURLs = new Map();
87+
this._scriptIds = new Set();
7188
this._scriptSources = new Map();
7289
this._eventListeners = [];
7390
this._resetOnNavigation = false;
@@ -82,7 +99,7 @@ class JSCoverage {
8299
this._resetOnNavigation = resetOnNavigation;
83100
this._reportAnonymousScripts = reportAnonymousScripts;
84101
this._enabled = true;
85-
this._scriptURLs.clear();
102+
this._scriptIds.clear();
86103
this._scriptSources.clear();
87104
this._eventListeners = [
88105
helper.addEventListener(this._client, 'Debugger.scriptParsed', this._onScriptParsed.bind(this)),
@@ -91,7 +108,7 @@ class JSCoverage {
91108
this._client.on('Debugger.paused', () => this._client.send('Debugger.resume'));
92109
await Promise.all([
93110
this._client.send('Profiler.enable'),
94-
this._client.send('Profiler.startPreciseCoverage', {callCount: false, detailed: true}),
111+
this._client.send('Profiler.startPreciseCoverage', { callCount: true, detailed: true }),
95112
this._client.send('Debugger.enable'),
96113
this._client.send('Debugger.setSkipAllPauses', {skip: true})
97114
]);
@@ -100,28 +117,28 @@ class JSCoverage {
100117
_onExecutionContextsCleared() {
101118
if (!this._resetOnNavigation)
102119
return;
103-
this._scriptURLs.clear();
120+
this._scriptIds.clear();
104121
this._scriptSources.clear();
105122
}
106123

107124
async _onScriptParsed(event: Protocol.Debugger.scriptParsedPayload) {
108125
// Ignore playwright-injected scripts
109126
if (event.url === EVALUATION_SCRIPT_URL)
110127
return;
128+
this._scriptIds.add(event.scriptId);
111129
// Ignore other anonymous scripts unless the reportAnonymousScripts option is true.
112130
if (!event.url && !this._reportAnonymousScripts)
113131
return;
114132
try {
115133
const response = await this._client.send('Debugger.getScriptSource', {scriptId: event.scriptId});
116-
this._scriptURLs.set(event.scriptId, event.url);
117134
this._scriptSources.set(event.scriptId, response.scriptSource);
118135
} catch (e) {
119136
// This might happen if the page has already navigated away.
120137
debugError(e);
121138
}
122139
}
123140

124-
async stop(): Promise<CoverageEntry[]> {
141+
async stop(): Promise<JSCoverageEntry[]> {
125142
assert(this._enabled, 'JSCoverage is not enabled');
126143
this._enabled = false;
127144
const [profileResponse] = await Promise.all([
@@ -132,19 +149,17 @@ class JSCoverage {
132149
] as const);
133150
helper.removeEventListeners(this._eventListeners);
134151

135-
const coverage = [];
152+
const coverage: JSCoverageEntry[] = [];
136153
for (const entry of profileResponse.result) {
137-
let url = this._scriptURLs.get(entry.scriptId);
138-
if (!url && this._reportAnonymousScripts)
139-
url = 'debugger://VM' + entry.scriptId;
140-
const text = this._scriptSources.get(entry.scriptId);
141-
if (text === undefined || url === undefined)
154+
if (!this._scriptIds.has(entry.scriptId))
155+
continue;
156+
if (!entry.url && !this._reportAnonymousScripts)
142157
continue;
143-
const flattenRanges = [];
144-
for (const func of entry.functions)
145-
flattenRanges.push(...func.ranges);
146-
const ranges = convertToDisjointRanges(flattenRanges);
147-
coverage.push({url, ranges, text});
158+
const source = this._scriptSources.get(entry.scriptId);
159+
if (source)
160+
coverage.push({...entry, source});
161+
else
162+
coverage.push(entry);
148163
}
149164
return coverage;
150165
}
@@ -207,7 +222,7 @@ class CSSCoverage {
207222
}
208223
}
209224

210-
async stop(): Promise<CoverageEntry[]> {
225+
async stop(): Promise<CSSCoverageEntry[]> {
211226
assert(this._enabled, 'CSSCoverage is not enabled');
212227
this._enabled = false;
213228
const ruleTrackingResponse = await this._client.send('CSS.stopRuleUsageTracking');
@@ -232,7 +247,7 @@ class CSSCoverage {
232247
});
233248
}
234249

235-
const coverage: CoverageEntry[] = [];
250+
const coverage: CSSCoverageEntry[] = [];
236251
for (const styleSheetId of this._stylesheetURLs.keys()) {
237252
const url = this._stylesheetURLs.get(styleSheetId)!;
238253
const text = this._stylesheetSources.get(styleSheetId)!;

src/chromium/crPage.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import * as network from '../network';
2323
import { CRSession, CRConnection } from './crConnection';
2424
import { EVALUATION_SCRIPT_URL, CRExecutionContext } from './crExecutionContext';
2525
import { CRNetworkManager } from './crNetworkManager';
26-
import { Page, Coverage, Worker } from '../page';
26+
import { Page, Worker } from '../page';
2727
import { Protocol } from './protocol';
2828
import { Events } from '../events';
2929
import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crProtocolHelper';
@@ -544,7 +544,7 @@ export class CRPage implements PageDelegate {
544544
return this._pdf.generate(options);
545545
}
546546

547-
coverage(): Coverage | undefined {
547+
coverage(): CRCoverage {
548548
return this._coverage;
549549
}
550550

src/firefox/ffPage.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { helper, RegisteredListener, debugError, assert } from '../helper';
2020
import * as dom from '../dom';
2121
import { FFSession } from './ffConnection';
2222
import { FFExecutionContext } from './ffExecutionContext';
23-
import { Page, PageDelegate, Coverage, Worker } from '../page';
23+
import { Page, PageDelegate, Worker } from '../page';
2424
import { FFNetworkManager } from './ffNetworkManager';
2525
import { Events } from '../events';
2626
import * as dialog from '../dialog';
@@ -438,10 +438,6 @@ export class FFPage implements PageDelegate {
438438
return getAccessibilityTree(this._session, needle);
439439
}
440440

441-
coverage(): Coverage | undefined {
442-
return undefined;
443-
}
444-
445441
async getFrameElement(frame: frames.Frame): Promise<dom.ElementHandle> {
446442
const parent = frame.parentFrame();
447443
if (!parent)

0 commit comments

Comments
 (0)