Skip to content

Commit f817618

Browse files
authoredFeb 21, 2023
feat(coverage): add support for coverage reporter options (#2690)
1 parent 8ab33f1 commit f817618

File tree

12 files changed

+170
-35
lines changed

12 files changed

+170
-35
lines changed
 

‎docs/config/index.md

+17-2
Original file line numberDiff line numberDiff line change
@@ -725,13 +725,28 @@ Directory to write coverage report to.
725725

726726
#### reporter
727727

728-
- **Type:** `string | string[]`
728+
- **Type:** `string | string[] | [string, {}][]`
729729
- **Default:** `['text', 'html', 'clover', 'json']`
730730
- **Available for providers:** `'c8' | 'istanbul'`
731731
- **CLI:** `--coverage.reporter=<reporter>`, `--coverage.reporter=<reporter1> --coverage.reporter=<reporter2>`
732732

733-
Coverage reporters to use. See [istanbul documentation](https://istanbul.js.org/docs/advanced/alternative-reporters/) for detailed list of all reporters.
733+
Coverage reporters to use. See [istanbul documentation](https://istanbul.js.org/docs/advanced/alternative-reporters/) for detailed list of all reporters. See [`@types/istanbul-reporter`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/276d95e4304b3670eaf6e8e5a7ea9e265a14e338/types/istanbul-reports/index.d.ts) for details about reporter specific options.
734+
735+
The reporter has three different types:
734736

737+
- A single reporter: `{ reporter: 'html' }`
738+
- Multiple reporters without options: `{ reporter: ['html', 'json'] }`
739+
- A single or multiple reporters with reporter options:
740+
<!-- eslint-skip -->
741+
```ts
742+
{
743+
reporter: [
744+
['lcov', { 'projectRoot': './src' }],
745+
['json', { 'file': 'coverage.json' }],
746+
['text']
747+
]
748+
}
749+
```
735750

736751
#### skipFull
737752

‎package.json

-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
"@vitest/coverage-istanbul": "workspace:*",
4747
"@vitest/ui": "workspace:*",
4848
"bumpp": "^8.2.1",
49-
"c8": "^7.12.0",
5049
"esbuild": "^0.16.16",
5150
"eslint": "^8.31.0",
5251
"esno": "^0.16.3",

‎packages/coverage-c8/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"vitest": ">=0.29.0 <1"
4646
},
4747
"dependencies": {
48-
"c8": "^7.12.0",
48+
"c8": "^7.13.0",
4949
"picocolors": "^1.0.0",
5050
"std-env": "^3.3.1"
5151
},

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

+31-2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ export class C8CoverageProvider implements CoverageProvider {
5151
const options: ConstructorParameters<typeof Report>[0] = {
5252
...this.options,
5353
all: this.options.all && allTestsRun,
54+
reporter: this.options.reporter.map(([reporterName]) => reporterName),
55+
reporterOptions: this.options.reporter.reduce((all, [name, options]) => ({
56+
...all,
57+
[name]: {
58+
skipFull: this.options.skipFull,
59+
projectRoot: this.ctx.config.root,
60+
...options,
61+
},
62+
}), {}),
5463
}
5564

5665
const report = createReport(options)
@@ -152,7 +161,6 @@ export class C8CoverageProvider implements CoverageProvider {
152161

153162
function resolveC8Options(options: CoverageC8Options, root: string): Options {
154163
const reportsDirectory = resolve(root, options.reportsDirectory || coverageConfigDefaults.reportsDirectory)
155-
const reporter = options.reporter || coverageConfigDefaults.reporter
156164

157165
const resolved: Options = {
158166
...coverageConfigDefaults,
@@ -166,7 +174,7 @@ function resolveC8Options(options: CoverageC8Options, root: string): Options {
166174

167175
// Resolved fields
168176
provider: 'c8',
169-
reporter: Array.isArray(reporter) ? reporter : [reporter],
177+
reporter: resolveReporters(options.reporter || coverageConfigDefaults.reporter),
170178
reportsDirectory,
171179
}
172180

@@ -179,3 +187,24 @@ function resolveC8Options(options: CoverageC8Options, root: string): Options {
179187

180188
return resolved
181189
}
190+
191+
function resolveReporters(configReporters: NonNullable<CoverageC8Options['reporter']>): Options['reporter'] {
192+
// E.g. { reporter: "html" }
193+
if (!Array.isArray(configReporters))
194+
return [[configReporters, {}]]
195+
196+
const resolvedReporters: Options['reporter'] = []
197+
198+
for (const reporter of configReporters) {
199+
if (Array.isArray(reporter)) {
200+
// E.g. { reporter: [ ["html", { skipEmpty: true }], ["lcov"], ["json", { file: "map.json" }] ]}
201+
resolvedReporters.push([reporter[0], reporter[1] || {}])
202+
}
203+
else {
204+
// E.g. { reporter: ["html", "json"]}
205+
resolvedReporters.push([reporter, {}])
206+
}
207+
}
208+
209+
return resolvedReporters
210+
}

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

+24-3
Original file line numberDiff line numberDiff line change
@@ -123,9 +123,10 @@ export class IstanbulCoverageProvider implements CoverageProvider {
123123
})
124124

125125
for (const reporter of this.options.reporter) {
126-
reports.create(reporter, {
126+
reports.create(reporter[0], {
127127
skipFull: this.options.skipFull,
128128
projectRoot: this.ctx.config.root,
129+
...reporter[1],
129130
}).execute(context)
130131
}
131132

@@ -221,7 +222,6 @@ export class IstanbulCoverageProvider implements CoverageProvider {
221222

222223
function resolveIstanbulOptions(options: CoverageIstanbulOptions, root: string): Options {
223224
const reportsDirectory = resolve(root, options.reportsDirectory || coverageConfigDefaults.reportsDirectory)
224-
const reporter = options.reporter || coverageConfigDefaults.reporter
225225

226226
const resolved: Options = {
227227
...coverageConfigDefaults,
@@ -232,7 +232,7 @@ function resolveIstanbulOptions(options: CoverageIstanbulOptions, root: string):
232232
// Resolved fields
233233
provider: 'istanbul',
234234
reportsDirectory,
235-
reporter: Array.isArray(reporter) ? reporter : [reporter],
235+
reporter: resolveReporters(options.reporter || coverageConfigDefaults.reporter),
236236
}
237237

238238
return resolved
@@ -287,3 +287,24 @@ function isEmptyCoverageRange(range: libCoverage.Range) {
287287
|| range.end.column === undefined
288288
)
289289
}
290+
291+
function resolveReporters(configReporters: NonNullable<CoverageIstanbulOptions['reporter']>): Options['reporter'] {
292+
// E.g. { reporter: "html" }
293+
if (!Array.isArray(configReporters))
294+
return [[configReporters, {}]]
295+
296+
const resolvedReporters: Options['reporter'] = []
297+
298+
for (const reporter of configReporters) {
299+
if (Array.isArray(reporter)) {
300+
// E.g. { reporter: [ ["html", { skipEmpty: true }], ["lcov"], ["json", { file: "map.json" }] ]}
301+
resolvedReporters.push([reporter[0], reporter[1] || {}])
302+
}
303+
else {
304+
// E.g. { reporter: ["html", "json"]}
305+
resolvedReporters.push([reporter, {}])
306+
}
307+
}
308+
309+
return resolvedReporters
310+
}

‎packages/vitest/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@
144144
"@edge-runtime/vm": "2.0.2",
145145
"@sinonjs/fake-timers": "^10.0.2",
146146
"@types/diff": "^5.0.2",
147+
"@types/istanbul-reports": "^3.0.1",
147148
"@types/jsdom": "^21.1.0",
148149
"@types/micromatch": "^4.0.2",
149150
"@types/natural-compare": "^1.4.1",

‎packages/vitest/src/defaults.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const coverageConfigDefaults: ResolvedCoverageOptions = {
3333
cleanOnRerun: true,
3434
reportsDirectory: './coverage',
3535
exclude: defaultCoverageExcludes,
36-
reporter: ['text', 'html', 'clover', 'json'],
36+
reporter: [['text', {}], ['html', {}], ['clover', {}], ['json', {}]],
3737
// default extensions used by c8, plus '.vue' and '.svelte'
3838
// see https://github.com/istanbuljs/schema/blob/master/default-extension.js
3939
extension: ['.js', '.cjs', '.mjs', '.ts', '.mts', '.cts', '.tsx', '.jsx', '.vue', '.svelte'],

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

+11-17
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { TransformPluginContext, TransformResult } from 'rollup'
2+
import type { ReportOptions } from 'istanbul-reports'
23
import type { Vitest } from '../node'
34
import type { Arrayable } from './general'
45
import type { AfterSuiteRunMeta } from './worker'
@@ -48,20 +49,14 @@ export interface CoverageProviderModule {
4849
stopCoverage?(): unknown | Promise<unknown>
4950
}
5051

51-
export type CoverageReporter =
52-
| 'clover'
53-
| 'cobertura'
54-
| 'html-spa'
55-
| 'html'
56-
| 'json-summary'
57-
| 'json'
58-
| 'lcov'
59-
| 'lcovonly'
60-
| 'none'
61-
| 'teamcity'
62-
| 'text-lcov'
63-
| 'text-summary'
64-
| 'text'
52+
export type CoverageReporter = keyof ReportOptions
53+
54+
type CoverageReporterWithOptions<ReporterName extends CoverageReporter = CoverageReporter> =
55+
ReporterName extends CoverageReporter
56+
? ReportOptions[ReporterName] extends never
57+
? [ReporterName, {}] // E.g. the "none" reporter
58+
: [ReporterName, Partial<ReportOptions[ReporterName]>]
59+
: never
6560

6661
type Provider = 'c8' | 'istanbul' | 'custom' | undefined
6762

@@ -79,14 +74,13 @@ type FieldsWithDefaultValues =
7974
| 'reportsDirectory'
8075
| 'exclude'
8176
| 'extension'
82-
| 'reporter'
8377

8478
export type ResolvedCoverageOptions<T extends Provider = Provider> =
8579
& CoverageOptions<T>
8680
& Required<Pick<CoverageOptions<T>, FieldsWithDefaultValues>>
8781
// Resolved fields which may have different typings as public configuration API has
8882
& {
89-
reporter: CoverageReporter[]
83+
reporter: CoverageReporterWithOptions[]
9084
}
9185

9286
export interface BaseCoverageOptions {
@@ -148,7 +142,7 @@ export interface BaseCoverageOptions {
148142
*
149143
* @default ['text', 'html', 'clover', 'json']
150144
*/
151-
reporter?: Arrayable<CoverageReporter>
145+
reporter?: Arrayable<CoverageReporter> | (CoverageReporter | [CoverageReporter] | CoverageReporterWithOptions)[]
152146

153147
/**
154148
* Do not show files with 100% statement, branch, and function coverage

‎pnpm-lock.yaml

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

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ interface CoverageFinalJson {
1818
* Normalizes paths to keep contents consistent between OS's
1919
*/
2020
export async function readCoverageJson() {
21-
const jsonReport = JSON.parse(readFileSync('./coverage/coverage-final.json', 'utf8')) as CoverageFinalJson
21+
const jsonReport = JSON.parse(readFileSync('./coverage/custom-json-report-name.json', 'utf8')) as CoverageFinalJson
2222

2323
const normalizedReport: CoverageFinalJson['default'] = {}
2424

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

+53-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ test('provider module', () => {
102102
enabled: true,
103103
exclude: ['string'],
104104
extension: ['string'],
105-
reporter: ['html', 'json'],
105+
reporter: [['html', {}], ['json', { file: 'string' }]],
106106
reportsDirectory: 'string',
107107
}
108108
},
@@ -165,3 +165,55 @@ test('reporters, multiple', () => {
165165
// @ts-expect-error -- ... and all reporters must be known
166166
assertType<Coverage>({ reporter: ['html', 'json', 'unknown-reporter'] })
167167
})
168+
169+
test('reporters, with options', () => {
170+
assertType<Coverage>({
171+
reporter: [
172+
['clover', { projectRoot: 'string', file: 'string' }],
173+
['cobertura', { projectRoot: 'string', file: 'string' }],
174+
['html-spa', { metricsToShow: ['branches', 'functions'], verbose: true, subdir: 'string' }],
175+
['html', { verbose: true, subdir: 'string' }],
176+
['json-summary', { file: 'string' }],
177+
['json', { file: 'string' }],
178+
['lcov', { projectRoot: 'string', file: 'string' }],
179+
['lcovonly', { projectRoot: 'string', file: 'string' }],
180+
['none'],
181+
['teamcity', { blockName: 'string' }],
182+
['text-lcov', { projectRoot: 'string' }],
183+
['text-summary', { file: 'string' }],
184+
['text', { skipEmpty: true, skipFull: true, maxCols: 1 }],
185+
],
186+
})
187+
188+
assertType<Coverage>({
189+
reporter: [
190+
['html', { subdir: 'string' }],
191+
['json'],
192+
['lcov', { projectRoot: 'string' }],
193+
],
194+
})
195+
196+
assertType<Coverage>({
197+
reporter: [
198+
// @ts-expect-error -- teamcity report option on html reporter
199+
['html', { blockName: 'string' }],
200+
201+
// @ts-expect-error -- html-spa report option on json reporter
202+
['json', { metricsToShow: ['branches'] }],
203+
204+
// @ts-expect-error -- second value should be object even though TS intellisense prompts types of reporters
205+
['lcov', 'html-spa'],
206+
],
207+
})
208+
})
209+
210+
test('reporters, mixed variations', () => {
211+
assertType<Coverage>({
212+
reporter: [
213+
'clover',
214+
['cobertura'],
215+
['html-spa', {}],
216+
['html', { verbose: true, subdir: 'string' }],
217+
],
218+
})
219+
})

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ export default defineConfig({
1919
include: ['src/**'],
2020
clean: true,
2121
all: true,
22-
reporter: ['html', 'text', 'lcov', 'json'],
22+
reporter: [
23+
'text',
24+
['html'],
25+
['lcov', {}],
26+
['json', { file: 'custom-json-report-name.json' }],
27+
],
2328
},
2429
setupFiles: [
2530
resolve(__dirname, './setup.ts'),

0 commit comments

Comments
 (0)
Please sign in to comment.