monocart-coverage-reports 2.6.4 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -3
- package/lib/client/cdp-client.js +146 -0
- package/lib/client/cdp-session.js +63 -0
- package/lib/client/coverage-client.js +280 -0
- package/lib/converter/find-original-range.js +49 -27
- package/lib/generate.js +6 -0
- package/lib/index.d.ts +53 -0
- package/lib/index.js +2 -0
- package/lib/index.mjs +1 -0
- package/lib/packages/monocart-coverage-v8.js +1 -1
- package/lib/packages/monocart-coverage-vendor.js +44 -38
- package/lib/utils/request.js +6 -5
- package/lib/utils/snapshot.js +4 -0
- package/lib/v8/v8.js +2 -1
- package/package.json +83 -81
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|

|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
>
|
|
10
|
+
> A code coverage tool to generate native [V8](https://v8.dev/blog/javascript-code-coverage) reports or [Istanbul](https://istanbul.js.org/) reports. As early as 2017, V8 started offering native support for coverage data. However, we have essentially only been able to see coverage reports from Istanbul. Nowadays, we can directly generate V8 coverage reports with `monocart-coverage-reports`.
|
|
11
11
|
|
|
12
12
|
* [Usage](#usage)
|
|
13
13
|
* [Default Options](#default-options)
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* [Manually Resolve the Sourcemap](#manually-resolve-the-sourcemap)
|
|
23
23
|
* [Collecting Raw V8 Coverage Data with Puppeteer](#collecting-raw-v8-coverage-data-with-puppeteer)
|
|
24
24
|
* [Node.js V8 Coverage Report for Server Side](#nodejs-v8-coverage-report-for-server-side)
|
|
25
|
+
* [Collecting V8 Coverage Data with `CDPClient` API](#collecting-v8-coverage-data-with-cdpclient-api)
|
|
25
26
|
* [Multiprocessing Support](#multiprocessing-support)
|
|
26
27
|
* [Merge Coverage Reports](#merge-coverage-reports)
|
|
27
28
|
* [Resolve `sourcePath` for the Source Files](#resolve-sourcepath-for-the-source-files)
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
- [Playwright](#playwright)
|
|
39
40
|
- [Jest](#jest)
|
|
40
41
|
- [Vitest](#vitest)
|
|
42
|
+
- [Cypress](#cypress)
|
|
41
43
|
- [CodeceptJS](#codeceptjs)
|
|
42
44
|
- [WebdriverIO](#webdriverio)
|
|
43
45
|
- [Storybook Test Runner](#storybook-test-runner)
|
|
@@ -72,14 +74,30 @@ await coverageReport.generate();
|
|
|
72
74
|
|
|
73
75
|
## Default Options
|
|
74
76
|
- [lib/default/options.js](https://github.com/cenfun/monocart-coverage-reports/blob/main/lib/default/options.js)
|
|
77
|
+
- `reports` [Available Reports](#available-reports)
|
|
78
|
+
- `entryFilter` and `sourceFilter` [Using `entryFilter` and `sourceFilter` to filter the results for V8 report](#using-entryfilter-and-sourcefilter-to-filter-the-results-for-v8-report)
|
|
79
|
+
- `sourcePath` [Resolve `sourcePath` for the Source Files](#resolve-sourcepath-for-the-source-files)
|
|
80
|
+
- `all` [Adding Empty Coverage for Untested Files](#adding-empty-coverage-for-untested-files)
|
|
81
|
+
- `inputDir` [Merge Coverage Reports](#merge-coverage-reports)
|
|
82
|
+
- `logging` [Debug for Coverage and Sourcemap](#debug-for-coverage-and-sourcemap)
|
|
83
|
+
- `onEnd` [onEnd Hook](#onend-hook)
|
|
84
|
+
|
|
85
|
+
- Declaration [lib/index.d.ts](https://github.com/cenfun/monocart-coverage-reports/blob/main/lib/index.d.ts)
|
|
75
86
|
|
|
76
87
|
## Available Reports
|
|
77
88
|
|
|
78
89
|
> V8 build-in reports (V8 data only):
|
|
79
90
|
|
|
80
91
|
- `v8`
|
|
81
|
-
-
|
|
82
|
-
|
|
92
|
+
- Features:
|
|
93
|
+
- A Brand-New V8 Coverage Report User Interface
|
|
94
|
+
- Support for Native Byte Statistics
|
|
95
|
+
- Coverage for Any Runtime Code
|
|
96
|
+
- CSS Coverage Support
|
|
97
|
+
- Better Support for Sourcemap Conversion
|
|
98
|
+
- Demos:
|
|
99
|
+
- Browser: Build with webpack [V8](https://cenfun.github.io/monocart-coverage-reports/v8) and [Minify](https://cenfun.github.io/monocart-coverage-reports/minify); Build with [Rollup](https://cenfun.github.io/monocart-coverage-reports/rollup) and [Esbuild](https://cenfun.github.io/monocart-coverage-reports/esbuild); Collect with [puppeteer](https://cenfun.github.io/monocart-coverage-reports/puppeteer/); [anonymous](https://cenfun.github.io/monocart-coverage-reports/anonymous/) and [css](https://cenfun.github.io/monocart-coverage-reports/css/)
|
|
100
|
+
- Node.js: Collect with [env](https://cenfun.github.io/monocart-coverage-reports/node-env), and also V8 [API](https://cenfun.github.io/monocart-coverage-reports/node-api), [Inspector](https://cenfun.github.io/monocart-coverage-reports/node-ins) and [CDP](https://cenfun.github.io/monocart-coverage-reports/node-cdp); Web server example: [koa](https://cenfun.github.io/monocart-coverage-reports/node-koa/)
|
|
83
101
|
|
|
84
102
|

|
|
85
103
|
|
|
@@ -357,6 +375,8 @@ Options:
|
|
|
357
375
|
- see [Collecting Raw V8 Coverage Data with Puppeteer](#collecting-raw-v8-coverage-data-with-puppeteer)
|
|
358
376
|
- Node.js
|
|
359
377
|
- see [Node.js V8 Coverage Report for Server Side](#nodejs-v8-coverage-report-for-server-side)
|
|
378
|
+
- CDP
|
|
379
|
+
- see [Collecting V8 Coverage Data with `CDPClient` API](#collecting-v8-coverage-data-with-cdpclient-api)
|
|
360
380
|
|
|
361
381
|
## Manually Resolve the Sourcemap
|
|
362
382
|
> Sometimes, the sourcemap file cannot be successfully loaded with the `sourceMappingURL`, you can try to manually read the sourcemap file before the coverage data is added to the report.
|
|
@@ -436,6 +456,50 @@ Possible solutions:
|
|
|
436
456
|
- [Child Process](https://nodejs.org/docs/latest/api/child_process.html) + NODE_V8_COVERAGE
|
|
437
457
|
- see [`mcr` CLI](#mcr-cli)
|
|
438
458
|
|
|
459
|
+
## Collecting V8 Coverage Data with `CDPClient` API
|
|
460
|
+
- Work with node debugger `--inspect=9229`
|
|
461
|
+
```js
|
|
462
|
+
const MCR = require('monocart-coverage-reports');
|
|
463
|
+
const client = await MCR.CDPClient({
|
|
464
|
+
port: 9229
|
|
465
|
+
});
|
|
466
|
+
await client.startJSCoverage();
|
|
467
|
+
// run your test here
|
|
468
|
+
const coverageData = await client.stopJSCoverage();
|
|
469
|
+
```
|
|
470
|
+
- Work with [Playwright CDPSession](https://playwright.dev/docs/api/class-cdpsession)
|
|
471
|
+
```js
|
|
472
|
+
const { chromium } = require('playwright');
|
|
473
|
+
const MCR = require('monocart-coverage-reports');
|
|
474
|
+
const browser = await chromium.launch();
|
|
475
|
+
const page = await browser.newPage();
|
|
476
|
+
const session = await page.context().newCDPSession(page);
|
|
477
|
+
const client = await MCR.CDPClient({
|
|
478
|
+
session
|
|
479
|
+
});
|
|
480
|
+
// both js and css coverage
|
|
481
|
+
await client.startCoverage();
|
|
482
|
+
// run your test page here
|
|
483
|
+
await page.goto("your page url");
|
|
484
|
+
const coverageData = await client.stopCoverage();
|
|
485
|
+
```
|
|
486
|
+
- Work with [Puppeteer CDPSession](https://pptr.dev/api/puppeteer.cdpsession)
|
|
487
|
+
```js
|
|
488
|
+
const puppeteer = require('puppeteer');
|
|
489
|
+
const MCR = require('monocart-coverage-reports');
|
|
490
|
+
const browser = await puppeteer.launch({});
|
|
491
|
+
const page = await browser.newPage();
|
|
492
|
+
const session = await page.target().createCDPSession();
|
|
493
|
+
const client = await MCR.CDPClient({
|
|
494
|
+
session
|
|
495
|
+
});
|
|
496
|
+
// both js and css coverage
|
|
497
|
+
await client.startCoverage();
|
|
498
|
+
// run your test page here
|
|
499
|
+
await page.goto("your page url");
|
|
500
|
+
const coverageData = await client.stopCoverage();
|
|
501
|
+
```
|
|
502
|
+
|
|
439
503
|
## Multiprocessing Support
|
|
440
504
|
> The data will be added to `[outputDir]/.cache`, After the generation of the report, this data will be removed unless debugging has been enabled or a raw report has been used, see [Debug for Coverage and Sourcemap](#debug-for-coverage-and-sourcemap)
|
|
441
505
|
- Main process, before the start of testing
|
|
@@ -746,6 +810,9 @@ const coverageOptions = {
|
|
|
746
810
|
### [Vitest](https://github.com/vitest-dev/vitest)
|
|
747
811
|
- [vitest-monocart-coverage](https://github.com/cenfun/vitest-monocart-coverage) - A Vitest custom provider module for coverage reports
|
|
748
812
|
|
|
813
|
+
### [Cypress](https://github.com/cypress-io/cypress)
|
|
814
|
+
- [cypress-monocart-coverage](https://github.com/cenfun/cypress-monocart-coverage) - A Cypress plugin for coverage reports
|
|
815
|
+
|
|
749
816
|
### [CodeceptJS](https://github.com/codeceptjs/CodeceptJS)
|
|
750
817
|
- [codeceptjs-monocart-coverage](https://github.com/cenfun/codeceptjs-monocart-coverage) - A CodeceptJS plugin for coverage reports
|
|
751
818
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
|
|
2
|
+
const EC = require('eight-colors');
|
|
3
|
+
|
|
4
|
+
const { WebSocket } = require('../packages/monocart-coverage-vendor.js');
|
|
5
|
+
const Util = require('../utils/util.js');
|
|
6
|
+
|
|
7
|
+
const CDPSession = require('./cdp-session.js');
|
|
8
|
+
const CoverageClient = require('./coverage-client.js');
|
|
9
|
+
|
|
10
|
+
const getDebuggerUrl = async (options) => {
|
|
11
|
+
|
|
12
|
+
const protocol = options.secure ? 'https' : 'http';
|
|
13
|
+
|
|
14
|
+
const url = `${protocol}://${options.host}:${options.port}/json/list`;
|
|
15
|
+
const [err, res] = await Util.request({
|
|
16
|
+
url,
|
|
17
|
+
timeout: options.timeout
|
|
18
|
+
});
|
|
19
|
+
if (err) {
|
|
20
|
+
return [err];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const targets = res.data;
|
|
24
|
+
if (!Util.isList(targets)) {
|
|
25
|
+
return [new Error(`Invalid response data: ${url}`)];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// console.log(targets);
|
|
29
|
+
const target = options.target(targets);
|
|
30
|
+
if (!target) {
|
|
31
|
+
return [new Error(`Not found target: ${url}`)];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return [null, target.webSocketDebuggerUrl];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const getCDPUrl = async (options) => {
|
|
38
|
+
if (options.url) {
|
|
39
|
+
return [null, options.url];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// get debugger url
|
|
43
|
+
const [err, debuggerUrl] = await getDebuggerUrl(options);
|
|
44
|
+
if (err) {
|
|
45
|
+
return [err];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return [null, debuggerUrl];
|
|
49
|
+
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const getCDPSession = async (options) => {
|
|
53
|
+
|
|
54
|
+
if (options.session) {
|
|
55
|
+
return [null, options.session];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// create session
|
|
59
|
+
const [err, url] = await getCDPUrl(options);
|
|
60
|
+
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
|
|
63
|
+
if (err) {
|
|
64
|
+
resolve([err]);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const timeoutId = setTimeout(() => {
|
|
69
|
+
resolve([new Error(`Timeout to connect: ${url}`)]);
|
|
70
|
+
}, options.timeout);
|
|
71
|
+
|
|
72
|
+
Util.logDebug(`Connect to ${url}`);
|
|
73
|
+
const ws = new WebSocket(url, [], {
|
|
74
|
+
maxPayload: 256 * 1024 * 1024,
|
|
75
|
+
perMessageDeflate: false,
|
|
76
|
+
followRedirects: true,
|
|
77
|
+
... options.ws
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
ws.once('error', (wsErr) => {
|
|
81
|
+
clearTimeout(timeoutId);
|
|
82
|
+
resolve([wsErr]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
ws.once('open', () => {
|
|
86
|
+
clearTimeout(timeoutId);
|
|
87
|
+
Util.logDebug(`${EC.green('Connected')} ${url}`);
|
|
88
|
+
const session = new CDPSession(ws);
|
|
89
|
+
resolve([null, session]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
const CDPClient = async (cdpOptions) => {
|
|
99
|
+
|
|
100
|
+
const defaultOptions = {
|
|
101
|
+
session: null,
|
|
102
|
+
url: null,
|
|
103
|
+
port: 9222,
|
|
104
|
+
host: 'localhost',
|
|
105
|
+
secure: false,
|
|
106
|
+
target: (targets) => {
|
|
107
|
+
// defaults to first page
|
|
108
|
+
const page = targets.find((it) => it.webSocketDebuggerUrl && it.type === 'page');
|
|
109
|
+
if (page) {
|
|
110
|
+
return page;
|
|
111
|
+
}
|
|
112
|
+
return targets.find((it) => it.webSocketDebuggerUrl);
|
|
113
|
+
},
|
|
114
|
+
ws: {},
|
|
115
|
+
timeout: 10 * 1000
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if (typeof cdpOptions === 'string') {
|
|
119
|
+
cdpOptions = {
|
|
120
|
+
url: cdpOptions
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const options = {
|
|
125
|
+
... defaultOptions,
|
|
126
|
+
... cdpOptions
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const [err, session] = await getCDPSession(options);
|
|
130
|
+
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
|
|
133
|
+
if (err) {
|
|
134
|
+
Util.logError(err);
|
|
135
|
+
resolve();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const helper = new CoverageClient(session);
|
|
140
|
+
resolve(helper);
|
|
141
|
+
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
module.exports = CDPClient;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const { EventEmitter } = require('events');
|
|
2
|
+
|
|
3
|
+
class CDPSession extends EventEmitter {
|
|
4
|
+
constructor(ws) {
|
|
5
|
+
super();
|
|
6
|
+
this.ws = ws;
|
|
7
|
+
this.requestId = 1;
|
|
8
|
+
this.requestCache = new Map();
|
|
9
|
+
ws.on('message', (data, isBinary) => {
|
|
10
|
+
|
|
11
|
+
const message = JSON.parse(data);
|
|
12
|
+
// console.log(message);
|
|
13
|
+
|
|
14
|
+
const { id, method } = message;
|
|
15
|
+
if (id) {
|
|
16
|
+
const request = this.requestCache.get(id);
|
|
17
|
+
this.requestCache.delete(id);
|
|
18
|
+
if (request) {
|
|
19
|
+
request.resolve(message.result);
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (method) {
|
|
25
|
+
this.emit(method, message.params, message.sessionId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
send(method, params) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
if (!this.ws) {
|
|
34
|
+
reject(new Error('Invalid websocket'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const id = this.requestId++;
|
|
38
|
+
const message = {
|
|
39
|
+
id,
|
|
40
|
+
method,
|
|
41
|
+
params: params || {}
|
|
42
|
+
};
|
|
43
|
+
this.ws.send(JSON.stringify(message), (err) => {
|
|
44
|
+
if (err) {
|
|
45
|
+
reject(err);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.requestCache.set(id, {
|
|
49
|
+
resolve
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
detach() {
|
|
56
|
+
if (this.ws) {
|
|
57
|
+
this.ws.terminate();
|
|
58
|
+
this.ws = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = CDPSession;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
const Util = require('../utils/util.js');
|
|
2
|
+
|
|
3
|
+
const bindEvents = (target, events) => {
|
|
4
|
+
Object.keys(events).forEach((eventType) => {
|
|
5
|
+
target.on(eventType, events[eventType]);
|
|
6
|
+
});
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const unbindEvents = (target, events) => {
|
|
10
|
+
Object.keys(events).forEach((eventType) => {
|
|
11
|
+
target.off(eventType, events[eventType]);
|
|
12
|
+
});
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// keep same ranges with playwright
|
|
16
|
+
// eslint-disable-next-line complexity
|
|
17
|
+
const convertToDisjointRanges = (nestedRanges) => {
|
|
18
|
+
const points = [];
|
|
19
|
+
for (const range of nestedRanges) {
|
|
20
|
+
points.push({
|
|
21
|
+
offset: range.start,
|
|
22
|
+
type: 0,
|
|
23
|
+
range
|
|
24
|
+
});
|
|
25
|
+
points.push({
|
|
26
|
+
offset: range.end,
|
|
27
|
+
type: 1,
|
|
28
|
+
range
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// Sort points to form a valid parenthesis sequence.
|
|
32
|
+
points.sort((a, b) => {
|
|
33
|
+
// Sort with increasing offsets.
|
|
34
|
+
if (a.offset !== b.offset) {
|
|
35
|
+
return a.offset - b.offset;
|
|
36
|
+
}
|
|
37
|
+
// All "end" points should go before "start" points.
|
|
38
|
+
if (a.type !== b.type) {
|
|
39
|
+
return b.type - a.type;
|
|
40
|
+
}
|
|
41
|
+
const aLength = a.range.end - a.range.start;
|
|
42
|
+
const bLength = b.range.end - b.range.start;
|
|
43
|
+
// For two "start" points, the one with longer range goes first.
|
|
44
|
+
if (a.type === 0) {
|
|
45
|
+
return bLength - aLength;
|
|
46
|
+
}
|
|
47
|
+
// For two "end" points, the one with shorter range goes first.
|
|
48
|
+
return aLength - bLength;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const hitCountStack = [];
|
|
52
|
+
const results = [];
|
|
53
|
+
let lastOffset = 0;
|
|
54
|
+
// Run scanning line to intersect all ranges.
|
|
55
|
+
for (const point of points) {
|
|
56
|
+
if (hitCountStack.length && lastOffset < point.offset && hitCountStack[hitCountStack.length - 1] > 0) {
|
|
57
|
+
const lastResult = results.length ? results[results.length - 1] : null;
|
|
58
|
+
if (lastResult && lastResult.end === lastOffset) {
|
|
59
|
+
lastResult.end = point.offset;
|
|
60
|
+
} else {
|
|
61
|
+
results.push({
|
|
62
|
+
start: lastOffset,
|
|
63
|
+
end: point.offset
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
lastOffset = point.offset;
|
|
68
|
+
if (point.type === 0) {
|
|
69
|
+
hitCountStack.push(point.range.count);
|
|
70
|
+
} else {
|
|
71
|
+
hitCountStack.pop();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Filter out empty ranges.
|
|
75
|
+
return results.filter((range) => range.end - range.start > 1);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
class CoverageClient {
|
|
79
|
+
constructor(session) {
|
|
80
|
+
this.session = session;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// =================================================================================================
|
|
84
|
+
async startJSCoverage() {
|
|
85
|
+
if (!this.session || this.enabledJS) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.enabledJS = true;
|
|
89
|
+
this.scriptSources = new Map();
|
|
90
|
+
this.jsEvents = {
|
|
91
|
+
'Debugger.scriptParsed': (params) => {
|
|
92
|
+
const { scriptId } = params;
|
|
93
|
+
this.session.send('Debugger.getScriptSource', {
|
|
94
|
+
scriptId
|
|
95
|
+
}).then((res) => {
|
|
96
|
+
this.scriptSources.set(scriptId, res && res.scriptSource);
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
'Debugger.paused': () => {
|
|
100
|
+
this.send('Debugger.resume');
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
bindEvents(this.session, this.jsEvents);
|
|
105
|
+
|
|
106
|
+
await this.session.send('Debugger.enable');
|
|
107
|
+
await this.session.send('Debugger.setSkipAllPauses', {
|
|
108
|
+
skip: true
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await this.session.send('Profiler.enable');
|
|
112
|
+
await this.session.send('Profiler.startPreciseCoverage', {
|
|
113
|
+
callCount: true,
|
|
114
|
+
detailed: true
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Util.logDebug('startJSCoverage');
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async stopJSCoverage() {
|
|
122
|
+
if (!this.session || !this.enabledJS) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const profileResponse = await this.session.send('Profiler.takePreciseCoverage');
|
|
127
|
+
await this.session.send('Profiler.stopPreciseCoverage');
|
|
128
|
+
await this.session.send('Profiler.disable');
|
|
129
|
+
// await this.session.send('Debugger.disable');
|
|
130
|
+
|
|
131
|
+
unbindEvents(this.session, this.jsEvents);
|
|
132
|
+
this.jsEvents = null;
|
|
133
|
+
|
|
134
|
+
const jsCoverage = profileResponse.result;
|
|
135
|
+
// console.log('coverageList', coverageList);
|
|
136
|
+
|
|
137
|
+
jsCoverage.forEach((entry) => {
|
|
138
|
+
// anonymous url
|
|
139
|
+
entry.url = entry.url || '';
|
|
140
|
+
// add source
|
|
141
|
+
const source = this.scriptSources.get(entry.scriptId);
|
|
142
|
+
if (!source) {
|
|
143
|
+
Util.logDebug(`Not found js source: ${entry.url}`);
|
|
144
|
+
}
|
|
145
|
+
entry.source = source || '';
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
this.scriptSources.clear();
|
|
149
|
+
this.enabledJS = false;
|
|
150
|
+
|
|
151
|
+
return jsCoverage;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// =================================================================================================
|
|
155
|
+
|
|
156
|
+
async startCSSCoverage() {
|
|
157
|
+
if (!this.session || this.enabledCSS) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.enabledCSS = true;
|
|
161
|
+
this.styleEntries = new Map();
|
|
162
|
+
|
|
163
|
+
this.cssEvents = {
|
|
164
|
+
'CSS.styleSheetAdded': (e) => {
|
|
165
|
+
const { sourceURL, styleSheetId } = e.header;
|
|
166
|
+
|
|
167
|
+
// anonymous url
|
|
168
|
+
const url = sourceURL || '';
|
|
169
|
+
this.session.send('CSS.getStyleSheetText', {
|
|
170
|
+
styleSheetId
|
|
171
|
+
}).then((res) => {
|
|
172
|
+
// add source
|
|
173
|
+
const text = res && res.text;
|
|
174
|
+
if (!text) {
|
|
175
|
+
Util.logDebug(`Not found css source: ${url}`);
|
|
176
|
+
}
|
|
177
|
+
this.styleEntries.set(styleSheetId, {
|
|
178
|
+
url,
|
|
179
|
+
text: text || '',
|
|
180
|
+
ranges: []
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
bindEvents(this.session, this.cssEvents);
|
|
187
|
+
|
|
188
|
+
await this.session.send('DOM.enable');
|
|
189
|
+
await this.session.send('CSS.enable');
|
|
190
|
+
await this.session.send('CSS.startRuleUsageTracking');
|
|
191
|
+
|
|
192
|
+
// Util.logDebug('startCSSCoverage');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async stopCSSCoverage() {
|
|
196
|
+
if (!this.session || !this.enabledCSS) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const ruleTrackingResponse = await this.session.send('CSS.stopRuleUsageTracking');
|
|
201
|
+
await this.session.send('CSS.disable');
|
|
202
|
+
await this.session.send('DOM.disable');
|
|
203
|
+
|
|
204
|
+
unbindEvents(this.session, this.cssEvents);
|
|
205
|
+
this.cssEvents = null;
|
|
206
|
+
|
|
207
|
+
const cssCoverage = [];
|
|
208
|
+
|
|
209
|
+
if (ruleTrackingResponse) {
|
|
210
|
+
for (const usage of ruleTrackingResponse.ruleUsage) {
|
|
211
|
+
const entry = this.styleEntries.get(usage.styleSheetId);
|
|
212
|
+
if (entry) {
|
|
213
|
+
entry.ranges.push({
|
|
214
|
+
start: usage.startOffset,
|
|
215
|
+
end: usage.endOffset,
|
|
216
|
+
count: usage.used ? 1 : 0
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
this.styleEntries.forEach((entry) => {
|
|
221
|
+
entry.ranges = convertToDisjointRanges(entry.ranges);
|
|
222
|
+
cssCoverage.push(entry);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.styleEntries.clear();
|
|
227
|
+
this.enabledCSS = false;
|
|
228
|
+
|
|
229
|
+
return cssCoverage;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// =================================================================================================
|
|
233
|
+
|
|
234
|
+
async startCoverage() {
|
|
235
|
+
await Promise.all([
|
|
236
|
+
this.startJSCoverage(),
|
|
237
|
+
this.startCSSCoverage()
|
|
238
|
+
]);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async stopCoverage() {
|
|
242
|
+
const [jsCoverage, cssCoverage] = await Promise.all([
|
|
243
|
+
this.stopJSCoverage(),
|
|
244
|
+
this.stopCSSCoverage()
|
|
245
|
+
]);
|
|
246
|
+
return [... jsCoverage, ... cssCoverage];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// =================================================================================================
|
|
250
|
+
// write the coverage started by NODE_V8_COVERAGE to disk on demand
|
|
251
|
+
async writeCoverage() {
|
|
252
|
+
// // enable and start v8 coverage
|
|
253
|
+
await this.session.send('Runtime.enable');
|
|
254
|
+
|
|
255
|
+
// write the coverage started by NODE_V8_COVERAGE to disk on demand
|
|
256
|
+
const res = await this.session.send('Runtime.evaluate', {
|
|
257
|
+
expression: 'new Promise(resolve=>{ require("v8").takeCoverage(); resolve(process.env.NODE_V8_COVERAGE); })',
|
|
258
|
+
includeCommandLineAPI: true,
|
|
259
|
+
returnByValue: true,
|
|
260
|
+
awaitPromise: true
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await this.session.send('Runtime.disable');
|
|
264
|
+
|
|
265
|
+
return res.result.value;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// =================================================================================================
|
|
269
|
+
async close() {
|
|
270
|
+
if (!this.session) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
await this.session.detach();
|
|
275
|
+
this.session = null;
|
|
276
|
+
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
module.exports = CoverageClient;
|