diff --git a/docs/config/index.md b/docs/config/index.md
index f367eb01e77c..0363fafdc3f9 100644
--- a/docs/config/index.md
+++ b/docs/config/index.md
@@ -1171,6 +1171,23 @@ The reporter has three different types:
}
```
+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.
+
+
+```ts
+ {
+ reporter: [
+ // Specify reporter using name of the NPM package
+ '@vitest/custom-coverage-reporter',
+ ['@vitest/custom-coverage-reporter', { someOption: true }],
+
+ // Specify reporter using local path
+ '/absolute/path/to/custom-reporter.cjs',
+ ['/absolute/path/to/custom-reporter.cjs', { someOption: true }],
+ ]
+ }
+```
+
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.
#### coverage.reportOnFailure 0.31.2+
@@ -2045,7 +2062,7 @@ Path to a [workspace](/guide/workspace) config file relative to [root](#root).
- **Type:** `boolean`
- **Default:** `true`
-- **CLI:** `--no-isolate`, `--isolate=false`
+- **CLI:** `--no-isolate`, `--isolate=false`
Run tests in an isolated environment. This option has no effect on `vmThreads` pool.
diff --git a/docs/guide/coverage.md b/docs/guide/coverage.md
index 9662dcc5022b..72447aba6e5e 100644
--- a/docs/guide/coverage.md
+++ b/docs/guide/coverage.md
@@ -70,6 +70,55 @@ export default defineConfig({
})
```
+## Custom Coverage Reporter
+
+You can use custom coverage reporters by passing either the name of the package or absolute path in `test.coverage.reporter`:
+
+```ts
+// vitest.config.ts
+import { defineConfig } from 'vitest/config'
+
+export default defineConfig({
+ test: {
+ coverage: {
+ reporter: [
+ // Specify reporter using name of the NPM package
+ ['@vitest/custom-coverage-reporter', { someOption: true }],
+
+ // Specify reporter using local path
+ '/absolute/path/to/custom-reporter.cjs',
+ ],
+ },
+ },
+})
+```
+
+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.
+
+```js
+// custom-reporter.cjs
+const { ReportBase } = require('istanbul-lib-report')
+
+module.exports = class CustomReporter extends ReportBase {
+ constructor(opts) {
+ super()
+
+ // Options passed from configuration are available here
+ this.file = opts.file
+ }
+
+ onStart(root, context) {
+ this.contentWriter = context.writer.writeFile(this.file)
+ this.contentWriter.println('Start of custom coverage report')
+ }
+
+ onEnd() {
+ this.contentWriter.println('End of custom coverage report')
+ this.contentWriter.close()
+ }
+}
+```
+
## Custom Coverage Provider
It's also possible to provide your custom coverage provider by passing `'custom'` in `test.coverage.provider`:
diff --git a/packages/coverage-istanbul/src/provider.ts b/packages/coverage-istanbul/src/provider.ts
index 1eb6a70597c2..c25a147f7e73 100644
--- a/packages/coverage-istanbul/src/provider.ts
+++ b/packages/coverage-istanbul/src/provider.ts
@@ -206,7 +206,8 @@ export class IstanbulCoverageProvider extends BaseCoverageProvider implements Co
this.ctx.logger.log(c.blue(' % ') + c.dim('Coverage report from ') + c.yellow(this.name))
for (const reporter of this.options.reporter) {
- reports.create(reporter[0], {
+ // Type assertion required for custom reporters
+ reports.create(reporter[0] as Parameters[0], {
skipFull: this.options.skipFull,
projectRoot: this.ctx.config.root,
...reporter[1],
diff --git a/packages/coverage-v8/src/provider.ts b/packages/coverage-v8/src/provider.ts
index d3f95a874886..19b80986fcf0 100644
--- a/packages/coverage-v8/src/provider.ts
+++ b/packages/coverage-v8/src/provider.ts
@@ -198,7 +198,8 @@ export class V8CoverageProvider extends BaseCoverageProvider implements Coverage
this.ctx.logger.log(c.blue(' % ') + c.dim('Coverage report from ') + c.yellow(this.name))
for (const reporter of this.options.reporter) {
- reports.create(reporter[0], {
+ // Type assertion required for custom reporters
+ reports.create(reporter[0] as Parameters[0], {
skipFull: this.options.skipFull,
projectRoot: this.ctx.config.root,
...reporter[1],
diff --git a/packages/ui/node/index.ts b/packages/ui/node/index.ts
index 60841d02b8f3..e15a2146c0ff 100644
--- a/packages/ui/node/index.ts
+++ b/packages/ui/node/index.ts
@@ -57,7 +57,7 @@ function resolveCoverageFolder(ctx: Vitest) {
? htmlReporter[1].subdir
: undefined
- if (!subdir)
+ if (!subdir || typeof subdir !== 'string')
return [root, `/${basename(root)}/`]
return [resolve(root, subdir), `/${basename(root)}/${subdir}/`]
diff --git a/packages/vitest/src/types/coverage.ts b/packages/vitest/src/types/coverage.ts
index 73006b68287f..e7a9e1a42c8e 100644
--- a/packages/vitest/src/types/coverage.ts
+++ b/packages/vitest/src/types/coverage.ts
@@ -52,14 +52,14 @@ export interface CoverageProviderModule {
stopCoverage?(): unknown | Promise
}
-export type CoverageReporter = keyof ReportOptions
+export type CoverageReporter = keyof ReportOptions | (string & {})
type CoverageReporterWithOptions =
- ReporterName extends CoverageReporter
+ ReporterName extends keyof ReportOptions
? ReportOptions[ReporterName] extends never
? [ReporterName, {}] // E.g. the "none" reporter
: [ReporterName, Partial]
- : never
+ : [ReporterName, Record]
type Provider = 'v8' | 'istanbul' | 'custom' | undefined
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index cefdd64f1b74..b1191d9c5cc8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1590,6 +1590,9 @@ importers:
'@types/istanbul-lib-coverage':
specifier: ^2.0.6
version: 2.0.6
+ '@types/istanbul-lib-report':
+ specifier: ^3.0.3
+ version: 3.0.3
'@vitejs/plugin-vue':
specifier: latest
version: 4.5.0(vite@5.0.2)(vue@3.3.8)
@@ -1611,6 +1614,9 @@ importers:
istanbul-lib-coverage:
specifier: ^3.2.0
version: 3.2.0
+ istanbul-lib-report:
+ specifier: ^3.0.1
+ version: 3.0.1
magicast:
specifier: ^0.3.2
version: 0.3.2
diff --git a/test/coverage-test/coverage-report-tests/generic.report.test.ts b/test/coverage-test/coverage-report-tests/generic.report.test.ts
index b8c016dbb8a8..d7e5821b6d3d 100644
--- a/test/coverage-test/coverage-report-tests/generic.report.test.ts
+++ b/test/coverage-test/coverage-report-tests/generic.report.test.ts
@@ -31,6 +31,20 @@ test('lcov report', async () => {
expect(lcovReportFiles).toContain('index.html')
})
+test('custom report', async () => {
+ const coveragePath = resolve('./coverage')
+ const files = fs.readdirSync(coveragePath)
+
+ expect(files).toContain('custom-reporter-output.md')
+
+ const content = fs.readFileSync(resolve(coveragePath, 'custom-reporter-output.md'), 'utf-8')
+ expect(content).toMatchInlineSnapshot(`
+ "Start of custom coverage report
+ End of custom coverage report
+ "
+ `)
+})
+
test('all includes untested files', () => {
const coveragePath = resolve('./coverage/src')
const files = fs.readdirSync(coveragePath)
diff --git a/test/coverage-test/custom-reporter.cjs b/test/coverage-test/custom-reporter.cjs
new file mode 100644
index 000000000000..c8c46c00a9e8
--- /dev/null
+++ b/test/coverage-test/custom-reporter.cjs
@@ -0,0 +1,25 @@
+/* Istanbul uses `require`: https://github.com/istanbuljs/istanbuljs/blob/5584b50305a6a17d3573aea25c84e254d4a08b65/packages/istanbul-reports/index.js#L19 */
+
+'use strict'
+const { ReportBase } = require('istanbul-lib-report')
+
+module.exports = class CustomReporter extends ReportBase {
+ constructor(opts) {
+ super()
+
+ if (!opts.file)
+ throw new Error('File is required as custom reporter parameter')
+
+ this.file = opts.file
+ }
+
+ onStart(root, context) {
+ this.contentWriter = context.writer.writeFile(this.file)
+ this.contentWriter.println('Start of custom coverage report')
+ }
+
+ onEnd() {
+ this.contentWriter.println('End of custom coverage report')
+ this.contentWriter.close()
+ }
+}
diff --git a/test/coverage-test/package.json b/test/coverage-test/package.json
index fbdddf72c679..48f5f21b4b95 100644
--- a/test/coverage-test/package.json
+++ b/test/coverage-test/package.json
@@ -14,6 +14,7 @@
"devDependencies": {
"@ampproject/remapping": "^2.2.1",
"@types/istanbul-lib-coverage": "^2.0.6",
+ "@types/istanbul-lib-report": "^3.0.3",
"@vitejs/plugin-vue": "latest",
"@vitest/browser": "workspace:*",
"@vitest/coverage-istanbul": "workspace:*",
@@ -21,6 +22,7 @@
"@vue/test-utils": "latest",
"happy-dom": "latest",
"istanbul-lib-coverage": "^3.2.0",
+ "istanbul-lib-report": "^3.0.1",
"magicast": "^0.3.2",
"vite": "latest",
"vitest": "workspace:*",
diff --git a/test/coverage-test/test/configuration-options.test-d.ts b/test/coverage-test/test/configuration-options.test-d.ts
index d4dad6bb4771..f5f60cdcf045 100644
--- a/test/coverage-test/test/configuration-options.test-d.ts
+++ b/test/coverage-test/test/configuration-options.test-d.ts
@@ -149,9 +149,7 @@ test('reporters, single', () => {
assertType({ reporter: 'text-lcov' })
assertType({ reporter: 'text-summary' })
assertType({ reporter: 'text' })
-
- // @ts-expect-error -- String reporters must be known built-in's
- assertType({ reporter: 'unknown-reporter' })
+ assertType({ reporter: 'custom-reporter' })
})
test('reporters, multiple', () => {
@@ -173,11 +171,8 @@ test('reporters, multiple', () => {
],
})
- // @ts-expect-error -- List of string reporters must be known built-in's
- assertType({ reporter: ['unknown-reporter'] })
-
- // @ts-expect-error -- ... and all reporters must be known
- assertType({ reporter: ['html', 'json', 'unknown-reporter'] })
+ assertType({ reporter: ['custom-reporter'] })
+ assertType({ reporter: ['html', 'json', 'custom-reporter'] })
})
test('reporters, with options', () => {
@@ -196,6 +191,7 @@ test('reporters, with options', () => {
['text-lcov', { projectRoot: 'string' }],
['text-summary', { file: 'string' }],
['text', { skipEmpty: true, skipFull: true, maxCols: 1 }],
+ ['custom-reporter', { 'someOption': true, 'some-other-custom-option': { width: 123 } }],
],
})
@@ -209,12 +205,6 @@ test('reporters, with options', () => {
assertType({
reporter: [
- // @ts-expect-error -- teamcity report option on html reporter
- ['html', { blockName: 'string' }],
-
- // @ts-expect-error -- html-spa report option on json reporter
- ['json', { metricsToShow: ['branches'] }],
-
// @ts-expect-error -- second value should be object even though TS intellisense prompts types of reporters
['lcov', 'html-spa'],
],
@@ -225,9 +215,13 @@ test('reporters, mixed variations', () => {
assertType({
reporter: [
'clover',
+ 'custom-reporter-1',
['cobertura'],
+ ['custom-reporter-2'],
['html-spa', {}],
+ ['custom-reporter-3', {}],
['html', { verbose: true, subdir: 'string' }],
+ ['custom-reporter-4', { some: 'option', width: 123 }],
],
})
})
diff --git a/test/coverage-test/vitest.config.ts b/test/coverage-test/vitest.config.ts
index a95ca0ed051d..a6aae64bead2 100644
--- a/test/coverage-test/vitest.config.ts
+++ b/test/coverage-test/vitest.config.ts
@@ -79,6 +79,7 @@ export default defineConfig({
['html'],
['lcov', {}],
['json', { file: 'custom-json-report-name.json' }],
+ [resolve('./custom-reporter.cjs'), { file: 'custom-reporter-output.md' }],
],
// These will be updated by tests and reseted back by generic.report.test.ts