monocart-coverage-reports 2.2.1 → 2.3.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
@@ -18,8 +18,11 @@
18
18
  * [Compare Workflows](#compare-workflows)
19
19
  * [Collecting Istanbul Coverage Data](#collecting-istanbul-coverage-data)
20
20
  * [Collecting V8 Coverage Data](#collecting-v8-coverage-data)
21
+ * [Manually Resolve the Sourcemap](#manually-resolve-the-sourcemap)
22
+ * [Collecting Raw V8 Coverage Data with Puppeteer](#collecting-raw-v8-coverage-data-with-puppeteer)
21
23
  * [Node.js V8 Coverage Report for Server Side](#nodejs-v8-coverage-report-for-server-side)
22
24
  * [Multiprocessing Support](#multiprocessing-support)
25
+ * [Merge Coverage Reports](#merge-coverage-reports)
23
26
  * [Integration](#integration)
24
27
  * [Ignoring Uncovered Codes](#ignoring-uncovered-codes)
25
28
  * [Chromium Coverage API](#chromium-coverage-api)
@@ -64,11 +67,12 @@ console.log(coverageResults.summary);
64
67
 
65
68
  - `v8-json` (V8 data only)
66
69
  - [V8 coverage-report.json](https://cenfun.github.io/monocart-coverage-reports/v8-and-istanbul/coverage-report.json)
67
- - `console-summary` shows coverage summary in console
70
+ - `codecov` (V8 data only)
71
+ - coverage data for [Codecov](https://docs.codecov.com/docs/codecov-custom-coverage-format), see [example](https://app.codecov.io/github/cenfun/monocart-coverage-reports)
68
72
 
69
73
  ![](test/console-summary.png)
70
74
 
71
- > Following are [istanbul reports](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib)
75
+ > Istanbul [build-in reports](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib)
72
76
  - `clover`
73
77
  - `cobertura`
74
78
  - `html`
@@ -88,11 +92,41 @@ console.log(coverageResults.summary);
88
92
  - `text-lcov`
89
93
  - `text-summary`
90
94
 
95
+ > Other reports
96
+ - `console-summary` shows coverage summary in console
97
+ - `raw` only keep all original data, which can be used for other reports input with `inputDir`
98
+ - see [Merge Coverage Reports](#merge-coverage-reports)
99
+
100
+ - Custom Reporter
101
+ ```js
102
+ {
103
+ reports: [
104
+ [path.resolve('./test/custom-istanbul-reporter.js'), {
105
+ type: 'istanbul',
106
+ file: 'custom-istanbul-coverage.text'
107
+ }],
108
+ [path.resolve('./test/custom-v8-reporter.js'), {
109
+ type: 'v8',
110
+ outputFile: 'custom-v8-coverage.json'
111
+ }],
112
+ [path.resolve('./test/custom-v8-reporter.mjs'), {
113
+ type: 'both'
114
+ }]
115
+ ]
116
+ }
117
+ ```
118
+ - istanbul custom reporter
119
+ > example: [./test/custom-istanbul-reporter.js](./test/custom-istanbul-reporter.js), see [istanbul built-in reporters' implementation](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib) for reference,
120
+ - v8 custom reporter
121
+ > example: [./test/custom-v8-reporter.js](./test/custom-v8-reporter.js)
122
+
123
+ ### Multiple Reports:
91
124
  ```js
92
125
  const MCR = require('monocart-coverage-reports');
93
126
  const options = {
94
127
  outputDir: './coverage-reports',
95
128
  reports: [
129
+ // build-in reports
96
130
  ['console-summary'],
97
131
  ['v8'],
98
132
  ['html', {
@@ -101,7 +135,18 @@ const options = {
101
135
  ['json', {
102
136
  file: 'my-json-file.json'
103
137
  }],
104
- 'lcovonly'
138
+ 'lcovonly',
139
+
140
+ // custom reports
141
+ // Specify reporter name with the NPM package
142
+ ["custom-reporter-1"],
143
+ ["custom-reporter-2", {
144
+ type: "istanbul",
145
+ option: "value"
146
+ }],
147
+ // Specify reporter name with local path
148
+ ['/absolute/path/to/custom-reporter.js']
149
+
105
150
  ]
106
151
  }
107
152
  const coverageReport = MCR(options);
@@ -248,13 +293,48 @@ mcr "node ./test/test-node-env.js" -c test/cli-options.js
248
293
  - [rollup](https://rollupjs.org/configuration-options/): `sourcemap: true`
249
294
  - [vite](https://vitejs.dev/config/build-options.html): `sourcemap: true` and `minify: false`
250
295
  - [esbuild](https://esbuild.github.io/api/): `sourcemap: true` and `minify: false`
296
+ - [Manually Resolve the Sourcemap](#manually-resolve-the-sourcemap)
251
297
  - Browser (Chromium Only)
252
298
  > Collecting coverage data with [Chromium Coverage API](#chromium-coverage-api):
253
299
  - [Playwright example](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-v8.js), and [anonymous](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-anonymous.js), [css](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-css.js)
254
- - [Puppeteer example](https://github.com/cenfun/monocart-coverage-reports/blob/main/test/test-puppeteer.js)
300
+ - see [Collecting Raw V8 Coverage Data with Puppeteer](#collecting-raw-v8-coverage-data-with-puppeteer)
255
301
  - Node.js
256
- - see following [Node.js V8 Coverage Report for Server Side](#nodejs-v8-coverage-report-for-server-side)
302
+ - see [Node.js V8 Coverage Report for Server Side](#nodejs-v8-coverage-report-for-server-side)
257
303
 
304
+ ## Manually Resolve the Sourcemap
305
+ > If the `js` file is loaded with `addScriptTag` [API](https://playwright.dev/docs/api/class-page#page-add-script-tag), then its sourcemap file may not work. You can try to manually read the sourcemap file before the coverage data is added to the report.
306
+ ```js
307
+ const jsCoverage = await page.coverage.stopJSCoverage();
308
+ jsCoverage.forEach((entry) => {
309
+ // read sourcemap for the my-dist.js manually
310
+ if (entry.url.endsWith('my-dist.js')) {
311
+ entry.sourceMap = JSON.parse(fs.readFileSync('dist/my-dist.js.map').toString('utf-8'));
312
+ }
313
+ });
314
+
315
+ await MCR(coverageOptions).add(jsCoverage);
316
+
317
+ ```
318
+
319
+ ## Collecting Raw V8 Coverage Data with Puppeteer
320
+ > Puppeteer does not provide raw v8 coverage data by default. A simple conversion is required, see example: [./test/test-puppeteer.js](./test/test-puppeteer.js)
321
+ ```js
322
+ await page.coverage.startJSCoverage({
323
+ // provide raw v8 coverage data
324
+ includeRawScriptCoverage: true
325
+ });
326
+
327
+ await page.goto(url);
328
+
329
+ const jsCoverage = await page.coverage.stopJSCoverage();
330
+ const rawV8CoverageData = jsCoverage.map((it) => {
331
+ // Convert to raw v8 coverage format
332
+ return {
333
+ source: it.text,
334
+ ... it.rawScriptCoverage
335
+ };
336
+ }
337
+ ```
258
338
 
259
339
  ## Node.js V8 Coverage Report for Server Side
260
340
  Possible solutions:
@@ -317,9 +397,65 @@ const coverageResults = await coverageReport.generate();
317
397
  console.log(coverageResults.summary);
318
398
  ```
319
399
 
400
+ ## Merge Coverage Reports
401
+ The following usage scenarios may require merging coverage reports:
402
+ - When the code is executed in different environments, like Node.js Server Side and browser Client Side (Next.js for instance). Each environment may generate its own coverage report. Merging them can give a more comprehensive view of the test coverage.
403
+ - When the code is subjected to different kinds of testing. For example, unit tests with Jest might cover certain parts of the code, while end-to-end tests with Playwright might cover other parts. Merging these different coverage reports can provide a holistic view of what code has been tested.
404
+ - When tests are run on different machines or different shards, each might produce its own coverage report. Merging these can give a complete picture of the test coverage across all machines or shards.
405
+
406
+ First, using the `raw` report to export the original coverage data to the specified directory.
407
+ ```js
408
+ const coverageOptions = {
409
+ name: 'My Unit Test Coverage Report',
410
+ outputDir: "./coverage-reports/unit",
411
+ reports: [
412
+ ['raw', {
413
+ // relative path will be "./coverage-reports/unit/raw"
414
+ outputDir: "raw"
415
+ }],
416
+ ['v8'],
417
+ ['console-summary']
418
+ ]
419
+ };
420
+ ```
421
+ Then, after all the tests are completed, generate a merged report with option `inputDir`:
422
+ ```js
423
+ import {CoverageReport} from 'monocart-coverage-reports';
424
+ const coverageOptions = {
425
+ name: 'My Merged Coverage Report',
426
+ inputDir: [
427
+ './coverage-reports/unit/raw',
428
+ './coverage-reports/e2e/raw'
429
+ ],
430
+ outputDir: './coverage-reports/merged',
431
+ reports: [
432
+ ['v8'],
433
+ ['console-summary']
434
+ ]
435
+ };
436
+ await new CoverageReport(coverageOptions).generate();
437
+ ```
438
+ If the source file comes from the sourcemap, then its path is a virtual path. Using the `sourcePath` option to convert it.
439
+ ```js
440
+ const coverageOptions = {
441
+ sourcePath: (filePath) => {
442
+ // Remove the virtual prefix
443
+ const list = ['my-dist-file1/', 'my-dist-file2/'];
444
+ for (const str of list) {
445
+ if (filePath.startsWith(str)) {
446
+ return filePath.slice(str.length);
447
+ }
448
+ }
449
+ return filePath;
450
+ }
451
+ };
452
+ ```
453
+ see example: [./test/test-merge.js](./test/test-merge.js)
454
+
320
455
  ## Integration
321
456
  - [monocart-reporter](https://cenfun.github.io/monocart-reporter/) - Test reporter for [Playwright](https://github.com/microsoft/playwright)
322
- - [vitest-monocart-coverage](https://github.com/cenfun/vitest-monocart-coverage) - Integration with [Vitest](https://github.com/vitest-dev/vitest) coverage
457
+ - [jest-monocart-coverage](https://github.com/cenfun/jest-monocart-coverage) - Integration with [Jest](https://github.com/jestjs/jest/) for coverage reports
458
+ - [vitest-monocart-coverage](https://github.com/cenfun/vitest-monocart-coverage) - Integration with [Vitest](https://github.com/vitest-dev/vitest) for coverage reports
323
459
 
324
460
  ## Ignoring Uncovered Codes
325
461
  To ignore codes, use the special comment which starts with `v8 ignore `:
@@ -392,26 +528,6 @@ export type V8CoverageData = ScriptCoverage[];
392
528
  ```
393
529
  see devtools-protocol [ScriptCoverage](https://chromedevtools.github.io/devtools-protocol/tot/Profiler/#type-ScriptCoverage) and [v8-coverage](https://github.com/bcoe/v8-coverage)
394
530
 
395
- ### Collect raw v8 coverage data with Puppeteer
396
- ```js
397
- await page.coverage.startJSCoverage({
398
- // provide raw v8 coverage data
399
- includeRawScriptCoverage: true
400
- });
401
-
402
- await page.goto(url);
403
-
404
- const jsCoverage = await page.coverage.stopJSCoverage();
405
- const rawV8CoverageData = jsCoverage.map((it) => {
406
- // Convert to raw v8 coverage format
407
- return {
408
- source: it.text,
409
- ... it.rawScriptCoverage
410
- };
411
- }
412
- ```
413
- see example: [./test/test-puppeteer.js](./test/test-puppeteer.js)
414
-
415
531
  ## How to convert V8 to Istanbul
416
532
  ### Using [v8-to-istanbul](https://github.com/istanbuljs/v8-to-istanbul)
417
533
  It is a popular library which is used to convert V8 coverage format to istanbul's coverage format. Most test frameworks are using it, such as [Jest](https://github.com/jestjs/jest/), [Vitest](https://github.com/vitest-dev/vitest), but it has two major problems:
package/lib/cli.js CHANGED
@@ -36,12 +36,6 @@ const initOptions = (cliOptions) => {
36
36
  }
37
37
  }
38
38
 
39
- // init reports to list with `,`
40
- const reports = cliOptions.reports;
41
- if (reports) {
42
- cliOptions.reports = reports.split(',');
43
- }
44
-
45
39
  // report options
46
40
  const options = {
47
41
  ... defaultOptions
@@ -36,6 +36,17 @@ const collectAstInfo = (ast, state) => {
36
36
  const functions = [];
37
37
  const branchMap = new Map();
38
38
 
39
+ // root function branches
40
+ const functionRoot = state.functionRoot;
41
+ if (functionRoot) {
42
+ const rootFunctionInfo = {
43
+ count: functionRoot.count,
44
+ range: functionRoot
45
+ };
46
+ createBranches(ast, rootFunctionInfo, branchMap);
47
+ }
48
+
49
+ // functions branches
39
50
  Util.visitAst(ast, {
40
51
 
41
52
  VariableDeclarator(node, parents) {
@@ -101,15 +112,15 @@ const collectAstInfo = (ast, state) => {
101
112
  const generateCountState = (coverageList) => {
102
113
  const functionRanges = [];
103
114
  const functionNameMap = new Map();
115
+ let functionRoot;
104
116
  coverageList.forEach((block) => {
105
- const { functionName, ranges } = block;
117
+ const {
118
+ functionName, ranges, root
119
+ } = block;
106
120
 
107
121
  // first one is function coverage info
108
122
  const functionRange = ranges[0];
109
123
  functionRange.functionName = functionName;
110
- if (functionName) {
111
- functionNameMap.set(functionRange.startOffset + functionName.length, functionRange);
112
- }
113
124
 
114
125
  // blocks
115
126
  const len = ranges.length;
@@ -128,6 +139,15 @@ const generateCountState = (coverageList) => {
128
139
  functionRange.blockRanges = blockRanges;
129
140
  }
130
141
 
142
+ // root function
143
+ if (root) {
144
+ functionRoot = functionRange;
145
+ return;
146
+ }
147
+
148
+ if (functionName) {
149
+ functionNameMap.set(functionRange.startOffset + functionName.length, functionRange);
150
+ }
131
151
  functionRanges.push(functionRange);
132
152
 
133
153
  });
@@ -146,7 +166,8 @@ const generateCountState = (coverageList) => {
146
166
  return {
147
167
  functionMap,
148
168
  functionNameMap,
149
- functionRanges
169
+ functionRanges,
170
+ functionRoot
150
171
  };
151
172
 
152
173
  };
@@ -81,6 +81,14 @@ const resolveSourceMap = (data) => {
81
81
 
82
82
  const saveSourceFile = async (filePath, data) => {
83
83
  await Util.writeFile(filePath, JSON.stringify(data));
84
+
85
+ // save source and sourcemap file for debug
86
+ // https://evanw.github.io/source-map-visualization
87
+ // if (data.sourceMap) {
88
+ // await Util.writeFile(`${filePath}.js`, data.source);
89
+ // await Util.writeFile(`${filePath}.js.map`, JSON.stringify(data.sourceMap));
90
+ // }
91
+
84
92
  };
85
93
 
86
94
  const collectSourceMaps = async (v8list, options) => {
@@ -105,15 +113,19 @@ const collectSourceMaps = async (v8list, options) => {
105
113
  continue;
106
114
  }
107
115
 
116
+ // save source and sourceMap to separated json file
108
117
  const sourceData = {
109
118
  url,
110
119
  id,
111
- // source,
112
- source: convertSourceMap.removeComments(source),
113
- // could be existed
120
+ source,
114
121
  sourceMap
115
122
  };
116
123
 
124
+ // remove comments if not debug
125
+ if (Util.loggingType !== 'debug') {
126
+ sourceData.source = convertSourceMap.removeComments(source);
127
+ }
128
+
117
129
  // check sourceMap only for js
118
130
  if (type === 'js' && !sourceData.sourceMap) {
119
131
  // from inline sync