monocart-coverage-reports 2.7.9 → 2.8.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 +47 -56
- package/README.zh-Hans.md +1035 -0
- package/lib/cli.js +3 -0
- package/lib/converter/converter.js +12 -4
- package/lib/converter/find-original-range.js +9 -0
- package/lib/default/options.js +3 -0
- package/lib/generate.js +20 -5
- package/lib/index.d.ts +11 -6
- package/lib/index.js +3 -16
- package/lib/packages/monocart-coverage-v8.js +1 -1
- package/lib/packages/monocart-coverage-vendor.js +32 -32
- package/lib/platform/share.js +0 -4
- package/lib/utils/util.js +54 -1
- package/lib/v8/v8.js +12 -4
- package/package.json +101 -100
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
# Monocart Coverage Reports
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/monocart-coverage-reports)
|
|
4
|
+
[](https://www.npmjs.com/package/monocart-coverage-reports)
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
🌐 [English](README.md) | 简体中文
|
|
10
|
+
|
|
11
|
+
> JS代码覆盖率工具,用来生成原生的[V8](https://v8.dev/blog/javascript-code-coverage)或者[Istanbul](https://istanbul.js.org/)代码覆盖率报告
|
|
12
|
+
|
|
13
|
+
* [用法](#usage)
|
|
14
|
+
* [选项配置](#options)
|
|
15
|
+
* [所有支持的报告类型](#available-reports)
|
|
16
|
+
* [比较两种报告](#compare-reports)
|
|
17
|
+
* [如何收集Istanbul覆盖率数据](#collecting-istanbul-coverage-data)
|
|
18
|
+
* [如何收集V8覆盖率数据](#collecting-v8-coverage-data)
|
|
19
|
+
- [用Playwright](#collecting-v8-coverage-data-with-playwright)
|
|
20
|
+
- [用Puppeteer](#collecting-raw-v8-coverage-data-with-puppeteer)
|
|
21
|
+
- [从Node.js](#collecting-v8-coverage-data-from-nodejs)
|
|
22
|
+
- [使用`CDPClient`API](#collecting-v8-coverage-data-with-cdpclient-api)
|
|
23
|
+
- [参考V8覆盖率的API](#v8-coverage-data-api)
|
|
24
|
+
* [使用 `entryFilter` 和 `sourceFilter` 来过滤V8覆盖率数据](#using-entryfilter-and-sourcefilter-to-filter-the-results-for-v8-report)
|
|
25
|
+
* [使用 `sourcePath` 修改源文件路径](#resolve-sourcepath-for-the-source-files)
|
|
26
|
+
* [为未测试的文件添加空的覆盖率报告](#adding-empty-coverage-for-untested-files)
|
|
27
|
+
* [onEnd回调函数](#onend-hook)
|
|
28
|
+
* [如何忽略未覆盖的代码](#ignoring-uncovered-codes)
|
|
29
|
+
* [多进程支持](#multiprocessing-support)
|
|
30
|
+
* [如何使用CLI命令行](#command-line)
|
|
31
|
+
* [如何加载配置文件](#config-file)
|
|
32
|
+
* [如何合并覆盖率报告](#merge-coverage-reports)
|
|
33
|
+
- [自动合并](#automatic-merging)
|
|
34
|
+
- [手动合并](#manual-merging)
|
|
35
|
+
* [常见问题](#common-issues)
|
|
36
|
+
- [Unexpected coverage](#unexpected-coverage)
|
|
37
|
+
- [Unparsable source](#unparsable-source)
|
|
38
|
+
* [如何调试覆盖率数据和查看sourcemap](#debug-for-coverage-and-sourcemap)
|
|
39
|
+
* [如何跟其他框架集成](#integration-with-any-testing-framework)
|
|
40
|
+
* [集成的例子](#integration-examples)
|
|
41
|
+
- [Playwright](#playwright)
|
|
42
|
+
- [CodeceptJS](#codeceptjs)
|
|
43
|
+
- [Jest](#jest)
|
|
44
|
+
- [Vitest](#vitest)
|
|
45
|
+
- [Puppeteer](#puppeteer)
|
|
46
|
+
- [Cypress](#cypress)
|
|
47
|
+
- [WebdriverIO](#webdriverio)
|
|
48
|
+
- [Storybook Test Runner](#storybook-test-runner)
|
|
49
|
+
- [TestCafe](#testcafe)
|
|
50
|
+
- [Selenium Webdriver](#selenium-webdriver)
|
|
51
|
+
- [Mocha](#mocha)
|
|
52
|
+
- [tsx](#tsx)
|
|
53
|
+
- [ts-node](#ts-node)
|
|
54
|
+
- [AVA](#ava)
|
|
55
|
+
- [Codecov](#codecov)
|
|
56
|
+
- [Coveralls](#coveralls)
|
|
57
|
+
- [Sonar Cloud](#sonar-cloud)
|
|
58
|
+
* [Contributing](#contributing)
|
|
59
|
+
* [更新日志](CHANGELOG.md)
|
|
60
|
+
* [感谢](#thanks)
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
> 推荐使用 [Node.js 20+](https://nodejs.org/).
|
|
64
|
+
- 安装
|
|
65
|
+
```sh
|
|
66
|
+
npm install monocart-coverage-reports
|
|
67
|
+
```
|
|
68
|
+
- API
|
|
69
|
+
```js
|
|
70
|
+
const MCR = require('monocart-coverage-reports');
|
|
71
|
+
const mcr = MCR({
|
|
72
|
+
name: 'My Coverage Report - 2024-02-28',
|
|
73
|
+
outputDir: './coverage-reports',
|
|
74
|
+
reports: ["v8", "console-details"],
|
|
75
|
+
cleanCache: true
|
|
76
|
+
});
|
|
77
|
+
await mcr.add(coverageData);
|
|
78
|
+
await mcr.generate();
|
|
79
|
+
```
|
|
80
|
+
也可以使用ESM的 `import` 然后加载[配置文件](#config-file)
|
|
81
|
+
```js
|
|
82
|
+
import { CoverageReport } from 'monocart-coverage-reports';
|
|
83
|
+
const mcr = new CoverageReport();
|
|
84
|
+
await mcr.loadConfig();
|
|
85
|
+
```
|
|
86
|
+
参见 [多进程支持](#multiprocessing-support)
|
|
87
|
+
|
|
88
|
+
- CLI
|
|
89
|
+
```sh
|
|
90
|
+
mcr node my-app.js -r v8,console-details
|
|
91
|
+
```
|
|
92
|
+
参见 [命令行](#command-line)
|
|
93
|
+
|
|
94
|
+
## Options
|
|
95
|
+
- 默认选项: [lib/default/options.js](./lib/default/options.js)
|
|
96
|
+
- 选项的类型描述,见 `CoverageReportOptions` [lib/index.d.ts](./lib/index.d.ts)
|
|
97
|
+
- [配置文件](#config-file)
|
|
98
|
+
|
|
99
|
+
## Available Reports
|
|
100
|
+
|
|
101
|
+
> 内置V8报告(仅V8格式数据支持):
|
|
102
|
+
|
|
103
|
+
- `v8`
|
|
104
|
+
- 推荐使用:
|
|
105
|
+
- 全新的原生V8覆盖率报告界面,更好的用户体验
|
|
106
|
+
- 支持原生的Bytes覆盖率指标
|
|
107
|
+
- 支持任何运行时代码的覆盖率(压缩后的)
|
|
108
|
+
- 支持CSS代码覆盖率(用于分析CSS的冗余代码)
|
|
109
|
+
- 对Sourcemap转换有更好的支持
|
|
110
|
+
- 预览: [V8](https://cenfun.github.io/monocart-coverage-reports/v8) and [more](https://cenfun.github.io/monocart-coverage-reports/)
|
|
111
|
+
|
|
112
|
+

|
|
113
|
+
|
|
114
|
+
- `v8-json`
|
|
115
|
+
- [V8 coverage-report.json](https://cenfun.github.io/monocart-coverage-reports/v8-and-istanbul/coverage-report.json)
|
|
116
|
+
|
|
117
|
+
> 内置Istanbul报告 (V8和Istanbul格式数据都支持):
|
|
118
|
+
|
|
119
|
+
- `clover`
|
|
120
|
+
- `cobertura`
|
|
121
|
+
- `html`
|
|
122
|
+
- [Istanbul html](https://cenfun.github.io/monocart-coverage-reports/istanbul/)
|
|
123
|
+
- [V8 to Istanbul](https://cenfun.github.io/monocart-coverage-reports/v8-and-istanbul/istanbul)
|
|
124
|
+
- `html-spa`
|
|
125
|
+
- `json`
|
|
126
|
+
- `json-summary`
|
|
127
|
+
- `lcov`
|
|
128
|
+
- `lcovonly`
|
|
129
|
+
- [V8 lcov.info](https://cenfun.github.io/monocart-coverage-reports/v8/lcov.info)
|
|
130
|
+
- [Istanbul lcov.info](https://cenfun.github.io/monocart-coverage-reports/istanbul/lcov.info)
|
|
131
|
+
- `none`
|
|
132
|
+
- `teamcity`
|
|
133
|
+
- `text`
|
|
134
|
+
- `text-lcov`
|
|
135
|
+
- `text-summary`
|
|
136
|
+
|
|
137
|
+
> 其他内置报告 (V8和Istanbul格式数据都支持):
|
|
138
|
+
|
|
139
|
+
- `codecov`
|
|
140
|
+
- 专属支持[Codecov](https://docs.codecov.com/docs/codecov-custom-coverage-format)的报告, see [example](https://app.codecov.io/github/cenfun/monocart-coverage-reports)
|
|
141
|
+
|
|
142
|
+
- `console-summary` 在控制台显示覆盖率概要
|
|
143
|
+
|
|
144
|
+

|
|
145
|
+
|
|
146
|
+
- `console-details` 在控制台显示每个文件的覆盖率概要。如果是Github actions,可以使用环境变量`FORCE_COLOR: true`来强制开启颜色支持
|
|
147
|
+
|
|
148
|
+

|
|
149
|
+
|
|
150
|
+
- `raw` 只是保存原始覆盖率数据, 用于使用`inputDir`参数来导入多个原始数据进行合并报告
|
|
151
|
+
- 参见 [合并覆盖率报告](#merge-coverage-reports)
|
|
152
|
+
|
|
153
|
+
- 自定义报告
|
|
154
|
+
```js
|
|
155
|
+
{
|
|
156
|
+
reports: [
|
|
157
|
+
[path.resolve('./test/custom-istanbul-reporter.js'), {
|
|
158
|
+
type: 'istanbul',
|
|
159
|
+
file: 'custom-istanbul-coverage.text'
|
|
160
|
+
}],
|
|
161
|
+
[path.resolve('./test/custom-v8-reporter.js'), {
|
|
162
|
+
type: 'v8',
|
|
163
|
+
outputFile: 'custom-v8-coverage.json'
|
|
164
|
+
}],
|
|
165
|
+
[path.resolve('./test/custom-v8-reporter.mjs'), {
|
|
166
|
+
type: 'both'
|
|
167
|
+
}]
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
- Istanbul自定义报告
|
|
172
|
+
> 例子: [./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.
|
|
173
|
+
- V8自定义报告
|
|
174
|
+
> 例子: [./test/custom-v8-reporter.js](./test/custom-v8-reporter.js)
|
|
175
|
+
|
|
176
|
+
### Multiple Reports:
|
|
177
|
+
如何配置多个报告
|
|
178
|
+
```js
|
|
179
|
+
const MCR = require('monocart-coverage-reports');
|
|
180
|
+
const coverageOptions = {
|
|
181
|
+
outputDir: './coverage-reports',
|
|
182
|
+
reports: [
|
|
183
|
+
// build-in reports
|
|
184
|
+
['console-summary'],
|
|
185
|
+
['v8'],
|
|
186
|
+
['html', {
|
|
187
|
+
subdir: 'istanbul'
|
|
188
|
+
}],
|
|
189
|
+
['json', {
|
|
190
|
+
file: 'my-json-file.json'
|
|
191
|
+
}],
|
|
192
|
+
'lcovonly',
|
|
193
|
+
|
|
194
|
+
// custom reports
|
|
195
|
+
// Specify reporter name with the NPM package
|
|
196
|
+
["custom-reporter-1"],
|
|
197
|
+
["custom-reporter-2", {
|
|
198
|
+
type: "istanbul",
|
|
199
|
+
key: "value"
|
|
200
|
+
}],
|
|
201
|
+
// Specify reporter name with local path
|
|
202
|
+
['/absolute/path/to/custom-reporter.js']
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
const mcr = MCR(coverageOptions);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Compare Reports
|
|
209
|
+
> 如果是V8数据格式使用Istanbul的报告,将自动从V8转换到Istanbul
|
|
210
|
+
|
|
211
|
+
| | Istanbul | V8 | V8 to Istanbul |
|
|
212
|
+
| :--------------| :------ | :------ | :---------------------- |
|
|
213
|
+
| 数据格式 | [Istanbul](https://github.com/gotwarlost/istanbul/blob/master/coverage.json.md) (Object) | [V8](#v8-coverage-data-format) (Array) | [V8](#v8-coverage-data-format) (Array) |
|
|
214
|
+
| 输出报告 | [Istanbul reports](#available-reports) | [V8 reports](#available-reports) | [Istanbul reports](#available-reports) |
|
|
215
|
+
| - Bytes 字节覆盖率 | ❌ | ✅ | ❌ |
|
|
216
|
+
| - Statements 语句覆盖率 | ✅ | ✅ | ✅ |
|
|
217
|
+
| - Branches 分支覆盖率 | ✅ | ✅ | ✅ |
|
|
218
|
+
| - Functions 函数覆盖率 | ✅ | ✅ | ✅ |
|
|
219
|
+
| - Lines 行覆盖率 | ✅ | ✅ | ✅ |
|
|
220
|
+
| - Execution counts 函数执行数 | ✅ | ✅ | ✅ |
|
|
221
|
+
| CSS 覆盖率 | ❌ | ✅ | ✅ |
|
|
222
|
+
| 压缩过的代码 | ❌ | ✅ | ❌ |
|
|
223
|
+
|
|
224
|
+
## Collecting Istanbul Coverage Data
|
|
225
|
+
- 在收集Istanbul覆盖率数据之前,需要编译源代码来安装Istanbul计数器
|
|
226
|
+
- webpack babel-loader: [babel-plugin-istanbul](https://github.com/istanbuljs/babel-plugin-istanbul), 参见例子: [webpack.config-istanbul.js](./test/build/webpack.config-istanbul.js)
|
|
227
|
+
- 官方CLI: [nyc instrument](https://github.com/istanbuljs/nyc/blob/master/docs/instrument.md) 或API: [istanbul-lib-instrument](https://github.com/istanbuljs/istanbuljs/blob/main/packages/istanbul-lib-instrument/api.md)
|
|
228
|
+
- vite: [vite-plugin-istanbul](https://github.com/ifaxity/vite-plugin-istanbul)
|
|
229
|
+
- rollup: [rollup-plugin-istanbul](https://github.com/artberri/rollup-plugin-istanbul)
|
|
230
|
+
- swc: [swc-plugin-coverage-instrument](https://github.com/kwonoj/swc-plugin-coverage-instrument)
|
|
231
|
+
|
|
232
|
+
- 从浏览器
|
|
233
|
+
- Istanbul的覆盖率数据会保存到全局的`window.__coverage__`,直接读取即可, 参见例子: [test-istanbul.js](./test/test-istanbul.js)
|
|
234
|
+
|
|
235
|
+
- 从Node.js
|
|
236
|
+
- 同理对于Node.js会保存到全局的`global.__coverage__`
|
|
237
|
+
|
|
238
|
+
- 使用CDP
|
|
239
|
+
- `getIstanbulCoverage()` 参见[`CDPClient` API](#collecting-v8-coverage-data-with-cdpclient-api)
|
|
240
|
+
|
|
241
|
+
## Collecting V8 Coverage Data
|
|
242
|
+
- 在收集V8覆盖率数据之前,需要开启构建工具的`sourcemap`支持,并且不要压缩代码
|
|
243
|
+
- [webpack](https://webpack.js.org/configuration/): `devtool: source-map` and `mode: development`, example [webpack.config-v8.js](./test/build/webpack.config-v8.js)
|
|
244
|
+
- [rollup](https://rollupjs.org/configuration-options/): `sourcemap: true` and `treeshake: false`
|
|
245
|
+
- [esbuild](https://esbuild.github.io/api/): `sourcemap: true`, `treeShaking: false` and `minify: false`
|
|
246
|
+
- [vite](https://vitejs.dev/config/build-options.html): `sourcemap: true` and `minify: false`
|
|
247
|
+
|
|
248
|
+
- 浏览器 (仅支持基于Chromium的浏览器)
|
|
249
|
+
- [使用Playwright](#collecting-v8-coverage-data-with-playwright)
|
|
250
|
+
- [使用Puppeteer](#collecting-raw-v8-coverage-data-with-puppeteer)
|
|
251
|
+
|
|
252
|
+
- 从Node.js
|
|
253
|
+
- [从Node.js收集V8覆盖率数据](#collecting-v8-coverage-data-from-nodejs)
|
|
254
|
+
|
|
255
|
+
- 使用CDP
|
|
256
|
+
- [使用`CDPClient` API收集V8覆盖率数据](#collecting-v8-coverage-data-with-cdpclient-api)
|
|
257
|
+
|
|
258
|
+
### Collecting V8 Coverage Data with Playwright
|
|
259
|
+
使用Playwright的覆盖接口收集覆盖率数据
|
|
260
|
+
```js
|
|
261
|
+
await Promise.all([
|
|
262
|
+
page.coverage.startJSCoverage({
|
|
263
|
+
// reportAnonymousScripts: true,
|
|
264
|
+
resetOnNavigation: false
|
|
265
|
+
}),
|
|
266
|
+
page.coverage.startCSSCoverage({
|
|
267
|
+
// Note, anonymous styles (without sourceURLs) are not supported, alternatively, you can use CDPClient
|
|
268
|
+
resetOnNavigation: false
|
|
269
|
+
})
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
await page.goto("your page url");
|
|
273
|
+
|
|
274
|
+
const [jsCoverage, cssCoverage] = await Promise.all([
|
|
275
|
+
page.coverage.stopJSCoverage(),
|
|
276
|
+
page.coverage.stopCSSCoverage()
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
const coverageData = [... jsCoverage, ... cssCoverage];
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
参见例子 [./test/test-v8.js](./test/test-v8.js), and [anonymous](./test/test-anonymous.js), [css](./test/test-css.js)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
### Collecting Raw V8 Coverage Data with Puppeteer
|
|
286
|
+
使用Puppeteer的覆盖接口收集覆盖率数据,注意Puppeteer默认不会提供原生V8的覆盖率数据,需要设置`includeRawScriptCoverage`
|
|
287
|
+
```js
|
|
288
|
+
await Promise.all([
|
|
289
|
+
page.coverage.startJSCoverage({
|
|
290
|
+
// reportAnonymousScripts: true,
|
|
291
|
+
resetOnNavigation: false,
|
|
292
|
+
// provide raw v8 coverage data
|
|
293
|
+
includeRawScriptCoverage: true
|
|
294
|
+
}),
|
|
295
|
+
page.coverage.startCSSCoverage({
|
|
296
|
+
resetOnNavigation: false
|
|
297
|
+
})
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
await page.goto("your page url");
|
|
301
|
+
|
|
302
|
+
const [jsCoverage, cssCoverage] = await Promise.all([
|
|
303
|
+
page.coverage.stopJSCoverage(),
|
|
304
|
+
page.coverage.stopCSSCoverage()
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
// to raw V8 script coverage
|
|
308
|
+
const coverageData = [... jsCoverage.map((it) => {
|
|
309
|
+
return {
|
|
310
|
+
source: it.text,
|
|
311
|
+
... it.rawScriptCoverage
|
|
312
|
+
};
|
|
313
|
+
}), ... cssCoverage];
|
|
314
|
+
```
|
|
315
|
+
参见: [./test/test-puppeteer.js](./test/test-puppeteer.js)
|
|
316
|
+
|
|
317
|
+
### Collecting V8 Coverage Data from Node.js
|
|
318
|
+
有多种方法可以从Node.js收集V8覆盖率数据:
|
|
319
|
+
- [NODE_V8_COVERAGE](https://nodejs.org/docs/latest/api/cli.html#node_v8_coveragedir)=`dir`
|
|
320
|
+
- 使用Node.js环境变量`NODE_V8_COVERAGE`=`dir`来启动程序, 然后在进程正常结束之后,覆盖率数据将自动保存到指定的`dir`目录.
|
|
321
|
+
- 从`dir`目录读取所有的JSON文件,来生成覆盖率报告
|
|
322
|
+
- 参见例子:
|
|
323
|
+
> cross-env NODE_V8_COVERAGE=`.temp/v8-coverage-env` node [./test/test-node-env.js](./test/test-node-env.js) && node [./test/generate-report.js](./test/generate-report.js)
|
|
324
|
+
|
|
325
|
+
- [V8](https://nodejs.org/docs/latest/api/v8.html#v8takecoverage) API + NODE_V8_COVERAGE
|
|
326
|
+
- 如果进程不能正常结束,比如被强制关闭,或者压根就不结束,比如启动了一个服务类的,那么需要手动写入覆盖率数据,这里需要调用接口`v8.takeCoverage()`
|
|
327
|
+
- 参见例子:
|
|
328
|
+
> cross-env NODE_V8_COVERAGE=`.temp/v8-coverage-api` node [./test/test-node-api.js](./test/test-node-api.js)
|
|
329
|
+
|
|
330
|
+
- [Inspector](https://nodejs.org/docs/latest/api/inspector.html) API
|
|
331
|
+
- 首先连接到Node.js的V8 inspector
|
|
332
|
+
- 然后使用inspector的覆盖相关API来开启和收集覆盖率数据
|
|
333
|
+
- 参见例子:
|
|
334
|
+
> node [./test/test-node-ins.js](./test/test-node-ins.js)
|
|
335
|
+
- vm的例子 (注意这里需要使用`scriptOffset`,因为vm里一般都会加一层包裹代码,需要这个偏移位置来修正覆盖率数据块的位置):
|
|
336
|
+
> node [./test/test-node-vm.js](./test/test-node-vm.js)
|
|
337
|
+
|
|
338
|
+
- [CDP](https://chromedevtools.github.io/devtools-protocol/) API
|
|
339
|
+
- 开启[Node调试](https://nodejs.org/en/guides/debugging-getting-started/)
|
|
340
|
+
- 使用CDP的覆盖率接口开启和收集覆盖率数据
|
|
341
|
+
- 参见例子:
|
|
342
|
+
> node --inspect=9229 [./test/test-node-cdp.js](./test/test-node-cdp.js)
|
|
343
|
+
|
|
344
|
+
- [Node Debugging](https://nodejs.org/en/guides/debugging-getting-started) + CDP + NODE_V8_COVERAGE + V8 API
|
|
345
|
+
- 如果启动了一个Node服务,可以手动调用`v8.takeCoverage()`接口来保存覆盖率数据,开启Node调试就可以远程通过CDP连接的`Runtime.evaluate`,来调用这个接口.
|
|
346
|
+
- 参见[koa](https://github.com/koajs/koa)的例子:
|
|
347
|
+
> node [./test/test-node-koa.js](./test/test-node-koa.js)
|
|
348
|
+
|
|
349
|
+
- [Child Process](https://nodejs.org/docs/latest/api/child_process.html) + NODE_V8_COVERAGE
|
|
350
|
+
- 如果是子进程,可参见 [命令行](#command-line)
|
|
351
|
+
|
|
352
|
+
### Collecting V8 Coverage Data with `CDPClient` API
|
|
353
|
+
- `CDPClient`为`MCR`提供的内置接口类,用来更便捷的处理覆盖率相关数据,所有的API如下
|
|
354
|
+
```js
|
|
355
|
+
// 开始和停止并收集JS的覆盖率数据
|
|
356
|
+
startJSCoverage: () => Promise<void>;
|
|
357
|
+
stopJSCoverage: () => Promise<V8CoverageEntry[]>;
|
|
358
|
+
|
|
359
|
+
// 开始和停止并收集CSS的覆盖率数据,支持匿名文件(比如style里的css)
|
|
360
|
+
startCSSCoverage: () => Promise<void>;
|
|
361
|
+
stopCSSCoverage: () => Promise<V8CoverageEntry[]>;
|
|
362
|
+
|
|
363
|
+
// 开始和停止并收集JS和CSS的覆盖率数据
|
|
364
|
+
startCoverage: () => Promise<void>;
|
|
365
|
+
stopCoverage: () => Promise<V8CoverageEntry[]>;
|
|
366
|
+
|
|
367
|
+
/** 如果开启了NODE_V8_COVERAGE,这个接口用来手动保存当前覆盖率数据 */
|
|
368
|
+
writeCoverage: () => Promise<string>;
|
|
369
|
+
|
|
370
|
+
/** 收集istanbul覆盖率数据 */
|
|
371
|
+
getIstanbulCoverage: (coverageKey?: string) => Promise<any>;
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
- 结合使用Node调试端口`--inspect=9229` 或者浏览器调试端口 `--remote-debugging-port=9229`
|
|
375
|
+
```js
|
|
376
|
+
const MCR = require('monocart-coverage-reports');
|
|
377
|
+
const client = await MCR.CDPClient({
|
|
378
|
+
port: 9229
|
|
379
|
+
});
|
|
380
|
+
await client.startJSCoverage();
|
|
381
|
+
// run your test here
|
|
382
|
+
const coverageData = await client.stopJSCoverage();
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
- 结合使用 [Playwright CDPSession](https://playwright.dev/docs/api/class-cdpsession)
|
|
386
|
+
```js
|
|
387
|
+
const { chromium } = require('playwright');
|
|
388
|
+
const MCR = require('monocart-coverage-reports');
|
|
389
|
+
const browser = await chromium.launch();
|
|
390
|
+
const page = await browser.newPage();
|
|
391
|
+
const session = await page.context().newCDPSession(page);
|
|
392
|
+
const client = await MCR.CDPClient({
|
|
393
|
+
session
|
|
394
|
+
});
|
|
395
|
+
// both js and css coverage
|
|
396
|
+
await client.startCoverage();
|
|
397
|
+
// run your test page here
|
|
398
|
+
await page.goto("your page url");
|
|
399
|
+
const coverageData = await client.stopCoverage();
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
- 结合使用 [Puppeteer CDPSession](https://pptr.dev/api/puppeteer.cdpsession)
|
|
403
|
+
```js
|
|
404
|
+
const puppeteer = require('puppeteer');
|
|
405
|
+
const MCR = require('monocart-coverage-reports');
|
|
406
|
+
const browser = await puppeteer.launch({});
|
|
407
|
+
const page = await browser.newPage();
|
|
408
|
+
const session = await page.target().createCDPSession();
|
|
409
|
+
const client = await MCR.CDPClient({
|
|
410
|
+
session
|
|
411
|
+
});
|
|
412
|
+
// both js and css coverage
|
|
413
|
+
await client.startCoverage();
|
|
414
|
+
// run your test page here
|
|
415
|
+
await page.goto("your page url");
|
|
416
|
+
const coverageData = await client.stopCoverage();
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
- 结合使用 [Selenium Webdriver](https://www.selenium.dev/documentation/webdriver/) WebSocket (仅支持Chrome/Edge浏览器)
|
|
420
|
+
```js
|
|
421
|
+
const { Builder, Browser } = require('selenium-webdriver');
|
|
422
|
+
const MCR = require('monocart-coverage-reports');
|
|
423
|
+
const driver = await new Builder().forBrowser(Browser.CHROME).build();
|
|
424
|
+
const pageCdpConnection = await driver.createCDPConnection('page');
|
|
425
|
+
const session = new MCR.WSSession(pageCdpConnection._wsConnection);
|
|
426
|
+
const client = await MCR.CDPClient({
|
|
427
|
+
session
|
|
428
|
+
})
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
### V8 Coverage Data API
|
|
432
|
+
- [JavaScript V8代码覆盖官方说明](https://v8.dev/blog/javascript-code-coverage)
|
|
433
|
+
- [Playwright的覆盖率接口](https://playwright.dev/docs/api/class-coverage)
|
|
434
|
+
- [Puppeteer的覆盖率接口](https://pptr.dev/api/puppeteer.coverage)
|
|
435
|
+
- [DevTools Protocol的覆盖率接口](https://chromedevtools.github.io/devtools-protocol/tot/Profiler/#method-startPreciseCoverage) 参见 [ScriptCoverage](https://chromedevtools.github.io/devtools-protocol/tot/Profiler/#type-ScriptCoverage) 和 [v8-coverage](https://github.com/bcoe/v8-coverage)
|
|
436
|
+
```js
|
|
437
|
+
// Coverage data for a source range.
|
|
438
|
+
export interface CoverageRange {
|
|
439
|
+
// JavaScript script source offset for the range start.
|
|
440
|
+
startOffset: integer;
|
|
441
|
+
// JavaScript script source offset for the range end.
|
|
442
|
+
endOffset: integer;
|
|
443
|
+
// Collected execution count of the source range.
|
|
444
|
+
count: integer;
|
|
445
|
+
}
|
|
446
|
+
// Coverage data for a JavaScript function.
|
|
447
|
+
/**
|
|
448
|
+
* @functionName can be an empty string.
|
|
449
|
+
* @ranges is always non-empty. The first range is called the "root range".
|
|
450
|
+
* @isBlockCoverage indicates if the function has block coverage information.
|
|
451
|
+
If this is false, it usually means that the functions was never called.
|
|
452
|
+
It seems to be equivalent to ranges.length === 1 && ranges[0].count === 0.
|
|
453
|
+
*/
|
|
454
|
+
export interface FunctionCoverage {
|
|
455
|
+
// JavaScript function name.
|
|
456
|
+
functionName: string;
|
|
457
|
+
// Source ranges inside the function with coverage data.
|
|
458
|
+
ranges: CoverageRange[];
|
|
459
|
+
// Whether coverage data for this function has block granularity.
|
|
460
|
+
isBlockCoverage: boolean;
|
|
461
|
+
}
|
|
462
|
+
// Coverage data for a JavaScript script.
|
|
463
|
+
export interface ScriptCoverage {
|
|
464
|
+
// JavaScript script id.
|
|
465
|
+
scriptId: Runtime.ScriptId;
|
|
466
|
+
// JavaScript script name or url.
|
|
467
|
+
url: string;
|
|
468
|
+
// Functions contained in the script that has coverage data.
|
|
469
|
+
functions: FunctionCoverage[];
|
|
470
|
+
}
|
|
471
|
+
export type V8CoverageData = ScriptCoverage[];
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
## Using `entryFilter` and `sourceFilter` to filter the results for V8 report
|
|
475
|
+
当收集到V8的覆盖数据时,它实际上包含了所有的入口文件的覆盖率数据, 比如有以下3个文件:
|
|
476
|
+
|
|
477
|
+
- *dist/main.js*
|
|
478
|
+
- *dist/vendor.js*
|
|
479
|
+
- *dist/something-else.js*
|
|
480
|
+
|
|
481
|
+
这个时候可以使用`entryFilter`来过滤这些入口文件. 比如我们不需要看到`vendor.js`和`something-else.js`的覆盖率,就可以过滤掉,只剩下1个文件
|
|
482
|
+
|
|
483
|
+
- *dist/main.js*
|
|
484
|
+
|
|
485
|
+
如果一个入口文件存在行内或者链接的sourcemap文件,那么我们会尝试读取并解析sourcemap,以获取入口文件包含的所有源文件,并添加到列表。此时如果`logging`没有设置成`debug`,那么这个入口文件在成功解出源文件后会被移除
|
|
486
|
+
|
|
487
|
+
- *src/index.js*
|
|
488
|
+
- *src/components/app.js*
|
|
489
|
+
- *node_modules/dependency/dist/dependency.js*
|
|
490
|
+
|
|
491
|
+
这个时候可以使用`sourceFilter`来过滤这些源文件。比如我们不需要看到源文件`dependency.js`的覆盖率,就可以过滤掉,最后只剩下如下文件
|
|
492
|
+
|
|
493
|
+
- *src/index.js*
|
|
494
|
+
- *src/components/app.js*
|
|
495
|
+
|
|
496
|
+
过滤可以使用函数:
|
|
497
|
+
```js
|
|
498
|
+
const coverageOptions = {
|
|
499
|
+
entryFilter: (entry) => entry.url.indexOf("main.js") !== -1,
|
|
500
|
+
sourceFilter: (sourcePath) => sourcePath.search(/src\//) !== -1
|
|
501
|
+
};
|
|
502
|
+
```
|
|
503
|
+
也可以使用便捷的[`minimatch`](https://github.com/isaacs/minimatch)来匹配(推荐):
|
|
504
|
+
```js
|
|
505
|
+
const coverageOptions = {
|
|
506
|
+
entryFilter: "**/main.js",
|
|
507
|
+
sourceFilter: "**/src/**"
|
|
508
|
+
};
|
|
509
|
+
```
|
|
510
|
+
支持多个匹配:
|
|
511
|
+
```js
|
|
512
|
+
const coverageOptions = {
|
|
513
|
+
entryFilter: {
|
|
514
|
+
'**/node_modules/**': false,
|
|
515
|
+
'**/vendor.js': false,
|
|
516
|
+
'**/src/**': true
|
|
517
|
+
},
|
|
518
|
+
sourceFilter: {
|
|
519
|
+
'**/node_modules/**': false,
|
|
520
|
+
'**/**': true
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
```
|
|
524
|
+
这些`minimatch`匹配的运行逻辑,大概相当于:
|
|
525
|
+
```js
|
|
526
|
+
const coverageOptions = {
|
|
527
|
+
// '**/node_modules/**': false,
|
|
528
|
+
// '**/vendor.js': false,
|
|
529
|
+
// '**/src/**': true
|
|
530
|
+
entryFilter: (entry) => {
|
|
531
|
+
if (minimatch(entry.url, '**/node_modules/**')) { return false; }
|
|
532
|
+
if (minimatch(entry.url, '**/vendor.js')) { return false; }
|
|
533
|
+
if (minimatch(entry.url, '**/src/**')) { return true; }
|
|
534
|
+
return false; // else unmatched
|
|
535
|
+
}
|
|
536
|
+
// 注意,前面的如果已经匹配到会直接返回,后面的不会继续匹配,所以如果存在包含关系的,需要注意每个匹配的上下顺序,最后如果都未匹配,则默认返回false
|
|
537
|
+
};
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
## Resolve `sourcePath` for the Source Files
|
|
541
|
+
当一个文件从sourcemap解包,它的路径可能是个虚拟路径, 此时可以使用`sourcePath`选项来修改文件路径。比如,我们测试了多个dist包的入口文件,它们的源文件可能包含了一些共同的文件,但路径可能不同,如果我们需要相同的文件覆盖率数据可以自动合并,那么需要使用`sourcePath`来统一这些相同文件的路径
|
|
542
|
+
```js
|
|
543
|
+
const coverageOptions = {
|
|
544
|
+
sourcePath: (filePath) => {
|
|
545
|
+
// Remove the virtual prefix
|
|
546
|
+
const list = ['my-dist-file1/', 'my-dist-file2/'];
|
|
547
|
+
for (const str of list) {
|
|
548
|
+
if (filePath.startsWith(str)) {
|
|
549
|
+
return filePath.slice(str.length);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return filePath;
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
```
|
|
556
|
+
它也支持简单key/value的替换:
|
|
557
|
+
```js
|
|
558
|
+
const coverageOptions = {
|
|
559
|
+
sourcePath: {
|
|
560
|
+
'my-dist-file1/': '',
|
|
561
|
+
'my-dist-file2/': ''
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
## Adding Empty Coverage for Untested Files
|
|
567
|
+
默认,未测试的文件是不会包含到覆盖率报告的,需要使用`all`选项来为这些文件添加一个空的覆盖率,也就是0%
|
|
568
|
+
```js
|
|
569
|
+
const coverageOptions = {
|
|
570
|
+
all: {
|
|
571
|
+
dir: ['./src'],
|
|
572
|
+
filter: (filePath) => {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
```
|
|
578
|
+
`filter`过滤也支持[`minimatch`](https://github.com/isaacs/minimatch)匹配:
|
|
579
|
+
```js
|
|
580
|
+
const coverageOptions = {
|
|
581
|
+
all: {
|
|
582
|
+
dir: ['./src'],
|
|
583
|
+
filter: '**/*.js'
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
// 多个匹配
|
|
587
|
+
const coverageOptions = {
|
|
588
|
+
all: {
|
|
589
|
+
dir: ['./src'],
|
|
590
|
+
filter: {
|
|
591
|
+
// exclude files
|
|
592
|
+
'**/ignored-*.js': false,
|
|
593
|
+
'**/*.html': false,
|
|
594
|
+
'**/*.ts': false,
|
|
595
|
+
// empty css coverage
|
|
596
|
+
'**/*.scss': "css",
|
|
597
|
+
'**/*': true
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
## onEnd Hook
|
|
604
|
+
结束回调可以用来自定义业务需求,比如检测覆盖率是否达标,对比每个指标的thresholds,如果低于要求的值则可以抛出一个错误退出
|
|
605
|
+
```js
|
|
606
|
+
const EC = require('eight-colors');
|
|
607
|
+
const coverageOptions = {
|
|
608
|
+
name: 'My Coverage Report',
|
|
609
|
+
outputDir: './coverage-reports',
|
|
610
|
+
onEnd: (coverageResults) => {
|
|
611
|
+
const thresholds = {
|
|
612
|
+
bytes: 80,
|
|
613
|
+
lines: 60
|
|
614
|
+
};
|
|
615
|
+
console.log('check thresholds ...', thresholds);
|
|
616
|
+
const errors = [];
|
|
617
|
+
const { summary } = coverageResults;
|
|
618
|
+
Object.keys(thresholds).forEach((k) => {
|
|
619
|
+
const pct = summary[k].pct;
|
|
620
|
+
if (pct < thresholds[k]) {
|
|
621
|
+
errors.push(`Coverage threshold for ${k} (${pct} %) not met: ${thresholds[k]} %`);
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
if (errors.length) {
|
|
625
|
+
const errMsg = errors.join('\n');
|
|
626
|
+
console.log(EC.red(errMsg));
|
|
627
|
+
// throw new Error(errMsg);
|
|
628
|
+
// process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
## Ignoring Uncovered Codes
|
|
635
|
+
使用特定的注释,以`v8 ignore `开头可以忽略未覆盖的代码:
|
|
636
|
+
- 忽略开始到结束
|
|
637
|
+
```js
|
|
638
|
+
/* v8 ignore start */
|
|
639
|
+
function uncovered() {
|
|
640
|
+
}
|
|
641
|
+
/* v8 ignore stop */
|
|
642
|
+
```
|
|
643
|
+
- 忽略接下来一行或者多行
|
|
644
|
+
```js
|
|
645
|
+
/* v8 ignore next */
|
|
646
|
+
const os = platform === 'wind32' ? 'Windows' : 'Other';
|
|
647
|
+
|
|
648
|
+
const os = platform === 'wind32' ? 'Windows' /* v8 ignore next */ : 'Other';
|
|
649
|
+
|
|
650
|
+
// v8 ignore next 3
|
|
651
|
+
if (platform === 'linux') {
|
|
652
|
+
console.log('hello linux');
|
|
653
|
+
}
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
## Multiprocessing Support
|
|
657
|
+
> 多进程支持可以很好的解决异步并行的情况。所有的覆盖率数据会保存到`[outputDir]/.cache`,在报告生成之后,这些缓存数据会被清除。除非开启了[调试模式](#debug-for-coverage-and-sourcemap),或者使用了`raw`报告
|
|
658
|
+
- 主进程,初始化,清理之前的缓存
|
|
659
|
+
```js
|
|
660
|
+
const MCR = require('monocart-coverage-reports');
|
|
661
|
+
const coverageOptions = require('path-to/same-options.js');
|
|
662
|
+
const mcr = MCR(coverageOptions);
|
|
663
|
+
// clean previous cache before the start of testing
|
|
664
|
+
// unless the running environment is new and no cache
|
|
665
|
+
mcr.cleanCache();
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
- 子进程1, 测试业务1
|
|
669
|
+
```js
|
|
670
|
+
const MCR = require('monocart-coverage-reports');
|
|
671
|
+
const coverageOptions = require('path-to/same-options.js');
|
|
672
|
+
const mcr = MCR(coverageOptions);
|
|
673
|
+
await mcr.add(coverageData1);
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
- 子进程2, 测试业务2
|
|
677
|
+
```js
|
|
678
|
+
const MCR = require('monocart-coverage-reports');
|
|
679
|
+
const coverageOptions = require('path-to/same-options.js');
|
|
680
|
+
const mcr = MCR(coverageOptions);
|
|
681
|
+
await mcr.add(coverageData2);
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
- 主进程,所有测试完成之后
|
|
685
|
+
```js
|
|
686
|
+
// generate coverage reports after the completion of testing
|
|
687
|
+
const MCR = require('monocart-coverage-reports');
|
|
688
|
+
const coverageOptions = require('path-to/same-options.js');
|
|
689
|
+
const mcr = MCR(coverageOptions);
|
|
690
|
+
await mcr.generate();
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
## Command Line
|
|
694
|
+
> 使用`mcr`命令行将使用`NODE_V8_COVERAGE=dir`来启动一个[子进程](https://nodejs.org/docs/latest/api/child_process.html)运行程序,直到正常退出,然后自动从`dir`目录来读取覆盖率数据,并生成覆盖率报告
|
|
695
|
+
|
|
696
|
+
- 全局安装
|
|
697
|
+
```sh
|
|
698
|
+
npm i monocart-coverage-reports -g
|
|
699
|
+
mcr node ./test/specs/node.test.js -r v8,console-details --lcov
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
- 本地项目安装
|
|
703
|
+
```sh
|
|
704
|
+
npm i monocart-coverage-reports
|
|
705
|
+
npx mcr node ./test/specs/node.test.js -r v8,console-details --lcov
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
- 命令行参数
|
|
709
|
+
直接运行 `mcr` 或 `mcr --help` 查看所有CLI的参数
|
|
710
|
+
|
|
711
|
+
- 使用 `--` 可以隔离子程序参数,以免两种参数混淆
|
|
712
|
+
```sh
|
|
713
|
+
mcr -c mcr.config.js -- sub-cli -c sub-cli.config.js
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
- 参见例子
|
|
717
|
+
- [Mocha](#mocha)
|
|
718
|
+
- [tsx](#tsx)
|
|
719
|
+
- [ts-node](#ts-node)
|
|
720
|
+
- [AVA](#ava)
|
|
721
|
+
|
|
722
|
+
## Config File
|
|
723
|
+
根据以下优先级加载配置文件
|
|
724
|
+
- 自定义配置文件(如果没有指定则加载后面的默认配置文件):
|
|
725
|
+
- CLI: `mcr --config <my-config-file-path>`
|
|
726
|
+
- API: `await mcr.loadConfig("my-config-file-path")`
|
|
727
|
+
- `mcr.config.js`
|
|
728
|
+
- `mcr.config.cjs`
|
|
729
|
+
- `mcr.config.mjs`
|
|
730
|
+
- `mcr.config.json` - json format
|
|
731
|
+
- `mcr.config.ts` (requires preloading the ts execution module)
|
|
732
|
+
|
|
733
|
+
## Merge Coverage Reports
|
|
734
|
+
以下这些使用场景可能需要使用合并覆盖率报告:
|
|
735
|
+
- 多个执行环境,比如Node.js服务端,以及浏览器客户端,比如`Next.js`
|
|
736
|
+
- 多种测试类型,比如`Jest`单元测试,以及`Playwright`的端到端自动化测试
|
|
737
|
+
- 分布式测试,测试结果保存到了多台机器或不同的容器中
|
|
738
|
+
|
|
739
|
+
### Automatic Merging
|
|
740
|
+
- 默认`MCR`在执行`generate()`时会自动合并覆盖率数据。所以可以在[多进程支持](#multiprocessing-support)下,多次添加覆盖率数据,最后将自动合并
|
|
741
|
+
- 比如`Next.js`就可以同时添加前后端覆盖率数据,最后再执行`generate()`生成覆盖率报告,见例子[nextjs-with-playwright](https://github.com/cenfun/nextjs-with-playwright)
|
|
742
|
+
- 使用`Codecov`在线覆盖率报告服务,请设置输出`codecov`报告, 它会生成专属的`codecov.json`,如果有多个`codecov.json`文件上传,它们会自动合并数据,参见[Codecov](#codecov) 和 [合并报告说明](https://docs.codecov.com/docs/merging-reports)
|
|
743
|
+
|
|
744
|
+
### Manual Merging
|
|
745
|
+
手动合并覆盖率报告需要使用`raw`报告来导出原始的覆盖率数据到指定的目录
|
|
746
|
+
- 比如,单元测试保存到`./coverage-reports/unit/raw`,见例子
|
|
747
|
+
- `Jest` + [jest-monocart-coverage](https://github.com/cenfun/jest-monocart-coverage)
|
|
748
|
+
- `Vitest` + [vitest-monocart-coverage](https://github.com/cenfun/vitest-monocart-coverage)
|
|
749
|
+
```js
|
|
750
|
+
const coverageOptions = {
|
|
751
|
+
name: 'My Unit Test Coverage Report',
|
|
752
|
+
outputDir: "./coverage-reports/unit",
|
|
753
|
+
reports: [
|
|
754
|
+
['raw', {
|
|
755
|
+
// relative path will be "./coverage-reports/unit/raw"
|
|
756
|
+
// defaults to raw
|
|
757
|
+
outputDir: "raw"
|
|
758
|
+
}],
|
|
759
|
+
['v8'],
|
|
760
|
+
['console-details']
|
|
761
|
+
]
|
|
762
|
+
};
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
- 同样的,E2E测试保存到`./coverage-reports/e2e/raw`. 见例子:
|
|
766
|
+
- `Playwright` + [monocart-reporter](https://github.com/cenfun/monocart-reporter) with coverage API
|
|
767
|
+
- `Playwright` + `MCR`, see [playwright-coverage](https://github.com/cenfun/playwright-coverage)
|
|
768
|
+
- see more [Integration Examples](#integration-examples)
|
|
769
|
+
|
|
770
|
+
- 然后创建一个`merge-coverage.js`文件,使用`inputDir`参数导入`raw`数据,来生成合并的覆盖率报告.
|
|
771
|
+
```js
|
|
772
|
+
// merge-coverage.js
|
|
773
|
+
const fs = require('fs');
|
|
774
|
+
const { CoverageReport } = require('monocart-coverage-reports');
|
|
775
|
+
const inputDir = [
|
|
776
|
+
'./coverage-reports/unit/raw',
|
|
777
|
+
'./coverage-reports/e2e/raw'
|
|
778
|
+
];
|
|
779
|
+
const coverageOptions = {
|
|
780
|
+
name: 'My Merged Coverage Report',
|
|
781
|
+
inputDir,
|
|
782
|
+
outputDir: './coverage-reports/merged',
|
|
783
|
+
|
|
784
|
+
// filter for both unit and e2e
|
|
785
|
+
entryFilter: {
|
|
786
|
+
'**/node_modules/**': false,
|
|
787
|
+
'**/*': true
|
|
788
|
+
},
|
|
789
|
+
sourceFilter: {
|
|
790
|
+
'**/node_modules/**': false,
|
|
791
|
+
'**/src/**': true
|
|
792
|
+
},
|
|
793
|
+
|
|
794
|
+
sourcePath: (filePath, info) => {
|
|
795
|
+
// Unify the file path for the same files
|
|
796
|
+
// For example, the file index.js has different paths:
|
|
797
|
+
// unit: unit-dist/src/index.js
|
|
798
|
+
// e2e: e2e-dist/src/index.js
|
|
799
|
+
// return filePath.replace("unit-dist/", "").replace("e2e-dist/", "")
|
|
800
|
+
return filePath;
|
|
801
|
+
},
|
|
802
|
+
|
|
803
|
+
reports: [
|
|
804
|
+
['v8'],
|
|
805
|
+
['console-details']
|
|
806
|
+
],
|
|
807
|
+
|
|
808
|
+
onEnd: () => {
|
|
809
|
+
// remove the raw files if it useless
|
|
810
|
+
// inputDir.forEach((p) => {
|
|
811
|
+
// fs.rmSync(p, {
|
|
812
|
+
// recursive: true,
|
|
813
|
+
// force: true
|
|
814
|
+
// });
|
|
815
|
+
// });
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
await new CoverageReport(coverageOptions).generate();
|
|
819
|
+
```
|
|
820
|
+
- 最后在所有测试完成后运行`node path/to/merge-coverage.js`. 所有的执行脚本大概如下:
|
|
821
|
+
```json
|
|
822
|
+
{
|
|
823
|
+
"scripts": {
|
|
824
|
+
"test:unit": "jest",
|
|
825
|
+
"test:e2e": "playwright test",
|
|
826
|
+
"merge-coverage": "node path/to/merge-coverage.js",
|
|
827
|
+
"test": "npm run test:unit && npm run test:e2e && npm run merge-coverage"
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
## Common issues
|
|
833
|
+
> 常见问题
|
|
834
|
+
### Unexpected coverage
|
|
835
|
+
覆盖率看起来不正确,多数情况是因为sourcemap转换的问题导致的. 可以先尝试设置构建工具的 `minify=false` 也就是不要压缩代码来解决。下面来看看sourcemap存在问题的具体原因:
|
|
836
|
+
```js
|
|
837
|
+
const a = tf ? 'true' : 'false';
|
|
838
|
+
^ ^ ^
|
|
839
|
+
m1 p m2
|
|
840
|
+
```
|
|
841
|
+
上面是经过构建工具编译过的代码,通过AST分析,位置`p`对应的原始位置是我们要找的,而从sourcemap里仅能找到离`p`最近的位置映射`m1`和`m2`,也就是位置`p`并没有精确的映射保存到sourcemap里,从而无法直接获取精确的原始位置,但我们能知道`p`的原始位置应该在`m1`和`m2`之间。
|
|
842
|
+
- 参见 [调试覆盖率和sourcemap](#debug-for-coverage-and-sourcemap)
|
|
843
|
+
|
|
844
|
+
`MCR`如何解决这个问题:
|
|
845
|
+
- 1, 首先会尝试使用[`diff-sequences`](https://github.com/jestjs/jest/tree/main/packages/diff-sequences)工具来比较`m1`和`m2`之间的生成代码和原始代码,找到`p`对应的字符位置,可以解决绝大多数问题。但是如果代码是非JS格式的,比如Vue模板是HTML,或JSX这些,不管怎么比较也是很难精确找到对应位置的,甚至此时的sourcemap本身都比较乱。
|
|
846
|
+
- 2, 然后就是通过分析[AST](https://github.com/acornjs/acorn),找到所有的functions, statements 和 branches,因为V8覆盖率本身不提供这些指标的覆盖率. (对于分支覆盖暂不支持`AssignmentPattern`类型,因为即使分析AST也无法从V8覆盖率找到它的数据)。
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
### Unparsable source
|
|
850
|
+
源码无法解析问题。由上面我们知道`MCR`通过分析源码的AST获取更多指标的覆盖率信息,但源码如果不是标准的 ECMAScript,比如`ts`, `jsx`这些,那么分析的时候就会报错,此时我们可以手动来编译这些文件(可行但不推荐).
|
|
851
|
+
```js
|
|
852
|
+
import * as fs from "fs";
|
|
853
|
+
import * as path from "path";
|
|
854
|
+
import { fileURLToPath } from "url";
|
|
855
|
+
import * as TsNode from 'ts-node';
|
|
856
|
+
const coverageOptions = {
|
|
857
|
+
onEntry: async (entry) => {
|
|
858
|
+
const filePath = fileURLToPath(entry.url)
|
|
859
|
+
const originalSource = fs.readFileSync(filePath).toString("utf-8");
|
|
860
|
+
const fileName = path.basename(filePath);
|
|
861
|
+
const tn = TsNode.create({});
|
|
862
|
+
const source = tn.compile(originalSource, fileName);
|
|
863
|
+
entry.fake = false;
|
|
864
|
+
entry.source = source;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
## Debug for Coverage and Sourcemap
|
|
870
|
+
> 当你觉得覆盖率存在问题的时候,`MCR`支持自行调试来核验覆盖率的准确性
|
|
871
|
+
- 首先打开调试设置`logging: 'debug'`
|
|
872
|
+
```js
|
|
873
|
+
const coverageOptions = {
|
|
874
|
+
logging: 'debug',
|
|
875
|
+
reports: [
|
|
876
|
+
['v8'],
|
|
877
|
+
['console-details']
|
|
878
|
+
]
|
|
879
|
+
};
|
|
880
|
+
```
|
|
881
|
+
调试模式下,也就是`logging`为`debug`的时候, 原始的覆盖率数据将保留在`[outputDir]/.cache`缓存目录下,不会删除,如果使用了`raw`报告,那么位置变为`[outputDir]/raw`下,这样我们可以打开v8报告的html文件,通过下面新增的一些调试帮助信息来核对覆盖率
|
|
882
|
+

|
|
883
|
+
|
|
884
|
+
- 调试sourcemap可以直接使用[Source Map Visualization](https://evanw.github.io/source-map-visualization/) (esbuild作者提供的sourcemap在线查看器)
|
|
885
|
+
|
|
886
|
+

|
|
887
|
+
|
|
888
|
+
## Integration with Any Testing Framework
|
|
889
|
+
通用集成方案
|
|
890
|
+
- 通过API接口在程序集成
|
|
891
|
+
- 首先,要自行收集覆盖率数据,然后,添加到报告实例 `await mcr.add(coverageData)`
|
|
892
|
+
- 最后,生成覆盖率报告 `await mcr.generate()`
|
|
893
|
+
- 参见 [多进程支持](#multiprocessing-support)
|
|
894
|
+
- 通过CLI命令行与其他命令行集成
|
|
895
|
+
- 直接在其他命令行前面添加mcr的命令行即可 `mcr your-cli --your-arguments`
|
|
896
|
+
- 参见 [命令行](#command-line)
|
|
897
|
+
|
|
898
|
+
## Integration Examples
|
|
899
|
+
|
|
900
|
+
### [Playwright](https://github.com/microsoft/playwright)
|
|
901
|
+
- [playwright-coverage](https://github.com/cenfun/playwright-coverage) - Example for Playwright coverage reports
|
|
902
|
+
- [playwright-bdd-coverage](https://github.com/cenfun/playwright-bdd-coverage) - Example for Playwright BDD coverage reports
|
|
903
|
+
- [monocart-reporter](https://github.com/cenfun/monocart-reporter) - Playwright custom reporter, supports generating [Code coverage report](https://github.com/cenfun/monocart-reporter?#code-coverage-report)
|
|
904
|
+
- Coverage for component testing with `monocart-reporter`:
|
|
905
|
+
- [playwright-ct-vue](https://github.com/cenfun/playwright-ct-vue)
|
|
906
|
+
- [playwright-ct-react](https://github.com/cenfun/playwright-ct-react)
|
|
907
|
+
- [playwright-ct-svelte](https://github.com/cenfun/playwright-ct-svelte)
|
|
908
|
+
- Coverage for Next.js, both server side and client side:
|
|
909
|
+
- [nextjs-with-playwright](https://github.com/cenfun/nextjs-with-playwright)
|
|
910
|
+
- [nextjs-with-playwright-istanbul](https://github.com/cenfun/nextjs-with-playwright-istanbul)
|
|
911
|
+
- see [Collecting V8 Coverage Data with Playwright](#collecting-v8-coverage-data-with-playwright)
|
|
912
|
+
|
|
913
|
+
### [CodeceptJS](https://github.com/codeceptjs/CodeceptJS)
|
|
914
|
+
- CodeceptJS is a [BDD](https://codecept.io/bdd/) + [AI](https://codecept.io/ai/) testing framework for e2e testing, it has integrated `MCR` since [v3.5.15](https://github.com/codeceptjs/CodeceptJS/releases/tag/3.5.15), see [plugins/coverage](https://codecept.io/plugins/#coverage). There's no need to use ~~[codeceptjs-monocart-coverage](https://github.com/cenfun/codeceptjs-monocart-coverage)~~ anymore.
|
|
915
|
+
|
|
916
|
+
### [Jest](https://github.com/jestjs/jest/)
|
|
917
|
+
- [jest-monocart-coverage](https://github.com/cenfun/jest-monocart-coverage) - Jest custom reporter for coverage reports
|
|
918
|
+
|
|
919
|
+
### [Vitest](https://github.com/vitest-dev/vitest)
|
|
920
|
+
- [vitest-monocart-coverage](https://github.com/cenfun/vitest-monocart-coverage) - Vitest custom provider module for coverage reports
|
|
921
|
+
|
|
922
|
+
### [Puppeteer](https://github.com/puppeteer/puppeteer/)
|
|
923
|
+
- [jest-puppeteer-coverage](https://github.com/cenfun/jest-puppeteer-coverage) - Example for Jest puppeteer coverage
|
|
924
|
+
- [maplibre-gl-js](https://github.com/maplibre/maplibre-gl-js) - Example for Jest (unit) + Puppeteer (e2e) + Codecov
|
|
925
|
+
- see [Collecting Raw V8 Coverage Data with Puppeteer](#collecting-raw-v8-coverage-data-with-puppeteer)
|
|
926
|
+
|
|
927
|
+
### [Cypress](https://github.com/cypress-io/cypress)
|
|
928
|
+
- [cypress-monocart-coverage](https://github.com/cenfun/cypress-monocart-coverage) - Cypress plugin for coverage reports
|
|
929
|
+
|
|
930
|
+
### [WebdriverIO](https://github.com/webdriverio/webdriverio)
|
|
931
|
+
- [wdio-monocart-service](https://github.com/cenfun/wdio-monocart-service) - WebdriverIO service for coverage reports
|
|
932
|
+
|
|
933
|
+
### [Storybook Test Runner](https://github.com/storybookjs/test-runner)
|
|
934
|
+
- [storybook-monocart-coverage](https://github.com/cenfun/storybook-monocart-coverage) - Example for Storybook V8 coverage reports
|
|
935
|
+
|
|
936
|
+
### [TestCafe](https://github.com/DevExpress/testcafe)
|
|
937
|
+
- [testcafe-reporter-coverage](https://github.com/cenfun/testcafe-reporter-coverage) - TestCafe custom reporter for coverage reports
|
|
938
|
+
|
|
939
|
+
### [Selenium Webdriver](https://github.com/seleniumhq/selenium)
|
|
940
|
+
- [selenium-webdriver-coverage](https://github.com/cenfun/selenium-webdriver-coverage) - Example for Selenium Webdriver V8 coverage reports
|
|
941
|
+
|
|
942
|
+
### [Mocha](https://github.com/mochajs/mocha)
|
|
943
|
+
```sh
|
|
944
|
+
mcr mocha ./test/**/*.js
|
|
945
|
+
```
|
|
946
|
+
```sh
|
|
947
|
+
mcr --import tsx mocha ./test/**/*.ts
|
|
948
|
+
```
|
|
949
|
+
- see [mcr-tsx](https://github.com/cenfun/mcr-tsx)
|
|
950
|
+
|
|
951
|
+
### [tsx](https://github.com/privatenumber/tsx)
|
|
952
|
+
```sh
|
|
953
|
+
mcr --import tsx tsx ./src/example.ts
|
|
954
|
+
```
|
|
955
|
+
- see [mcr-tsx](https://github.com/cenfun/mcr-tsx)
|
|
956
|
+
|
|
957
|
+
### [ts-node](https://github.com/TypeStrong/ts-node)
|
|
958
|
+
- see [mcr-ts-node](https://github.com/cenfun/mcr-ts-node)
|
|
959
|
+
|
|
960
|
+
### [AVA](https://github.com/avajs/ava)
|
|
961
|
+
```sh
|
|
962
|
+
mcr ava
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
### [Codecov](https://codecov.com/)
|
|
966
|
+
[](https://codecov.io/gh/cenfun/monocart-coverage-reports)
|
|
967
|
+
- Supports native `codecov` built-in report ([specification](https://docs.codecov.com/docs/codecov-custom-coverage-format))
|
|
968
|
+
```js
|
|
969
|
+
const coverageOptions = {
|
|
970
|
+
outputDir: "./coverage-reports",
|
|
971
|
+
reports: [
|
|
972
|
+
['codecov']
|
|
973
|
+
]
|
|
974
|
+
};
|
|
975
|
+
```
|
|
976
|
+
- Github actions example:
|
|
977
|
+
```yml
|
|
978
|
+
- name: Codecov
|
|
979
|
+
uses: codecov/codecov-action@v3
|
|
980
|
+
with:
|
|
981
|
+
files: ./coverage-reports/codecov.json
|
|
982
|
+
```
|
|
983
|
+
### [Coveralls](https://coveralls.io/)
|
|
984
|
+
[](https://coveralls.io/github/cenfun/monocart-coverage-reports?branch=main)
|
|
985
|
+
- Using `lcov` report:
|
|
986
|
+
```js
|
|
987
|
+
const coverageOptions = {
|
|
988
|
+
outputDir: "./coverage-reports",
|
|
989
|
+
lcov: true
|
|
990
|
+
};
|
|
991
|
+
```
|
|
992
|
+
- Github actions example:
|
|
993
|
+
```yml
|
|
994
|
+
- name: Coveralls
|
|
995
|
+
uses: coverallsapp/github-action@v2
|
|
996
|
+
with:
|
|
997
|
+
files: ./coverage-reports/lcov.info
|
|
998
|
+
```
|
|
999
|
+
### [Sonar Cloud](https://sonarcloud.io/)
|
|
1000
|
+
[](https://sonarcloud.io/summary/new_code?id=monocart-coverage-reports)
|
|
1001
|
+
- Using `lcov` report. Github actions example:
|
|
1002
|
+
```yml
|
|
1003
|
+
- name: Analyze with SonarCloud
|
|
1004
|
+
uses: sonarsource/sonarcloud-github-action@master
|
|
1005
|
+
env:
|
|
1006
|
+
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
1007
|
+
with:
|
|
1008
|
+
projectBaseDir: ./
|
|
1009
|
+
args: >
|
|
1010
|
+
-Dsonar.organization=cenfun
|
|
1011
|
+
-Dsonar.projectKey=monocart-coverage-reports
|
|
1012
|
+
-Dsonar.projectName=monocart-coverage-reports
|
|
1013
|
+
-Dsonar.javascript.lcov.reportPaths=docs/mcr/lcov.info
|
|
1014
|
+
-Dsonar.sources=lib
|
|
1015
|
+
-Dsonar.tests=test
|
|
1016
|
+
-Dsonar.exclusions=dist/*,packages/*
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
## Contributing
|
|
1020
|
+
```sh
|
|
1021
|
+
# Node.js 20+
|
|
1022
|
+
npm install starfall-cli -g
|
|
1023
|
+
npm install
|
|
1024
|
+
|
|
1025
|
+
npm run build
|
|
1026
|
+
npm run test
|
|
1027
|
+
|
|
1028
|
+
npm run dev
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
### VSCode Extension
|
|
1032
|
+
- [Coverage Gutters](https://github.com/ryanluker/vscode-coverage-gutters) - Display test coverage generated by lcov or xml in VSCode editor.
|
|
1033
|
+
|
|
1034
|
+
## Thanks
|
|
1035
|
+
- Special thanks to [@edumserrano](https://github.com/edumserrano)
|