Skip to content

Commit 96dc6e9

Browse files
authoredJan 12, 2024
feat(coverage): custom reporter support (#4828)
1 parent 480d866 commit 96dc6e9

File tree

12 files changed

+131
-21
lines changed

12 files changed

+131
-21
lines changed
 

‎docs/config/index.md

+18-1
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,23 @@ The reporter has three different types:
11711171
}
11721172
```
11731173

1174+
Since Vitest 1.2.0, you can also pass custom coverage reporters. See [Guide - Custom Coverage Reporter](/guide/coverage#custom-coverage-reporter) for more information.
1175+
1176+
<!-- eslint-skip -->
1177+
```ts
1178+
{
1179+
reporter: [
1180+
// Specify reporter using name of the NPM package
1181+
'@vitest/custom-coverage-reporter',
1182+
['@vitest/custom-coverage-reporter', { someOption: true }],
1183+
1184+
// Specify reporter using local path
1185+
'/absolute/path/to/custom-reporter.cjs',
1186+
['/absolute/path/to/custom-reporter.cjs', { someOption: true }],
1187+
]
1188+
}
1189+
```
1190+
11741191
Since Vitest 0.31.0, you can check your coverage report in Vitest UI: check [Vitest UI Coverage](/guide/coverage#vitest-ui) for more details.
11751192

11761193
#### coverage.reportOnFailure <Badge type="info">0.31.2+</Badge>
@@ -2045,7 +2062,7 @@ Path to a [workspace](/guide/workspace) config file relative to [root](#root).
20452062

20462063
- **Type:** `boolean`
20472064
- **Default:** `true`
2048-
- **CLI:** `--no-isolate`, `--isolate=false`
2065+
- **CLI:** `--no-isolate`, `--isolate=false`
20492066

20502067
Run tests in an isolated environment. This option has no effect on `vmThreads` pool.
20512068

‎docs/guide/coverage.md

+49
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,55 @@ export default defineConfig({
7070
})
7171
```
7272

