Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
100 changes: 64 additions & 36 deletions packages/vitest/src/node/reporters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,12 @@ export abstract class BaseReporter implements Reporter {
return
}

const tests = getTests(task)
const failed = tests.filter(t => t.result?.state === 'fail')
const skipped = tests.filter(t => t.mode === 'skip' || t.mode === 'todo')
const suites = getSuites(task)
const allTests = getTests(task)
const failed = allTests.filter(t => t.result?.state === 'fail')
const skipped = allTests.filter(t => t.mode === 'skip' || t.mode === 'todo')

let state = c.dim(`${tests.length} test${tests.length > 1 ? 's' : ''}`)
let state = c.dim(`${allTests.length} test${allTests.length > 1 ? 's' : ''}`)

if (failed.length) {
state += c.dim(' | ') + c.red(`${failed.length} failed`)
Expand Down Expand Up @@ -120,52 +121,79 @@ export abstract class BaseReporter implements Reporter {

this.log(` ${title} ${task.name} ${suffix}`)

const anyFailed = tests.some(test => test.result?.state === 'fail')
for (const suite of suites) {
const tests = suite.tasks.filter(task => task.type === 'test')

for (const test of tests) {
const { duration, retryCount, repeatCount } = test.result || {}
let suffix = ''

if (retryCount != null && retryCount > 0) {
suffix += c.yellow(` (retry x${retryCount})`)
if (!('filepath' in suite)) {
this.printSuite(suite)
}

if (repeatCount != null && repeatCount > 0) {
suffix += c.yellow(` (repeat x${repeatCount})`)
}
for (const test of tests) {
const { duration, retryCount, repeatCount } = test.result || {}
const padding = this.getTestIndentation(test)
let suffix = ''

if (test.result?.state === 'fail') {
this.log(c.red(` ${taskFail} ${getTestName(test, c.dim(' > '))}${this.getDurationPrefix(test)}`) + suffix)
if (retryCount != null && retryCount > 0) {
suffix += c.yellow(` (retry x${retryCount})`)
}

if (repeatCount != null && repeatCount > 0) {
suffix += c.yellow(` (repeat x${repeatCount})`)
}

if (test.result?.state === 'fail') {
this.log(c.red(` ${padding}${taskFail} ${this.getTestName(test, c.dim(' > '))}${this.getDurationPrefix(test)}`) + suffix)

test.result?.errors?.forEach((e) => {
// print short errors, full errors will be at the end in summary
this.log(c.red(` ${F_RIGHT} ${e?.message}`))
})
}
test.result?.errors?.forEach((error) => {
const message = this.formatShortError(error)

// also print slow tests
else if (duration && duration > this.ctx.config.slowTestThreshold) {
this.log(
` ${c.yellow(c.dim(F_CHECK))} ${getTestName(test, c.dim(' > '))}`
+ ` ${c.yellow(Math.round(duration) + c.dim('ms'))}${suffix}`,
)
}
if (message) {
this.log(c.red(` ${padding}${message}`))
}
})
}

else if (this.ctx.config.hideSkippedTests && (test.mode === 'skip' || test.result?.state === 'skip')) {
// Skipped tests are hidden when --hideSkippedTests
}
// also print slow tests
else if (duration && duration > this.ctx.config.slowTestThreshold) {
this.log(
` ${padding}${c.yellow(c.dim(F_CHECK))} ${this.getTestName(test, c.dim(' > '))}`
+ ` ${c.yellow(Math.round(duration) + c.dim('ms'))}${suffix}`,
)
}

// also print skipped tests that have notes
else if (test.result?.state === 'skip' && test.result.note) {
this.log(` ${getStateSymbol(test)} ${getTestName(test)}${c.dim(c.gray(` [${test.result.note}]`))}`)
}
else if (this.ctx.config.hideSkippedTests && (test.mode === 'skip' || test.result?.state === 'skip')) {
// Skipped tests are hidden when --hideSkippedTests
}

// also print skipped tests that have notes
else if (test.result?.state === 'skip' && test.result.note) {
this.log(` ${padding}${getStateSymbol(test)} ${this.getTestName(test)}${c.dim(c.gray(` [${test.result.note}]`))}`)
}

else if (this.renderSucceed || anyFailed) {
this.log(` ${getStateSymbol(test)} ${getTestName(test, c.dim(' > '))}${suffix}`)
else if (this.renderSucceed || failed.length > 0) {
this.log(` ${padding}${getStateSymbol(test)} ${this.getTestName(test, c.dim(' > '))}${suffix}`)
}
}
}
}

protected printSuite(_task: Task): void {
// Suite name is included in getTestName by default
}

protected getTestName(test: Task, separator?: string): string {
return getTestName(test, separator)
}

protected formatShortError(error: ErrorWithDiff): string {
return `${F_RIGHT} ${error.message}`
}

protected getTestIndentation(_test: Task) {
return ' '
}

private getDurationPrefix(task: Task) {
if (!task.result?.duration) {
return ''
Expand Down
31 changes: 30 additions & 1 deletion packages/vitest/src/node/reporters/verbose.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Task } from '@vitest/runner'
import { getFullName } from '@vitest/runner/utils'
import { getFullName, getTests } from '@vitest/runner/utils'
import c from 'tinyrainbow'
import { DefaultReporter } from './default'
import { F_RIGHT } from './renderers/figures'
Expand Down Expand Up @@ -45,4 +45,33 @@ export class VerboseReporter extends DefaultReporter {
task.result.errors?.forEach(error => this.log(c.red(` ${F_RIGHT} ${error?.message}`)))
}
}

protected printSuite(task: Task): void {
const indentation = ' '.repeat(getIndentation(task))
const tests = getTests(task)
const state = getStateSymbol(task)

this.log(` ${indentation}${state} ${task.name} ${c.dim(`(${tests.length})`)}`)
}

protected getTestName(test: Task): string {
return test.name
}

protected getTestIndentation(test: Task): string {
return ' '.repeat(getIndentation(test))
}

protected formatShortError(): string {
// Short errors are not shown in tree-view
return ''
}
}

function getIndentation(suite: Task, level = 1): number {
if (suite.suite && !('filepath' in suite.suite)) {
return getIndentation(suite.suite, level + 1)
}

return level
}
35 changes: 35 additions & 0 deletions test/reporters/fixtures/verbose/example-1.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { test, describe, expect } from "vitest";

test("test pass in root", () => {});

test.skip("test skip in root", () => {});

describe("suite in root", () => {
test("test pass in 1. suite #1", () => {});

test("test pass in 1. suite #2", () => {});

describe("suite in suite", () => {
test("test pass in nested suite #1", () => {});

test("test pass in nested suite #2", () => {});

describe("suite in nested suite", () => {
test("test failure in 2x nested suite", () => {
expect("should fail").toBe("as expected");
});
});
});
});

describe.skip("suite skip in root", () => {
test("test 1.3", () => {});

describe("suite in suite", () => {
test("test in nested suite", () => {});

test("test failure in nested suite of skipped suite", () => {
expect("should fail").toBe("but should not run");
});
});
});
9 changes: 9 additions & 0 deletions test/reporters/fixtures/verbose/example-2.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { test, describe } from "vitest";

test("test 0.1", () => {});

test.skip("test 0.2", () => {});

describe("suite 1.1", () => {
test("test 1.1", () => {});
});
52 changes: 49 additions & 3 deletions test/reporters/tests/default.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { TestSpecification } from 'vitest/node'
import { describe, expect, test } from 'vitest'
import { runVitest } from '../../test-utils'

Expand All @@ -7,11 +8,56 @@ describe('default reporter', async () => {
include: ['b1.test.ts', 'b2.test.ts'],
root: 'fixtures/default',
reporters: 'none',
fileParallelism: false,
sequence: {
sequencer: class StableTestFileOrderSorter {
sort(files: TestSpecification[]) {
return files.sort((a, b) => a.moduleId.localeCompare(b.moduleId))
}

shard(files: TestSpecification[]) {
return files
}
},
},
})

expect(stdout).contain('✓ b2 passed > b2 test')
expect(stdout).not.contain('✓ nested b1 test')
expect(stdout).contain('× b1 failed > b failed test')
const rows = stdout.replace(/\d+ms/g, '[...]ms').split('\n')
rows.splice(0, rows.findIndex(row => row.includes('b1.test.ts')))
rows.splice(rows.findIndex(row => row.includes('Test Files')))

expect(rows.join('\n').trim()).toMatchInlineSnapshot(`
"❯ b1.test.ts (13 tests | 1 failed) [...]ms
✓ b1 passed > b1 test
✓ b1 passed > b2 test
✓ b1 passed > b3 test
✓ b1 passed > nested b > nested b1 test
✓ b1 passed > nested b > nested b2 test
✓ b1 passed > nested b > nested b3 test
✓ b1 failed > b1 test
✓ b1 failed > b2 test
✓ b1 failed > b3 test
× b1 failed > b failed test [...]ms
→ expected 1 to be 2 // Object.is equality
✓ b1 failed > nested b > nested b1 test
✓ b1 failed > nested b > nested b2 test
✓ b1 failed > nested b > nested b3 test
❯ b2.test.ts (13 tests | 1 failed) [...]ms
✓ b2 passed > b1 test
✓ b2 passed > b2 test
✓ b2 passed > b3 test
✓ b2 passed > nested b > nested b1 test
✓ b2 passed > nested b > nested b2 test
✓ b2 passed > nested b > nested b3 test
✓ b2 failed > b1 test
✓ b2 failed > b2 test
✓ b2 failed > b3 test
× b2 failed > b failed test [...]ms
→ expected 1 to be 2 // Object.is equality
✓ b2 failed > nested b > nested b1 test
✓ b2 failed > nested b > nested b2 test
✓ b2 failed > nested b > nested b3 test"
`)
})

test('show full test suite when only one file', async () => {
Expand Down
96 changes: 91 additions & 5 deletions test/reporters/tests/verbose.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import type { TestSpecification } from 'vitest/node'
import { expect, test } from 'vitest'
import { runVitest } from '../../test-utils'

test('duration', async () => {
const result = await runVitest({
const { stdout } = await runVitest({
root: 'fixtures/duration',
reporters: 'verbose',
env: { CI: '1' },
})

const output = result.stdout.replace(/\d+ms/g, '[...]ms')
expect(output).toContain(`
expect(trimReporterOutput(stdout)).toContain(`
✓ basic.test.ts > fast
✓ basic.test.ts > slow [...]ms
`)
✓ basic.test.ts > slow [...]ms`,
)
})

test('prints error properties', async () => {
Expand Down Expand Up @@ -72,3 +72,89 @@ test('prints repeat count', async () => {
expect(stdout).toContain('1 passed')
expect(stdout).toContain('✓ repeat couple of times (repeat x3)')
})

test('renders tree when in TTY', async () => {
const { stdout } = await runVitest({
include: ['fixtures/verbose/*.test.ts'],
reporters: [['verbose', { isTTY: true, summary: false }]],
config: false,
fileParallelism: false,
sequence: {
sequencer: class StableTestFileOrderSorter {
sort(files: TestSpecification[]) {
return files.sort((a, b) => a.moduleId.localeCompare(b.moduleId))
}

shard(files: TestSpecification[]) {
return files
}
},
},
})

expect(trimReporterOutput(stdout)).toMatchInlineSnapshot(`
"❯ fixtures/verbose/example-1.test.ts (10 tests | 1 failed | 4 skipped) [...]ms
✓ test pass in root
↓ test skip in root
❯ suite in root (5)
✓ test pass in 1. suite #1
✓ test pass in 1. suite #2
❯ suite in suite (3)
✓ test pass in nested suite #1
✓ test pass in nested suite #2
❯ suite in nested suite (1)
× test failure in 2x nested suite [...]ms
↓ suite skip in root (3)
↓ test 1.3
↓ suite in suite (2)
↓ test in nested suite
↓ test failure in nested suite of skipped suite
✓ fixtures/verbose/example-2.test.ts (3 tests | 1 skipped) [...]ms
✓ test 0.1
↓ test 0.2
✓ suite 1.1 (1)
✓ test 1.1"
`)
})

test('does not render tree when in non-TTY', async () => {
const { stdout } = await runVitest({
include: ['fixtures/verbose/*.test.ts'],
reporters: [['verbose', { isTTY: false, summary: false }]],
config: false,
fileParallelism: false,
sequence: {
sequencer: class StableTestFileOrderSorter {
sort(files: TestSpecification[]) {
return files.sort((a, b) => a.moduleId.localeCompare(b.moduleId))
}

shard(files: TestSpecification[]) {
return files
}
},
},
})

expect(trimReporterOutput(stdout)).toMatchInlineSnapshot(`
"✓ fixtures/verbose/example-1.test.ts > test pass in root
✓ fixtures/verbose/example-1.test.ts > suite in root > test pass in 1. suite #1
✓ fixtures/verbose/example-1.test.ts > suite in root > test pass in 1. suite #2
✓ fixtures/verbose/example-1.test.ts > suite in root > suite in suite > test pass in nested suite #1
✓ fixtures/verbose/example-1.test.ts > suite in root > suite in suite > test pass in nested suite #2
× fixtures/verbose/example-1.test.ts > suite in root > suite in suite > suite in nested suite > test failure in 2x nested suite
→ expected 'should fail' to be 'as expected' // Object.is equality
✓ fixtures/verbose/example-2.test.ts > test 0.1
✓ fixtures/verbose/example-2.test.ts > suite 1.1 > test 1.1"
`)
})

function trimReporterOutput(report: string) {
const rows = report.replace(/\d+ms/g, '[...]ms').split('\n')

// Trim start and end, capture just rendered tree
rows.splice(0, rows.findIndex(row => row.includes('fixtures/verbose/example-')))
rows.splice(rows.findIndex(row => row.includes('Test Files')))

return rows.join('\n').trim()
}