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 CHANGED
@@ -7,7 +7,7 @@
7
7
  ![](https://img.shields.io/github/actions/workflow/status/cenfun/monocart-coverage-reports/static.yml)
8
8
 
9
9
 
10
- > Code coverage tool to generate native [V8](https://v8.dev/blog/javascript-code-coverage) reports or [Istanbul](https://istanbul.js.org/) reports.
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
- - 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/)
82
- - 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/)
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
  ![](./assets/v8.gif)
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;