···2626})
2727```
28282929+Some reporters can be customized by passing additional options to them. Reporter specific options are described in sections below.
3030+3131+:::tip
3232+Since Vitest v1.3.0
3333+:::
3434+3535+```ts
3636+export default defineConfig({
3737+ test: {
3838+ reporters: [
3939+ 'default',
4040+ ['junit', { suiteName: 'UI tests' }]
4141+ ],
4242+ },
4343+})
4444+```
4545+2946## Reporter Output
30473148By 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.
···234251 </testsuite>
235252</testsuites>
236253```
237237-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.
254254+255255+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:
256256+257257+```ts
258258+export default defineConfig({
259259+ test: {
260260+ reporters: [
261261+ ['junit', { suiteName: 'custom suite name', classname: 'custom-classname' }]
262262+ ]
263263+ },
264264+})
265265+```
238266239267### JSON Reporter
240268
···2424 })
25252626 test('passing the name of a single built-in reporter returns a new instance', async () => {
2727- const promisedReporters = await createReporters(['default'], ctx)
2727+ const promisedReporters = await createReporters([['default', {}]], ctx)
2828 expect(promisedReporters).toHaveLength(1)
2929 const reporter = promisedReporters[0]
3030 expect(reporter).toBeInstanceOf(DefaultReporter)
3131 })
32323333 test('passing in the path to a custom reporter returns a new instance', async () => {
3434- const promisedReporters = await createReporters(([customReporterPath]), ctx)
3434+ const promisedReporters = await createReporters(([[customReporterPath, {}]]), ctx)
3535 expect(promisedReporters).toHaveLength(1)
3636 const customReporter = promisedReporters[0]
3737 expect(customReporter).toBeInstanceOf(TestReporter)
3838 })
39394040 test('passing in a mix of built-in and custom reporters works', async () => {
4141- const promisedReporters = await createReporters(['default', customReporterPath], ctx)
4141+ const promisedReporters = await createReporters([['default', {}], [customReporterPath, {}]], ctx)
4242 expect(promisedReporters).toHaveLength(2)
4343 const defaultReporter = promisedReporters[0]
4444 expect(defaultReporter).toBeInstanceOf(DefaultReporter)
+46-4
packages/vitest/src/node/config.ts
···358358 if (options.related)
359359 resolved.related = toArray(options.related).map(file => resolve(resolved.root, file))
360360361361+ /*
362362+ * Reporters can be defined in many different ways:
363363+ * { reporter: 'json' }
364364+ * { reporter: { onFinish() { method() } } }
365365+ * { reporter: ['json', { onFinish() { method() } }] }
366366+ * { reporter: [[ 'json' ]] }
367367+ * { reporter: [[ 'json' ], 'html'] }
368368+ * { reporter: [[ 'json', { outputFile: 'test.json' } ], 'html'] }
369369+ */
370370+ if (options.reporters) {
371371+ if (!Array.isArray(options.reporters)) {
372372+ // Reporter name, e.g. { reporters: 'json' }
373373+ if (typeof options.reporters === 'string')
374374+ resolved.reporters = [[options.reporters, {}]]
375375+ // Inline reporter e.g. { reporters: { onFinish() { method() } } }
376376+ else
377377+ resolved.reporters = [options.reporters]
378378+ }
379379+ // It's an array of reporters
380380+ else {
381381+ resolved.reporters = []
382382+383383+ for (const reporter of options.reporters) {
384384+ if (Array.isArray(reporter)) {
385385+ // Reporter with options, e.g. { reporters: [ [ 'json', { outputFile: 'test.json' } ] ] }
386386+ resolved.reporters.push([reporter[0], reporter[1] || {}])
387387+ }
388388+ else if (typeof reporter === 'string') {
389389+ // Reporter name in array, e.g. { reporters: ["html", "json"]}
390390+ resolved.reporters.push([reporter, {}])
391391+ }
392392+ else {
393393+ // Inline reporter, e.g. { reporter: [{ onFinish() { method() } }] }
394394+ resolved.reporters.push(reporter)
395395+ }
396396+ }
397397+ }
398398+ }
399399+361400 if (mode !== 'benchmark') {
362401 // @ts-expect-error "reporter" is from CLI, should be absolute to the running directory
363402 // it is passed down as "vitest --reporter ../reporter.js"
364364- const cliReporters = toArray(resolved.reporter || []).map((reporter: string) => {
403403+ const reportersFromCLI = resolved.reporter
404404+405405+ const cliReporters = toArray(reportersFromCLI || []).map((reporter: string) => {
365406 // ./reporter.js || ../reporter.js, but not .reporters/reporter.js
366407 if (/^\.\.?\//.test(reporter))
367408 return resolve(process.cwd(), reporter)
368409 return reporter
369410 })
370370- const reporters = cliReporters.length ? cliReporters : resolved.reporters
371371- resolved.reporters = Array.from(new Set(toArray(reporters as 'json'[]))).filter(Boolean)
411411+412412+ if (cliReporters.length)
413413+ resolved.reporters = Array.from(new Set(toArray(cliReporters))).filter(Boolean).map(reporter => [reporter, {}])
372414 }
373415374416 if (!resolved.reporters.length)
375375- resolved.reporters.push('default')
417417+ resolved.reporters.push(['default', {}])
376418377419 if (resolved.changed)
378420 resolved.passWithNoTests ??= true
+13-3
packages/vitest/src/types/config.ts
···33import type { FakeTimerInstallOpts } from '@sinonjs/fake-timers'
44import type { SequenceHooks, SequenceSetupFiles } from '@vitest/runner'
55import type { ViteNodeServerOptions } from 'vite-node'
66-import type { BuiltinReporters } from '../node/reporters'
66+import type { BuiltinReporterOptions, BuiltinReporters } from '../node/reporters'
77import type { TestSequencerConstructor } from '../node/sequencers/types'
88import type { ChaiConfig } from '../integrations/chai/config'
99import type { CoverageOptions, ResolvedCoverageOptions } from './coverage'
···189189 moduleDirectories?: string[]
190190}
191191192192+type InlineReporter = Reporter
193193+type ReporterName = BuiltinReporters | 'html' | (string & {})
194194+type ReporterWithOptions<Name extends ReporterName = ReporterName> =
195195+ Name extends keyof BuiltinReporterOptions
196196+ ? BuiltinReporterOptions[Name] extends never
197197+ ? [Name, {}]
198198+ : [Name, Partial<BuiltinReporterOptions[Name]>]
199199+ : [Name, Record<string, unknown>]
200200+192201export interface InlineConfig {
193202 /**
194203 * Name of the project. Will be used to display in the reporter.
···365374 * Custom reporter for output. Can contain one or more built-in report names, reporter instances,
366375 * and/or paths to custom reporters.
367376 */
368368- reporters?: Arrayable<BuiltinReporters | 'html' | Reporter | Omit<string, BuiltinReporters>>
377377+ reporters?: Arrayable<ReporterName | InlineReporter> | ((ReporterName | InlineReporter) | [ReporterName] | ReporterWithOptions)[]
369378379379+ // TODO: v2.0.0 Remove in favor of custom reporter options, e.g. "reporters: [['json', { outputFile: 'some-dir/file.html' }]]"
370380 /**
371381 * Write test results to a file when the --reporter=json` or `--reporter=junit` option is also specified.
372382 * Also definable individually per reporter by using an object instead.
···786796 pool: Pool
787797 poolOptions?: PoolOptions
788798789789- reporters: (Reporter | BuiltinReporters)[]
799799+ reporters: (InlineReporter | ReporterWithOptions)[]
790800791801 defines: Record<string, any>
792802
+15-2
packages/vitest/src/node/reporters/index.ts
···22import { BasicReporter } from './basic'
33import { DefaultReporter } from './default'
44import { DotReporter } from './dot'
55-import { JsonReporter } from './json'
55+import { type JsonOptions, JsonReporter } from './json'
66import { VerboseReporter } from './verbose'
77import { TapReporter } from './tap'
88-import { JUnitReporter } from './junit'
88+import { type JUnitOptions, JUnitReporter } from './junit'
99import { TapFlatReporter } from './tap-flat'
1010import { HangingProcessReporter } from './hanging-process'
1111import type { BaseReporter } from './base'
···3838}
39394040export type BuiltinReporters = keyof typeof ReportersMap
4141+4242+export interface BuiltinReporterOptions {
4343+ default: never
4444+ basic: never
4545+ verbose: never
4646+ dot: never
4747+ json: JsonOptions
4848+ tap: never
4949+ 'tap-flat': never
5050+ junit: JUnitOptions
5151+ 'hanging-process': never
5252+ html: { outputFile?: string } // TODO: Any better place for defining this UI package's reporter options?
5353+}
41544255export * from './benchmark'