Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,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'
Expand Down Expand Up @@ -43,6 +44,7 @@ export class Vitest {

isFirstRun = true
restartsCount = 0
runner: ViteNodeRunner = undefined!

private _onRestartListeners: Array<() => void> = []

Expand All @@ -63,21 +65,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

Expand Down
14 changes: 2 additions & 12 deletions packages/vitest/src/node/plugins/globalSetup.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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)))
}
Expand Down
37 changes: 37 additions & 0 deletions packages/vitest/src/node/reporters/utils.ts
Original file line number Diff line number Diff line change
@@ -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 }
ericjgagnon marked this conversation as resolved.
Show resolved Hide resolved
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) => {
ericjgagnon marked this conversation as resolved.
Show resolved Hide resolved
if (typeof referenceOrInstance === 'string') {
if (referenceOrInstance in ReportersMap) {
const Reporter = ReportersMap[referenceOrInstance as BuiltinReporters]
return new Reporter()
}
else {
const CustomReporter = await loadCustomReporterModule(referenceOrInstance, fetchModule)
return new CustomReporter()
}
}
return referenceOrInstance
})
return Promise.all(promisedReporters)
}

export { createReporters }
14 changes: 1 addition & 13 deletions test/reporters/custom-reporter.vitest.config.ts
Original file line number Diff line number Diff line change
@@ -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: {
Expand Down
9 changes: 9 additions & 0 deletions test/reporters/src/custom-reporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module.exports = class TestReporter {
antfu marked this conversation as resolved.
Show resolved Hide resolved
onInit(ctx) {
this.ctx = ctx
}

onFinished() {
this.ctx.log('hello from custom reporter')
}
}
13 changes: 13 additions & 0 deletions test/reporters/src/custom-reporter.ts
Original file line number Diff line number Diff line change
@@ -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')
}
}
44 changes: 36 additions & 8 deletions test/reporters/tests/custom-reporter.spec.ts
Original file line number Diff line number Diff line change
@@ -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 runTest(...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,
Expand All @@ -19,5 +18,34 @@ test('custom reporters work', async() => {
windowsHide: false,
})

expect(stdout).toContain('hello from custom reporter')
}, 40000)
return stdout
}

describe('Custom reporters', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would also be great to see a test case for "custom reporter defined in configuration using path".

  test: {
    reporters: ['./src/custom-reporter'],
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AriPerkkio the first test case is one that uses a custom reporter defined in a config file

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that one is a reporter instance instead of a string path.

reporters: [new TestReporter()],

I don't think anything covers "custom reporter defined in configuration using path" yet.

test('custom reporters defined in configuration work', async() => {
// On Windows child_process is very unstable, we skip testing it
if (process.platform === 'win32' && process.env.CI)
return

const stdout = await runTest('--config', 'custom-reporter.vitest.config.ts')
expect(stdout).includes('hello from custom reporter')
}, 40000)

test('custom TS reporters using ESM given as a CLI argument work', async() => {
// On Windows child_process is very unstable, we skip testing it
if (process.platform === 'win32' && process.env.CI)
return

const stdout = await runTest('--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 work', async() => {
// On Windows child_process is very unstable, we skip testing it
if (process.platform === 'win32' && process.env.CI)
return

const stdout = await runTest('--config', 'without-custom-reporter.vitest.config.ts', '--reporter', customJSReporterPath)
expect(stdout).includes('hello from custom reporter')
}, 40000)
})
41 changes: 41 additions & 0 deletions test/reporters/tests/utils.test.ts
Original file line number Diff line number Diff line change
@@ -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 a the name of a single built-in reporter returns a new instance', async() => {
ericjgagnon marked this conversation as resolved.
Show resolved Hide resolved
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 or built-in and custom reporters works', async() => {
ericjgagnon marked this conversation as resolved.
Show resolved Hide resolved
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)
})
})
7 changes: 7 additions & 0 deletions test/reporters/without-custom-reporter.vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
include: ['tests/reporters.spec.ts'],
},
})