diff --git a/packages/vitest/src/node/reporters/dot.ts b/packages/vitest/src/node/reporters/dot.ts index 44254dd6e152..f005b92ea9e7 100644 --- a/packages/vitest/src/node/reporters/dot.ts +++ b/packages/vitest/src/node/reporters/dot.ts @@ -1,53 +1,126 @@ -import type { UserConsoleLog } from '../../types/general' +import type { File, TaskResultPack, TaskState, Test } from '@vitest/runner' +import type { Vitest } from '../core' +import c from 'tinyrainbow' import { BaseReporter } from './base' -import { createDotRenderer } from './renderers/dotRenderer' +import { WindowRenderer } from './renderers/windowedRenderer' +import { TaskParser } from './task-parser' export class DotReporter extends BaseReporter { - renderer?: ReturnType + private summary?: DotSummary - onTaskUpdate() {} + onInit(ctx: Vitest) { + super.onInit(ctx) - onCollected() { if (this.isTTY) { - const files = this.ctx.state.getFiles(this.watchFilters) - if (!this.renderer) { - this.renderer = createDotRenderer(files, { - logger: this.ctx.logger, - }).start() - } - else { - this.renderer.update(files) - } + this.summary = new DotSummary() + this.summary.onInit(ctx) + } + } + + onTaskUpdate(packs: TaskResultPack[]) { + this.summary?.onTaskUpdate(packs) + + if (!this.isTTY) { + super.onTaskUpdate(packs) } } - async onFinished( - files = this.ctx.state.getFiles(), - errors = this.ctx.state.getUnhandledErrors(), - ) { - await this.stopListRender() - this.ctx.logger.log() + onWatcherRerun(files: string[], trigger?: string) { + this.summary?.onWatcherRerun() + super.onWatcherRerun(files, trigger) + } + + onFinished(files?: File[], errors?: unknown[]) { + this.summary?.onFinished() super.onFinished(files, errors) } +} - async onWatcherStart() { - await this.stopListRender() - super.onWatcherStart() +class DotSummary extends TaskParser { + private renderer!: WindowRenderer + private tests = new Map() + + onInit(ctx: Vitest): void { + this.ctx = ctx + + this.renderer = new WindowRenderer({ + logger: ctx.logger, + getWindow: () => this.createSummary(), + }) + + this.ctx.onClose(() => this.renderer.stop()) } - async stopListRender() { - this.renderer?.stop() - this.renderer = undefined - await new Promise(resolve => setTimeout(resolve, 10)) + onWatcherRerun() { + this.tests.clear() + this.renderer.start() } - async onWatcherRerun(files: string[], trigger?: string) { - await this.stopListRender() - super.onWatcherRerun(files, trigger) + onFinished() { + this.tests.clear() + this.renderer.finish() + } + + onTestStart(test: Test) { + if (!this.tests.has(test.id)) { + this.tests.set(test.id, 'run') + } + } + + onTestFinished(test: Test) { + this.tests.set(test.id, test.result?.state || 'skip') } - onUserConsoleLog(log: UserConsoleLog) { - this.renderer?.clear() - super.onUserConsoleLog(log) + onTestFileFinished() { + const columns = this.renderer.getColumns() + + if (this.tests.size < columns) { + return + } + + const finishedTests = Array.from(this.tests).filter(entry => entry[1] !== 'run') + + if (finishedTests.length < columns) { + return + } + + // Remove finished tests from state and render them in static output + let output = '' + let count = 0 + + for (const [id, state] of finishedTests) { + if (count++ >= columns) { + break + } + + this.tests.delete(id) + output += getIcon(state) + } + + this.ctx.logger.log(output) + } + + private createSummary() { + const summary = [''] + + const row = Array.from(this.tests.values()).map(getIcon).join('') + summary.push(row) + + summary.push('') + return summary + } +} + +function getIcon(state: TaskState) { + switch (state) { + case 'pass': + return c.green('·') + case 'fail': + return c.red('x') + case 'skip': + case 'todo': + return c.dim(c.gray('-')) + default: + return c.yellow('*') } } diff --git a/packages/vitest/src/node/reporters/renderers/dotRenderer.ts b/packages/vitest/src/node/reporters/renderers/dotRenderer.ts deleted file mode 100644 index 7d1982172bed..000000000000 --- a/packages/vitest/src/node/reporters/renderers/dotRenderer.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { Task } from '@vitest/runner' -import type { Logger } from '../../logger' -import { getTests } from '@vitest/runner/utils' -import c from 'tinyrainbow' - -export interface DotRendererOptions { - logger: Logger -} - -interface Icon { - char: string - color: (char: string) => string -} - -const check: Icon = { char: '·', color: c.green } -const cross: Icon = { char: 'x', color: c.red } -const pending: Icon = { char: '*', color: c.yellow } -const skip: Icon = { char: '-', color: (char: string) => c.dim(c.gray(char)) } - -function getIcon(task: Task) { - if (task.mode === 'skip' || task.mode === 'todo') { - return skip - } - switch (task.result?.state) { - case 'pass': - return check - case 'fail': - return cross - default: - return pending - } -} - -function render(tasks: Task[], width: number): string { - const all = getTests(tasks) - let currentIcon = pending - let currentTasks = 0 - let previousLineWidth = 0 - let output = '' - - // The log-update uses various ANSI helper utilities, e.g. ansi-warp, ansi-slice, - // when printing. Passing it hundreds of single characters containing ANSI codes reduces - // performances. We can optimize it by reducing amount of ANSI codes, e.g. by coloring - // multiple tasks at once instead of each task separately. - const addOutput = () => { - const { char, color } = currentIcon - const availableWidth = width - previousLineWidth - if (availableWidth > currentTasks) { - output += color(char.repeat(currentTasks)) - previousLineWidth += currentTasks - } - else { - // We need to split the line otherwise it will mess up log-update's height calculation - // and spam the scrollback buffer with dots. - - // Fill the current line first - let buf = `${char.repeat(availableWidth)}\n` - const remaining = currentTasks - availableWidth - - // Then fill as many full rows as possible - const fullRows = Math.floor(remaining / width) - buf += `${char.repeat(width)}\n`.repeat(fullRows) - - // Add remaining dots which don't make a full row - const partialRow = remaining % width - if (partialRow > 0) { - buf += char.repeat(partialRow) - previousLineWidth = partialRow - } - else { - previousLineWidth = 0 - } - - output += color(buf) - } - } - for (const task of all) { - const icon = getIcon(task) - if (icon === currentIcon) { - currentTasks++ - continue - } - // Task mode/state has changed, add previous group to output - addOutput() - - // Start tracking new group - currentTasks = 1 - currentIcon = icon - } - addOutput() - return output -} - -export function createDotRenderer(_tasks: Task[], options: DotRendererOptions) { - let tasks = _tasks - let timer: any - - const { logUpdate: log, outputStream } = options.logger - const columns = 'columns' in outputStream ? outputStream.columns : 80 - - function update() { - log(render(tasks, columns)) - } - - return { - start() { - if (timer) { - return this - } - timer = setInterval(update, 16) - return this - }, - update(_tasks: Task[]) { - tasks = _tasks - return this - }, - async stop() { - if (timer) { - clearInterval(timer) - timer = undefined - } - log.clear() - options.logger.log(render(tasks, columns)) - return this - }, - clear() { - log.clear() - }, - } -} diff --git a/packages/vitest/src/node/reporters/renderers/windowedRenderer.ts b/packages/vitest/src/node/reporters/renderers/windowedRenderer.ts index 3c7a9dcd2371..eabef2d59bf1 100644 --- a/packages/vitest/src/node/reporters/renderers/windowedRenderer.ts +++ b/packages/vitest/src/node/reporters/renderers/windowedRenderer.ts @@ -78,6 +78,10 @@ export class WindowRenderer { clearInterval(this.renderInterval) } + getColumns() { + return 'columns' in this.options.logger.outputStream ? this.options.logger.outputStream.columns : 80 + } + private flushBuffer() { if (this.buffer.length === 0) { return this.render() @@ -113,11 +117,11 @@ export class WindowRenderer { } const windowContent = this.options.getWindow() - const rowCount = getRenderedRowCount(windowContent, this.options.logger.outputStream) + const rowCount = getRenderedRowCount(windowContent, this.getColumns()) let padding = this.windowHeight - rowCount if (padding > 0 && message) { - padding -= getRenderedRowCount([message], this.options.logger.outputStream) + padding -= getRenderedRowCount([message], this.getColumns()) } this.write(SYNC_START) @@ -178,9 +182,8 @@ export class WindowRenderer { } /** Calculate the actual row count needed to render `rows` into `stream` */ -function getRenderedRowCount(rows: string[], stream: Options['logger']['outputStream']) { +function getRenderedRowCount(rows: string[], columns: number) { let count = 0 - const columns = 'columns' in stream ? stream.columns : 80 for (const row of rows) { const text = stripVTControlCharacters(row)