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