feat: added support for passing a path to a custom reporter when usin… (#1136)

Co-authored-by: Anjorin Damilare <damilareanjorin1@gmail.com>
Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
This commit is contained in:
Eric Gagnon 2022-04-25 10:05:45 -04:00 committed by GitHub
parent 4f198ef284
commit f2bceb2c28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 176 additions and 47 deletions

View File

@ -227,6 +227,7 @@ Custom reporters for output. Reporters can be [a Reporter instance](https://gith
- `'dot'` - show each task as a single dot
- `'junit'` - JUnit XML reporter
- `'json'` - give a simple JSON summary
- path of a custom reporter (e.g. `'./path/to/reporter.ts'`, `'@scope/reporter'`)
### outputFile

View File

@ -41,7 +41,7 @@
| `--threads` | Enable Threads (default: `true`) |
| `--silent` | Silent console output from tests |
| `--isolate` | Isolate environment for each test file (default: `true`) |
| `--reporter <name>` | Select reporter: `default`, `verbose`, `dot`, `junit` or `json` |
| `--reporter <name>` | Select reporter: `default`, `verbose`, `dot`, `junit`, `json`, or a path to a custom reporter |
| `--outputFile <filename/-s>` | Write test results to a file when the `--reporter=json` or `--reporter=junit` option is also specified <br /> Via [cac's dot notation] you can specify individual outputs for multiple reporters |
| `--coverage` | Use c8 for coverage |
| `--run` | Do not watch |

View File

@ -5,14 +5,15 @@ import { relative, toNamespacedPath } from 'pathe'
import fg from 'fast-glob'
import mm from 'micromatch'
import c from 'picocolors'
import { ViteNodeRunner } from 'vite-node/client'
import { ViteNodeServer } from 'vite-node/server'
import type { ArgumentsType, Reporter, ResolvedConfig, UserConfig } from '../types'
import { SnapshotManager } from '../integrations/snapshot/manager'
import { clearTimeout, deepMerge, hasFailed, noop, setTimeout, slash, toArray } from '../utils'
import { cleanCoverage, reportCoverage } from '../integrations/coverage'
import { ReportersMap } from './reporters'
import { createPool } from './pool'
import type { WorkerPool } from './pool'
import { createReporters } from './reporters/utils'
import { StateManager } from './state'
import { resolveConfig } from './config'
import { printError } from './error'
@ -44,6 +45,7 @@ export class Vitest {
isFirstRun = true
restartsCount = 0
runner: ViteNodeRunner = undefined!
private _onRestartListeners: Array<() => void> = []
@ -64,21 +66,24 @@ export class Vitest {
this.config = resolved
this.state = new StateManager()
this.snapshot = new SnapshotManager({ ...resolved.snapshotOptions })
this.reporters = resolved.reporters
.map((i) => {
if (typeof i === 'string') {
const Reporter = ReportersMap[i]
if (!Reporter)
throw new Error(`Unknown reporter: ${i}`)
return new Reporter()
}
return i
})
if (this.config.watch)
this.registerWatcher()
this.vitenode = new ViteNodeServer(server, this.config)
const node = this.vitenode
this.runner = new ViteNodeRunner({
root: server.config.root,
base: server.config.base,
fetchModule(id: string) {
return node.fetchModule(id)
},
resolveId(id: string, importer: string|undefined) {
return node.resolveId(id, importer)
},
})
this.reporters = await createReporters(resolved.reporters, this.runner.executeFile.bind(this.runner))
this.runningPromise = undefined

View File

@ -1,5 +1,5 @@
import type { Plugin } from 'vite'
import { ViteNodeRunner } from 'vite-node/client'
import type { ViteNodeRunner } from 'vite-node/client'
import c from 'picocolors'
import type { Vitest } from '../core'
import { toArray } from '../../utils'
@ -12,18 +12,8 @@ interface GlobalSetupFile {
}
async function loadGlobalSetupFiles(ctx: Vitest): Promise<GlobalSetupFile[]> {
const node = ctx.vitenode
const server = ctx.server
const runner = new ViteNodeRunner({
root: server.config.root,
base: server.config.base,
fetchModule(id) {
return node.fetchModule(id)
},
resolveId(id, importer) {
return node.resolveId(id, importer)
},
})
const runner = ctx.runner
const globalSetupFiles = toArray(server.config.test?.globalSetup)
return Promise.all(globalSetupFiles.map(file => loadGlobalSetupFile(file, runner)))
}

View File

@ -0,0 +1,37 @@
import type { Reporter } from '../../types'
import { ReportersMap } from './index'
import type { BuiltinReporters } from './index'
async function loadCustomReporterModule<C extends Reporter>(path: string, fetchModule: (id: string) => Promise<any>): Promise<new () => C> {
let customReporterModule: { default: new () => C }
try {
customReporterModule = await fetchModule(path)
}
catch (customReporterModuleError) {
throw new Error(`Failed to load custom Reporter from ${path}`, { cause: customReporterModuleError as Error })
}
if (customReporterModule.default === null || customReporterModule.default === undefined)
throw new Error(`Custom reporter loaded from ${path} was not the default export`)
return customReporterModule.default
}
function createReporters(reporterReferences: Array<string|Reporter|BuiltinReporters>, fetchModule: (id: string) => Promise<any>) {
const promisedReporters = reporterReferences.map(async (referenceOrInstance) => {
if (typeof referenceOrInstance === 'string') {
if (referenceOrInstance in ReportersMap) {
const BuiltinReporter = ReportersMap[referenceOrInstance as BuiltinReporters]
return new BuiltinReporter()
}
else {
const CustomReporter = await loadCustomReporterModule(referenceOrInstance, fetchModule)
return new CustomReporter()
}
}
return referenceOrInstance
})
return Promise.all(promisedReporters)
}
export { createReporters }

View File

@ -128,9 +128,10 @@ export interface InlineConfig {
root?: string
/**
* Custom reporter for output
* Custom reporter for output. Can contain one or more built-in report names, reporter instances,
* and/or paths to custom reporters
*/
reporters?: Arrayable<BuiltinReporters | Reporter>
reporters?: Arrayable<BuiltinReporters | Reporter | string>
/**
* diff output length

View File

@ -0,0 +1,11 @@
import { resolve } from 'pathe'
import { defineConfig } from 'vitest/config'
const customReporter = resolve(__dirname, './src/custom-reporter.ts')
export default defineConfig({
test: {
include: ['tests/reporters.spec.ts'],
reporters: [customReporter],
},
})

View File

@ -1,17 +1,5 @@
import type { Reporter, Vitest } from 'vitest'
import { defineConfig } from 'vitest/config'
class TestReporter implements Reporter {
ctx!: Vitest
onInit(ctx: Vitest) {
this.ctx = ctx
}
onFinished() {
this.ctx.log('hello from custom reporter')
}
}
import TestReporter from './src/custom-reporter'
export default defineConfig({
test: {

View File

@ -0,0 +1,9 @@
export default class TestReporter {
onInit(ctx) {
this.ctx = ctx
}
onFinished() {
this.ctx.log('hello from custom reporter')
}
}

View File

@ -0,0 +1,13 @@
import type { Reporter, Vitest } from 'vitest'
export default class TestReporter implements Reporter {
ctx!: Vitest
onInit(ctx: Vitest) {
this.ctx = ctx
}
onFinished() {
this.ctx.log('hello from custom reporter')
}
}

View File

@ -1,15 +1,14 @@
import { execa } from 'execa'
import { resolve } from 'pathe'
import { expect, test } from 'vitest'
import { describe, expect, test } from 'vitest'
test('custom reporters work', async () => {
// in Windows child_process is very unstable, we skip testing it
if (process.platform === 'win32' && process.env.CI)
return
const customTsReporterPath = resolve(__dirname, '../src/custom-reporter.ts')
const customJSReporterPath = resolve(__dirname, '../src/custom-reporter.js')
async function run(...runOptions: string[]): Promise<string> {
const root = resolve(__dirname, '..')
const { stdout } = await execa('npx', ['vitest', 'run', '--config', 'custom-reporter.vitest.config.ts'], {
const { stdout } = await execa('npx', ['vitest', 'run', ...runOptions], {
cwd: root,
env: {
...process.env,
@ -19,5 +18,32 @@ test('custom reporters work', async () => {
windowsHide: false,
})
expect(stdout).toContain('hello from custom reporter')
}, 40000)
return stdout
}
describe('Custom reporters', () => {
// On Windows child_process is very unstable, we skip testing it
if (process.platform === 'win32' && process.env.CI)
return test.skip('skip on windows')
test('custom reporter instances defined in configuration works', async () => {
const stdout = await run('--config', 'custom-reporter.vitest.config.ts')
expect(stdout).includes('hello from custom reporter')
}, 40000)
test('a path to a custom reporter defined in configuration works', async () => {
const stdout = await run('--config', 'custom-reporter-path.vitest.config.ts', '--reporter', customJSReporterPath)
expect(stdout).includes('hello from custom reporter')
}, 40000)
test('custom TS reporters using ESM given as a CLI argument works', async () => {
const stdout = await run('--config', 'without-custom-reporter.vitest.config.ts', '--reporter', customTsReporterPath)
expect(stdout).includes('hello from custom reporter')
}, 40000)
test('custom JS reporters using CJS given as a CLI argument works', async () => {
const stdout = await run('--config', 'without-custom-reporter.vitest.config.ts', '--reporter', customJSReporterPath)
expect(stdout).includes('hello from custom reporter')
}, 40000)
})

View File

@ -0,0 +1,41 @@
/**
* @format
*/
import { resolve } from 'pathe'
import { describe, expect, test } from 'vitest'
import { createReporters } from 'vitest/src/node/reporters/utils'
import { DefaultReporter } from '../../../../vitest/packages/vitest/src/node/reporters/default'
import TestReporter from '../src/custom-reporter'
const customReporterPath = resolve(__dirname, '../src/custom-reporter.js')
const fetchModule = (id: string) => import(id)
describe('Reporter Utils', () => {
test('passing an empty array returns nothing', async () => {
const promisedReporters = await createReporters([], fetchModule)
expect(promisedReporters).toHaveLength(0)
})
test('passing the name of a single built-in reporter returns a new instance', async () => {
const promisedReporters = await createReporters(['default'], fetchModule)
expect(promisedReporters).toHaveLength(1)
const reporter = promisedReporters[0]
expect(reporter).toBeInstanceOf(DefaultReporter)
})
test('passing in the path to a custom reporter returns a new instance', async () => {
const promisedReporters = await createReporters(([customReporterPath]), fetchModule)
expect(promisedReporters).toHaveLength(1)
const customReporter = promisedReporters[0]
expect(customReporter).toBeInstanceOf(TestReporter)
})
test('passing in a mix of built-in and custom reporters works', async () => {
const promisedReporters = await createReporters(['default', customReporterPath], fetchModule)
expect(promisedReporters).toHaveLength(2)
const defaultReporter = promisedReporters[0]
expect(defaultReporter).toBeInstanceOf(DefaultReporter)
const customReporter = promisedReporters[1]
expect(customReporter).toBeInstanceOf(TestReporter)
})
})

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['tests/reporters.spec.ts'],
},
})