[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(reporters): support custom options (#5111)

authored by

Ari Perkkiö and committed by
GitHub
(Feb 5, 2024, 5:56 PM +0200) fec9ca0b 0bf52533

+292 -58
+29 -1
docs/guide/reporters.md
··· 26 26 }) 27 27 ``` 28 28 29 + Some reporters can be customized by passing additional options to them. Reporter specific options are described in sections below. 30 + 31 + :::tip 32 + Since Vitest v1.3.0 33 + ::: 34 + 35 + ```ts 36 + export default defineConfig({ 37 + test: { 38 + reporters: [ 39 + 'default', 40 + ['junit', { suiteName: 'UI tests' }] 41 + ], 42 + }, 43 + }) 44 + ``` 45 + 29 46 ## Reporter Output 30 47 31 48 By default, Vitest's reporters will print their output to the terminal. When using the `json`, `html` or `junit` reporters, you can instead write your tests' output to a file by including an `outputFile` [configuration option](/config/#outputfile) either in your Vite configuration file or via CLI. ··· 234 251 </testsuite> 235 252 </testsuites> 236 253 ``` 237 - The outputted XML contains nested `testsuites` and `testcase` tags. You can use the environment variables `VITEST_JUNIT_SUITE_NAME` and `VITEST_JUNIT_CLASSNAME` to configure their `name` and `classname` attributes, respectively. 254 + 255 + The outputted XML contains nested `testsuites` and `testcase` tags. You can use the environment variables `VITEST_JUNIT_SUITE_NAME` and `VITEST_JUNIT_CLASSNAME` to configure their `name` and `classname` attributes, respectively. These can also be customized via reporter options: 256 + 257 + ```ts 258 + export default defineConfig({ 259 + test: { 260 + reporters: [ 261 + ['junit', { suiteName: 'custom suite name', classname: 'custom-classname' }] 262 + ] 263 + }, 264 + }) 265 + ``` 238 266 239 267 ### JSON Reporter 240 268
+4
test/reporters/vitest.config.ts
··· 7 7 chaiConfig: { 8 8 truncateThreshold: 0, 9 9 }, 10 + typecheck: { 11 + enabled: true, 12 + include: ['./tests/configuration-options.test-d.ts'], 13 + }, 10 14 }, 11 15 })
+10 -1
packages/ui/node/reporter.ts
··· 9 9 import type { File, ModuleGraphData, Reporter, ResolvedConfig, Vitest } from 'vitest' 10 10 import { getModuleGraph } from '../../vitest/src/utils/graph' 11 11 12 + export interface HTMLOptions { 13 + outputFile?: string 14 + } 15 + 12 16 interface PotentialConfig { 13 17 outputFile?: string | Partial<Record<string, string>> 14 18 } ··· 37 41 start = 0 38 42 ctx!: Vitest 39 43 reportUIPath!: string 44 + options: HTMLOptions 45 + 46 + constructor(options: HTMLOptions) { 47 + this.options = options 48 + } 40 49 41 50 async onInit(ctx: Vitest) { 42 51 this.ctx = ctx ··· 60 69 } 61 70 62 71 async writeReport(report: string) { 63 - const htmlFile = getOutputFile(this.ctx.config) || 'html/index.html' 72 + const htmlFile = this.options.outputFile || getOutputFile(this.ctx.config) || 'html/index.html' 64 73 const htmlFileName = basename(htmlFile) 65 74 const htmlDir = resolve(this.ctx.config.root, dirname(htmlFile)) 66 75
+8
test/reporters/src/custom-reporter.ts
··· 2 2 3 3 export default class TestReporter implements Reporter { 4 4 ctx!: Vitest 5 + options?: unknown 6 + 7 + constructor(options?: unknown) { 8 + this.options = options 9 + } 5 10 6 11 onInit(ctx: Vitest) { 7 12 this.ctx = ctx ··· 9 14 10 15 onFinished() { 11 16 this.ctx.logger.log('hello from custom reporter') 17 + 18 + if (this.options) 19 + this.ctx.logger.log(`custom reporter options ${JSON.stringify(this.options)}`) 12 20 } 13 21 }
+61
test/reporters/tests/configuration-options.test-d.ts
··· 1 + import { assertType, test } from 'vitest' 2 + import type { defineConfig } from 'vitest/config' 3 + 4 + type NarrowToTestConfig<T> = T extends { test?: any } ? NonNullable<T['test']> : never 5 + type Configuration = NonNullable<NarrowToTestConfig<(Parameters<typeof defineConfig>[0])>> 6 + 7 + test('reporters, single', () => { 8 + assertType<Configuration>({ reporters: 'basic' }) 9 + assertType<Configuration>({ reporters: 'default' }) 10 + assertType<Configuration>({ reporters: 'dot' }) 11 + assertType<Configuration>({ reporters: 'hanging-process' }) 12 + assertType<Configuration>({ reporters: 'html' }) 13 + assertType<Configuration>({ reporters: 'json' }) 14 + assertType<Configuration>({ reporters: 'junit' }) 15 + assertType<Configuration>({ reporters: 'tap' }) 16 + assertType<Configuration>({ reporters: 'tap-flat' }) 17 + assertType<Configuration>({ reporters: 'verbose' }) 18 + 19 + assertType<Configuration>({ reporters: 'custom-reporter' }) 20 + assertType<Configuration>({ reporters: './reporter.mjs' }) 21 + assertType<Configuration>({ reporters: { onFinished() {} } }) 22 + }) 23 + 24 + test('reporters, multiple', () => { 25 + assertType<Configuration>({ 26 + reporters: [ 27 + 'basic', 28 + 'default', 29 + 'dot', 30 + 'hanging-process', 31 + 'html', 32 + 'json', 33 + 'junit', 34 + 'tap', 35 + 'tap-flat', 36 + 'verbose', 37 + ], 38 + }) 39 + assertType<Configuration>({ reporters: ['custom-reporter'] }) 40 + assertType<Configuration>({ reporters: ['html', 'json', 'custom-reporter'] }) 41 + }) 42 + 43 + test('reporters, with options', () => { 44 + assertType<Configuration>({ 45 + reporters: [ 46 + ['json', { outputFile: 'test.json' }], 47 + ['junit', { classname: 'something', suiteName: 'Suite name', outputFile: 'test.json' }], 48 + ['vitest-sonar-reporter', { outputFile: 'report.xml' }], 49 + ], 50 + }) 51 + }) 52 + 53 + test('reporters, mixed variations', () => { 54 + assertType<Configuration>({ 55 + reporters: [ 56 + 'default', 57 + ['verbose'], 58 + ['json', { outputFile: 'test.json' }], 59 + ], 60 + }) 61 + })
+6
test/reporters/tests/custom-reporter.spec.ts
··· 75 75 expect(stdout).not.includes('hello from package reporter') 76 76 expect(stdout).includes('hello from custom reporter') 77 77 }, TIMEOUT) 78 + 79 + test('custom reporter with options', async () => { 80 + const { stdout } = await runVitest({ root, reporters: [[customTsReporterPath, { some: { custom: 'option here' } }]], include: ['tests/reporters.spec.ts'] }) 81 + expect(stdout).includes('hello from custom reporter') 82 + expect(stdout).includes('custom reporter options {"some":{"custom":"option here"}}') 83 + }, TIMEOUT) 78 84 })
+44 -16
test/reporters/tests/junit.test.ts
··· 43 43 test('emits <failure> if a test has a syntax error', async () => { 44 44 const { stdout } = await runVitest({ reporters: 'junit', root }, ['with-syntax-error']) 45 45 46 - let xml = stdout 46 + const xml = stabilizeReport(stdout) 47 47 48 - // clear timestamp and hostname 49 - xml = xml.replace(/timestamp="[^"]+"/, 'timestamp="TIMESTAMP"') 50 - xml = xml.replace(/hostname="[^"]+"/, 'hostname="HOSTNAME"') 51 - 52 - expect(xml).toContain('<testsuite name="with-syntax-error.test.js" timestamp="TIMESTAMP" hostname="HOSTNAME" tests="1" failures="1" errors="0" skipped="0" time="0">') 48 + expect(xml).toContain('<testsuite name="with-syntax-error.test.js" timestamp="..." hostname="..." tests="1" failures="1" errors="0" skipped="0" time="...">') 53 49 expect(xml).toContain('<failure') 54 50 }) 55 51 56 52 test('emits <failure> when beforeAll/afterAll failed', async () => { 57 - let { stdout } = await runVitest({ reporters: 'junit', root: './fixtures/suite-hook-failure' }) 58 - // reduct non-deterministic output 59 - stdout = stdout.replaceAll(/(timestamp|hostname|time)=".*?"/g, '$1="..."') 60 - expect(stdout).toMatchSnapshot() 53 + const { stdout } = await runVitest({ reporters: 'junit', root: './fixtures/suite-hook-failure' }) 54 + 55 + const xml = stabilizeReport(stdout) 56 + 57 + expect(xml).toMatchSnapshot() 61 58 }) 62 59 63 60 test('write testsuite name relative to root config', async () => { 64 - let { stdout } = await runVitest({ reporters: 'junit', root: './fixtures/better-testsuite-name' }) 65 - stdout = stdout.replaceAll(/(timestamp|hostname|time)=".*?"/g, '$1="..."') 61 + const { stdout } = await runVitest({ reporters: 'junit', root: './fixtures/better-testsuite-name' }) 66 62 67 - const space1 = '<testsuite name="space-1/test/base.test.ts" timestamp="..." hostname="..." tests="1" failures="0" errors="0" skipped="0" time="...">' 68 - const space2 = '<testsuite name="space-2/test/base.test.ts" timestamp="..." hostname="..." tests="1" failures="0" errors="0" skipped="0" time="...">' 69 - expect(stdout).toContain(space1) 70 - expect(stdout).toContain(space2) 63 + const xml = stabilizeReport(stdout) 64 + 65 + expect(xml).toContain('<testsuite name="space-1/test/base.test.ts" timestamp="..." hostname="..." tests="1" failures="0" errors="0" skipped="0" time="...">') 66 + expect(xml).toContain('<testsuite name="space-2/test/base.test.ts" timestamp="..." hostname="..." tests="1" failures="0" errors="0" skipped="0" time="...">') 71 67 }) 68 + 69 + test('options.classname changes classname property', async () => { 70 + const { stdout } = await runVitest({ 71 + reporters: [['junit', { classname: 'some-custom-classname' }]], 72 + root: './fixtures/default', 73 + include: ['a.test.ts'], 74 + }) 75 + 76 + const xml = stabilizeReport(stdout) 77 + 78 + // All classname attributes should have the custom value 79 + expect(xml.match(/<testcase classname="a\.test\.ts"/g)).toBeNull() 80 + expect(xml.match(/<testcase classname="/g)).toHaveLength(13) 81 + expect(xml.match(/<testcase classname="some-custom-classname"/g)).toHaveLength(13) 82 + }) 83 + 84 + test('options.suiteName changes name property', async () => { 85 + const { stdout } = await runVitest({ 86 + reporters: [['junit', { suiteName: 'some-custom-suiteName' }]], 87 + root: './fixtures/default', 88 + include: ['a.test.ts'], 89 + }) 90 + 91 + const xml = stabilizeReport(stdout) 92 + 93 + expect(xml).not.toContain('<testsuites name="vitest tests"') 94 + expect(xml).toContain('<testsuites name="some-custom-suiteName"') 95 + }) 96 + 97 + function stabilizeReport(report: string) { 98 + return report.replaceAll(/(timestamp|hostname|time)=".*?"/g, '$1="..."') 99 + }
+13 -13
test/reporters/tests/reporters.spec.ts
··· 41 41 42 42 test('JUnit reporter', async () => { 43 43 // Arrange 44 - const reporter = new JUnitReporter() 44 + const reporter = new JUnitReporter({}) 45 45 const context = getContext() 46 46 47 47 vi.mock('os', () => ({ ··· 60 60 61 61 test('JUnit reporter (no outputFile entry)', async () => { 62 62 // Arrange 63 - const reporter = new JUnitReporter() 63 + const reporter = new JUnitReporter({}) 64 64 const context = getContext() 65 65 context.vitest.config.outputFile = {} 66 66 ··· 80 80 81 81 test('JUnit reporter with outputFile', async () => { 82 82 // Arrange 83 - const reporter = new JUnitReporter() 83 + const reporter = new JUnitReporter({}) 84 84 const outputFile = resolve('report.xml') 85 85 const context = getContext() 86 86 context.vitest.config.outputFile = outputFile ··· 106 106 107 107 test('JUnit reporter with outputFile with XML in error message', async () => { 108 108 // Arrange 109 - const reporter = new JUnitReporter() 109 + const reporter = new JUnitReporter({}) 110 110 const outputFile = resolve('report_escape_msg_xml.xml') 111 111 const context = getContext() 112 112 context.vitest.config.outputFile = outputFile ··· 135 135 136 136 test('JUnit reporter with outputFile object', async () => { 137 137 // Arrange 138 - const reporter = new JUnitReporter() 138 + const reporter = new JUnitReporter({}) 139 139 const outputFile = resolve('report_object.xml') 140 140 const context = getContext() 141 141 context.vitest.config.outputFile = { ··· 163 163 164 164 test('JUnit reporter with outputFile in non-existing directory', async () => { 165 165 // Arrange 166 - const reporter = new JUnitReporter() 166 + const reporter = new JUnitReporter({}) 167 167 const rootDirectory = resolve('junitReportDirectory') 168 168 const outputFile = `${rootDirectory}/deeply/nested/report.xml` 169 169 const context = getContext() ··· 190 190 191 191 test('JUnit reporter with outputFile object in non-existing directory', async () => { 192 192 // Arrange 193 - const reporter = new JUnitReporter() 193 + const reporter = new JUnitReporter({}) 194 194 const rootDirectory = resolve('junitReportDirectory_object') 195 195 const outputFile = `${rootDirectory}/deeply/nested/report.xml` 196 196 const context = getContext() ··· 219 219 220 220 test('json reporter', async () => { 221 221 // Arrange 222 - const reporter = new JsonReporter() 222 + const reporter = new JsonReporter({}) 223 223 const context = getContext() 224 224 225 225 vi.setSystemTime(1642587001759) ··· 234 234 235 235 test('json reporter (no outputFile entry)', async () => { 236 236 // Arrange 237 - const reporter = new JsonReporter() 237 + const reporter = new JsonReporter({}) 238 238 const context = getContext() 239 239 context.vitest.config.outputFile = {} 240 240 ··· 250 250 251 251 test('json reporter with outputFile', async () => { 252 252 // Arrange 253 - const reporter = new JsonReporter() 253 + const reporter = new JsonReporter({}) 254 254 const outputFile = resolve('report.json') 255 255 const context = getContext() 256 256 context.vitest.config.outputFile = outputFile ··· 272 272 273 273 test('json reporter with outputFile object', async () => { 274 274 // Arrange 275 - const reporter = new JsonReporter() 275 + const reporter = new JsonReporter({}) 276 276 const outputFile = resolve('report_object.json') 277 277 const context = getContext() 278 278 context.vitest.config.outputFile = { ··· 296 296 297 297 test('json reporter with outputFile in non-existing directory', async () => { 298 298 // Arrange 299 - const reporter = new JsonReporter() 299 + const reporter = new JsonReporter({}) 300 300 const rootDirectory = resolve('jsonReportDirectory') 301 301 const outputFile = `${rootDirectory}/deeply/nested/report.json` 302 302 const context = getContext() ··· 319 319 320 320 test('json reporter with outputFile object in non-existing directory', async () => { 321 321 // Arrange 322 - const reporter = new JsonReporter() 322 + const reporter = new JsonReporter({}) 323 323 const rootDirectory = resolve('jsonReportDirectory_object') 324 324 const outputFile = `${rootDirectory}/deeply/nested/report.json` 325 325 const context = getContext()
+3 -3
test/reporters/tests/utils.test.ts
··· 24 24 }) 25 25 26 26 test('passing the name of a single built-in reporter returns a new instance', async () => { 27 - const promisedReporters = await createReporters(['default'], ctx) 27 + const promisedReporters = await createReporters([['default', {}]], ctx) 28 28 expect(promisedReporters).toHaveLength(1) 29 29 const reporter = promisedReporters[0] 30 30 expect(reporter).toBeInstanceOf(DefaultReporter) 31 31 }) 32 32 33 33 test('passing in the path to a custom reporter returns a new instance', async () => { 34 - const promisedReporters = await createReporters(([customReporterPath]), ctx) 34 + const promisedReporters = await createReporters(([[customReporterPath, {}]]), ctx) 35 35 expect(promisedReporters).toHaveLength(1) 36 36 const customReporter = promisedReporters[0] 37 37 expect(customReporter).toBeInstanceOf(TestReporter) 38 38 }) 39 39 40 40 test('passing in a mix of built-in and custom reporters works', async () => { 41 - const promisedReporters = await createReporters(['default', customReporterPath], ctx) 41 + const promisedReporters = await createReporters([['default', {}], [customReporterPath, {}]], ctx) 42 42 expect(promisedReporters).toHaveLength(2) 43 43 const defaultReporter = promisedReporters[0] 44 44 expect(defaultReporter).toBeInstanceOf(DefaultReporter)
+46 -4
packages/vitest/src/node/config.ts
··· 358 358 if (options.related) 359 359 resolved.related = toArray(options.related).map(file => resolve(resolved.root, file)) 360 360 361 + /* 362 + * Reporters can be defined in many different ways: 363 + * { reporter: 'json' } 364 + * { reporter: { onFinish() { method() } } } 365 + * { reporter: ['json', { onFinish() { method() } }] } 366 + * { reporter: [[ 'json' ]] } 367 + * { reporter: [[ 'json' ], 'html'] } 368 + * { reporter: [[ 'json', { outputFile: 'test.json' } ], 'html'] } 369 + */ 370 + if (options.reporters) { 371 + if (!Array.isArray(options.reporters)) { 372 + // Reporter name, e.g. { reporters: 'json' } 373 + if (typeof options.reporters === 'string') 374 + resolved.reporters = [[options.reporters, {}]] 375 + // Inline reporter e.g. { reporters: { onFinish() { method() } } } 376 + else 377 + resolved.reporters = [options.reporters] 378 + } 379 + // It's an array of reporters 380 + else { 381 + resolved.reporters = [] 382 + 383 + for (const reporter of options.reporters) { 384 + if (Array.isArray(reporter)) { 385 + // Reporter with options, e.g. { reporters: [ [ 'json', { outputFile: 'test.json' } ] ] } 386 + resolved.reporters.push([reporter[0], reporter[1] || {}]) 387 + } 388 + else if (typeof reporter === 'string') { 389 + // Reporter name in array, e.g. { reporters: ["html", "json"]} 390 + resolved.reporters.push([reporter, {}]) 391 + } 392 + else { 393 + // Inline reporter, e.g. { reporter: [{ onFinish() { method() } }] } 394 + resolved.reporters.push(reporter) 395 + } 396 + } 397 + } 398 + } 399 + 361 400 if (mode !== 'benchmark') { 362 401 // @ts-expect-error "reporter" is from CLI, should be absolute to the running directory 363 402 // it is passed down as "vitest --reporter ../reporter.js" 364 - const cliReporters = toArray(resolved.reporter || []).map((reporter: string) => { 403 + const reportersFromCLI = resolved.reporter 404 + 405 + const cliReporters = toArray(reportersFromCLI || []).map((reporter: string) => { 365 406 // ./reporter.js || ../reporter.js, but not .reporters/reporter.js 366 407 if (/^\.\.?\//.test(reporter)) 367 408 return resolve(process.cwd(), reporter) 368 409 return reporter 369 410 }) 370 - const reporters = cliReporters.length ? cliReporters : resolved.reporters 371 - resolved.reporters = Array.from(new Set(toArray(reporters as 'json'[]))).filter(Boolean) 411 + 412 + if (cliReporters.length) 413 + resolved.reporters = Array.from(new Set(toArray(cliReporters))).filter(Boolean).map(reporter => [reporter, {}]) 372 414 } 373 415 374 416 if (!resolved.reporters.length) 375 - resolved.reporters.push('default') 417 + resolved.reporters.push(['default', {}]) 376 418 377 419 if (resolved.changed) 378 420 resolved.passWithNoTests ??= true
+13 -3
packages/vitest/src/types/config.ts
··· 3 3 import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers' 4 4 import type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner' 5 5 import type { ViteNodeServerOptions } from 'vite-node' 6 - import type { BuiltinReporters } from '../node/reporters' 6 + import type { BuiltinReporterOptions, BuiltinReporters } from '../node/reporters' 7 7 import type { TestSequencerConstructor } from '../node/sequencers/types' 8 8 import type { ChaiConfig } from '../integrations/chai/config' 9 9 import type { CoverageOptions, ResolvedCoverageOptions } from './coverage' ··· 189 189 moduleDirectories?: string[] 190 190 } 191 191 192 + type InlineReporter = Reporter 193 + type ReporterName = BuiltinReporters | 'html' | (string & {}) 194 + type ReporterWithOptions<Name extends ReporterName = ReporterName> = 195 + Name extends keyof BuiltinReporterOptions 196 + ? BuiltinReporterOptions[Name] extends never 197 + ? [Name, {}] 198 + : [Name, Partial<BuiltinReporterOptions[Name]>] 199 + : [Name, Record<string, unknown>] 200 + 192 201 export interface InlineConfig { 193 202 /** 194 203 * Name of the project. Will be used to display in the reporter. ··· 365 374 * Custom reporter for output. Can contain one or more built-in report names, reporter instances, 366 375 * and/or paths to custom reporters. 367 376 */ 368 - reporters?: Arrayable<BuiltinReporters | 'html' | Reporter | Omit<string, BuiltinReporters>> 377 + reporters?: Arrayable<ReporterName | InlineReporter> | ((ReporterName | InlineReporter) | [ReporterName] | ReporterWithOptions)[] 369 378 379 + // TODO: v2.0.0 Remove in favor of custom reporter options, e.g. "reporters: [['json', { outputFile: 'some-dir/file.html' }]]" 370 380 /** 371 381 * Write test results to a file when the --reporter=json` or `--reporter=junit` option is also specified. 372 382 * Also definable individually per reporter by using an object instead. ··· 786 796 pool: Pool 787 797 poolOptions?: PoolOptions 788 798 789 - reporters: (Reporter | BuiltinReporters)[] 799 + reporters: (InlineReporter | ReporterWithOptions)[] 790 800 791 801 defines: Record<string, any> 792 802
+15 -2
packages/vitest/src/node/reporters/index.ts
··· 2 2 import { BasicReporter } from './basic' 3 3 import { DefaultReporter } from './default' 4 4 import { DotReporter } from './dot' 5 - import { JsonReporter } from './json' 5 + import { type JsonOptions, JsonReporter } from './json' 6 6 import { VerboseReporter } from './verbose' 7 7 import { TapReporter } from './tap' 8 - import { JUnitReporter } from './junit' 8 + import { type JUnitOptions, JUnitReporter } from './junit' 9 9 import { TapFlatReporter } from './tap-flat' 10 10 import { HangingProcessReporter } from './hanging-process' 11 11 import type { BaseReporter } from './base' ··· 38 38 } 39 39 40 40 export type BuiltinReporters = keyof typeof ReportersMap 41 + 42 + export interface BuiltinReporterOptions { 43 + default: never 44 + basic: never 45 + verbose: never 46 + dot: never 47 + json: JsonOptions 48 + tap: never 49 + 'tap-flat': never 50 + junit: JUnitOptions 51 + 'hanging-process': never 52 + html: { outputFile?: string } // TODO: Any better place for defining this UI package's reporter options? 53 + } 41 54 42 55 export * from './benchmark'
+10 -1
packages/vitest/src/node/reporters/json.ts
··· 62 62 // wasInterrupted: boolean 63 63 } 64 64 65 + export interface JsonOptions { 66 + outputFile?: string 67 + } 68 + 65 69 export class JsonReporter implements Reporter { 66 70 start = 0 67 71 ctx!: Vitest 72 + options: JsonOptions 73 + 74 + constructor(options: JsonOptions) { 75 + this.options = options 76 + } 68 77 69 78 onInit(ctx: Vitest): void { 70 79 this.ctx = ctx ··· 162 171 * @param report 163 172 */ 164 173 async writeReport(report: string) { 165 - const outputFile = getOutputFile(this.ctx.config, 'json') 174 + const outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, 'json') 166 175 167 176 if (outputFile) { 168 177 const reportFile = resolve(this.ctx.config.root, outputFile)
+16 -3
packages/vitest/src/node/reporters/junit.ts
··· 12 12 import { getOutputFile } from '../../utils/config-helpers' 13 13 import { IndentedLogger } from './renderers/indented-logger' 14 14 15 + export interface JUnitOptions { 16 + outputFile?: string 17 + classname?: string 18 + suiteName?: string 19 + } 20 + 15 21 function flattenTasks(task: Task, baseName = ''): Task[] { 16 22 const base = baseName ? `${baseName} > ` : '' 17 23 ··· 80 86 private logger!: IndentedLogger<Promise<void>> 81 87 private _timeStart = new Date() 82 88 private fileFd?: fs.FileHandle 89 + private options: JUnitOptions 90 + 91 + constructor(options: JUnitOptions) { 92 + this.options = options 93 + } 83 94 84 95 async onInit(ctx: Vitest): Promise<void> { 85 96 this.ctx = ctx 86 97 87 - const outputFile = getOutputFile(this.ctx.config, 'junit') 98 + const outputFile = this.options.outputFile ?? getOutputFile(this.ctx.config, 'junit') 88 99 89 100 if (outputFile) { 90 101 this.reportFile = resolve(this.ctx.config.root, outputFile) ··· 173 184 async writeTasks(tasks: Task[], filename: string): Promise<void> { 174 185 for (const task of tasks) { 175 186 await this.writeElement('testcase', { 176 - classname: process.env.VITEST_JUNIT_CLASSNAME ?? filename, 187 + // TODO: v2.0.0 Remove env variable in favor of custom reporter options, e.g. "reporters: [['json', { classname: 'something' }]]" 188 + classname: this.options.classname ?? process.env.VITEST_JUNIT_CLASSNAME ?? filename, 177 189 name: task.name, 178 190 time: getDuration(task), 179 191 }, async () => { ··· 258 270 stats.failures += file.stats.failures 259 271 return stats 260 272 }, { 261 - name: process.env.VITEST_JUNIT_SUITE_NAME || 'vitest tests', 273 + // TODO: v2.0.0 Remove env variable in favor of custom reporter options, e.g. "reporters: [['json', { suiteName: 'something' }]]" 274 + name: this.options.suiteName || process.env.VITEST_JUNIT_SUITE_NAME || 'vitest tests', 262 275 tests: 0, 263 276 failures: 0, 264 277 errors: 0, // we cannot detect those
+14 -11
packages/vitest/src/node/reporters/utils.ts
··· 1 1 import type { ViteNodeRunner } from 'vite-node/client' 2 - import type { Reporter, Vitest } from '../../types' 2 + import type { Reporter, ResolvedConfig, Vitest } from '../../types' 3 3 import { BenchmarkReportsMap, ReportersMap } from './index' 4 4 import type { BenchmarkBuiltinReporters, BuiltinReporters } from './index' 5 5 6 - async function loadCustomReporterModule<C extends Reporter>(path: string, runner: ViteNodeRunner): Promise<new () => C> { 6 + async function loadCustomReporterModule<C extends Reporter>(path: string, runner: ViteNodeRunner): Promise<new (options?: unknown) => C> { 7 7 let customReporterModule: { default: new () => C } 8 8 try { 9 9 customReporterModule = await runner.executeId(path) ··· 18 18 return customReporterModule.default 19 19 } 20 20 21 - function createReporters(reporterReferences: Array<string | Reporter | BuiltinReporters>, ctx: Vitest) { 21 + function createReporters(reporterReferences: ResolvedConfig['reporters'], ctx: Vitest) { 22 22 const runner = ctx.runner 23 23 const promisedReporters = reporterReferences.map(async (referenceOrInstance) => { 24 - if (typeof referenceOrInstance === 'string') { 25 - if (referenceOrInstance === 'html') { 24 + if (Array.isArray(referenceOrInstance)) { 25 + const [reporterName, reporterOptions] = referenceOrInstance 26 + 27 + if (reporterName === 'html') { 26 28 await ctx.packageInstaller.ensureInstalled('@vitest/ui', runner.root) 27 29 const CustomReporter = await loadCustomReporterModule('@vitest/ui/reporter', runner) 28 - return new CustomReporter() 30 + return new CustomReporter(reporterOptions) 29 31 } 30 - else if (referenceOrInstance in ReportersMap) { 31 - const BuiltinReporter = ReportersMap[referenceOrInstance as BuiltinReporters] 32 - return new BuiltinReporter() 32 + else if (reporterName in ReportersMap) { 33 + const BuiltinReporter = ReportersMap[reporterName as BuiltinReporters] 34 + return new BuiltinReporter(reporterOptions) 33 35 } 34 36 else { 35 - const CustomReporter = await loadCustomReporterModule(referenceOrInstance, runner) 36 - return new CustomReporter() 37 + const CustomReporter = await loadCustomReporterModule(reporterName, runner) 38 + return new CustomReporter(reporterOptions) 37 39 } 38 40 } 41 + 39 42 return referenceOrInstance 40 43 }) 41 44 return Promise.all(promisedReporters)