73+
## Custom Coverage Reporter
74+
75+
You can use custom coverage reporters by passing either the name of the package or absolute path in `test.coverage.reporter`:
76+
77+
```ts
78+
// vitest.config.ts
79+
import { defineConfig } from 'vitest/config'
80+
81+
export default defineConfig({
82+
test: {
83+
coverage: {
84+
reporter: [
85+
// Specify reporter using name of the NPM package
86+
['@vitest/custom-coverage-reporter', { someOption: true }],
87+
88+
// Specify reporter using local path
89+
'/absolute/path/to/custom-reporter.cjs',
90+
],
91+
},
92+
},
93+
})
94+
```
95+
96+
Custom reporters are loaded by Istanbul and must match its reporter interface. See [built-in reporters' implementation](https://github.com/istanbuljs/istanbuljs/tree/master/packages/istanbul-reports/lib) for reference.
97+
98+
```js
99+
// custom-reporter.cjs
100+
const { ReportBase } = require('istanbul-lib-report')
101+
102+
module.exports = class CustomReporter extends ReportBase {
103+
constructor(opts) {
104+
super()
105+
106+
// Options passed from configuration are available here
107+
this.file = opts.file
108+
}
109+
110+
onStart(root, context) {
111+
this.contentWriter = context.writer.writeFile(this.file)
112+
this.contentWriter.println('Start of custom coverage report')
113+
}
114+
115+
onEnd() {
116+
this.contentWriter.println('End of custom coverage report')
117+
this.contentWriter.close()
118+
}
119+
}
120+
```
121+
73122
## Custom Coverage Provider
74123

75124
It's also possible to provide your custom coverage provider by passing `'custom'` in `test.coverage.provider`:

‎packages/coverage-istanbul/src/provider.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,8 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
206206
this.ctx.logger.log(c.blue(' % ') + c.dim('Coverage report from ') + c.yellow(this.name))
207207

208208
for (const reporter of this.options.reporter) {
209-
reports.create(reporter[0], {
209+
// Type assertion required for custom reporters
210+
reports.create(reporter[0] as Parameters<typeof reports.create>[0], {
210211
skipFull: this.options.skipFull,
211212
projectRoot: this.ctx.config.root,
212213
...reporter[1],

‎packages/coverage-v8/src/provider.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,8 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
198198
this.ctx.logger.log(c.blue(' % ') + c.dim('Coverage report from ') + c.yellow(this.name))
199199

200200
for (const reporter of this.options.reporter) {
201-
reports.create(reporter[0], {
201+
// Type assertion required for custom reporters
202+
reports.create(reporter[0] as Parameters<typeof reports.create>[0], {
202203
skipFull: this.options.skipFull,
203204
projectRoot: this.ctx.config.root,
204205
...reporter[1],

‎packages/ui/node/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function resolveCoverageFolder(ctx: Vitest) {
5757
? htmlReporter[1].subdir
5858
: undefined
5959

60-
if (!subdir)
60+
if (!subdir || typeof subdir !== 'string')
6161
return [root, `/${basename(root)}/`]
6262

6363
return [resolve(root, subdir), `/${basename(root)}/${subdir}/`]

‎packages/vitest/src/types/coverage.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,14 @@ export interface CoverageProviderModule {
5252
stopCoverage?(): unknown | Promise<unknown>
5353
}
5454

55-
export type CoverageReporter = keyof ReportOptions
55+
export type CoverageReporter = keyof ReportOptions | (string & {})
5656

5757
type CoverageReporterWithOptions<ReporterName extends CoverageReporter = CoverageReporter> =
58-
ReporterName extends CoverageReporter
58+
ReporterName extends keyof ReportOptions
5959
? ReportOptions[ReporterName] extends never
6060
? [ReporterName, {}] // E.g. the "none" reporter
6161
: [ReporterName, Partial<ReportOptions[ReporterName]>]
62-
: never
62+
: [ReporterName, Record<string, unknown>]
6363

6464
type Provider = 'v8' | 'istanbul' | 'custom' | undefined
6565

‎pnpm-lock.yaml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/coverage-test/coverage-report-tests/generic.report.test.ts

+14
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,20 @@ test('lcov report', async () => {
3131
expect(lcovReportFiles).toContain('index.html')
3232
})
3333

34+
test('custom report', async () => {
35+
const coveragePath = resolve('./coverage')
36+
const files = fs.readdirSync(coveragePath)
37+
38+
expect(files).toContain('custom-reporter-output.md')
39+
40+
const content = fs.readFileSync(resolve(coveragePath, 'custom-reporter-output.md'), 'utf-8')
41+
expect(content).toMatchInlineSnapshot(`
42+
"Start of custom coverage report
43+
End of custom coverage report
44+
"
45+
`)
46+
})
47+
3448
test('all includes untested files', () => {
3549
const coveragePath = resolve('./coverage/src')
3650
const files = fs.readdirSync(coveragePath)
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/* Istanbul uses `require`: https://github.com/istanbuljs/istanbuljs/blob/5584b50305a6a17d3573aea25c84e254d4a08b65/packages/istanbul-reports/index.js#L19 */
2+
3+
'use strict'
4+
const { ReportBase } = require('istanbul-lib-report')
5+
6+
module.exports = class CustomReporter extends ReportBase {
7+
constructor(opts) {
8+
super()
9+
10+
if (!opts.file)
11+
throw new Error('File is required as custom reporter parameter')
12+
13+
this.file = opts.file
14+
}
15+
16+
onStart(root, context) {
17+
this.contentWriter = context.writer.writeFile(this.file)
18+
this.contentWriter.println('Start of custom coverage report')
19+
}
20+
21+
onEnd() {
22+
this.contentWriter.println('End of custom coverage report')
23+
this.contentWriter.close()
24+
}
25+
}

‎test/coverage-test/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
"devDependencies": {
1515
"@ampproject/remapping": "^2.2.1",
1616
"@types/istanbul-lib-coverage": "^2.0.6",
17+
"@types/istanbul-lib-report": "^3.0.3",
1718
"@vitejs/plugin-vue": "latest",
1819
"@vitest/browser": "workspace:*",
1920
"@vitest/coverage-istanbul": "workspace:*",
2021
"@vitest/coverage-v8": "workspace:*",
2122
"@vue/test-utils": "latest",
2223
"happy-dom": "latest",
2324
"istanbul-lib-coverage": "^3.2.0",
25+
"istanbul-lib-report": "^3.0.1",
2426
"magicast": "^0.3.2",
2527
"vite": "latest",
2628
"vitest": "workspace:*",

‎test/coverage-test/test/configuration-options.test-d.ts

+8-14
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,7 @@ test('reporters, single', () => {
149149
assertType<Coverage>({ reporter: 'text-lcov' })
150150
assertType<Coverage>({ reporter: 'text-summary' })
151151
assertType<Coverage>({ reporter: 'text' })
152-
153-
// @ts-expect-error -- String reporters must be known built-in's
154-
assertType<Coverage>({ reporter: 'unknown-reporter' })
152+
assertType<Coverage>({ reporter: 'custom-reporter' })
155153
})
156154

157155
test('reporters, multiple', () => {
@@ -173,11 +171,8 @@ test('reporters, multiple', () => {
173171
],
174172
})
175173

176-
// @ts-expect-error -- List of string reporters must be known built-in's
177-
assertType<Coverage>({ reporter: ['unknown-reporter'] })
178-
179-
// @ts-expect-error -- ... and all reporters must be known
180-
assertType<Coverage>({ reporter: ['html', 'json', 'unknown-reporter'] })
174+
assertType<Coverage>({ reporter: ['custom-reporter'] })
175+
assertType<Coverage>({ reporter: ['html', 'json', 'custom-reporter'] })
181176
})
182177

183178
test('reporters, with options', () => {
@@ -196,6 +191,7 @@ test('reporters, with options', () => {
196191
['text-lcov', { projectRoot: 'string' }],
197192
['text-summary', { file: 'string' }],
198193
['text', { skipEmpty: true, skipFull: true, maxCols: 1 }],
194+
['custom-reporter', { 'someOption': true, 'some-other-custom-option': { width: 123 } }],
199195
],
200196
})
201197

@@ -209,12 +205,6 @@ test('reporters, with options', () => {
209205

210206
assertType<Coverage>({
211207
reporter: [
212-
// @ts-expect-error -- teamcity report option on html reporter
213-
['html', { blockName: 'string' }],
214-
215-
// @ts-expect-error -- html-spa report option on json reporter
216-
['json', { metricsToShow: ['branches'] }],
217-
218208
// @ts-expect-error -- second value should be object even though TS intellisense prompts types of reporters
219209
['lcov', 'html-spa'],
220210
],
@@ -225,9 +215,13 @@ test('reporters, mixed variations', () => {
225215
assertType<Coverage>({
226216
reporter: [
227217
'clover',
218+
'custom-reporter-1',
228219
['cobertura'],
220+
['custom-reporter-2'],
229221
['html-spa', {}],
222+
['custom-reporter-3', {}],
230223
['html', { verbose: true, subdir: 'string' }],
224+
['custom-reporter-4', { some: 'option', width: 123 }],
231225
],
232226
})
233227
})

‎test/coverage-test/vitest.config.ts

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export default defineConfig({
7979
['html'],
8080
['lcov', {}],
8181
['json', { file: 'custom-json-report-name.json' }],
82+
[resolve('./custom-reporter.cjs'), { file: 'custom-reporter-output.md' }],
8283
],
8384

8485
// These will be updated by tests and reseted back by generic.report.test.ts

0 commit comments

Comments
 (0)
Please sign in to comment.