-
-
Notifications
You must be signed in to change notification settings - Fork 1k
/
listRenderer.ts
128 lines (106 loc) · 3.8 KB
/
listRenderer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { createLogUpdate } from 'log-update'
import c from 'picocolors'
import cliTruncate from 'cli-truncate'
import stripAnsi from 'strip-ansi'
import type { SuiteHooks, Task } from '../../../types'
import { clearInterval, getTests, setInterval } from '../../../utils'
import { F_RIGHT } from '../../../utils/figures'
import { getCols, getHookStateSymbol, getStateSymbol } from './utils'
export interface ListRendererOptions {
renderSucceed?: boolean
outputStream: NodeJS.WritableStream
showHeap: boolean
}
const DURATION_LONG = 300
const outputMap = new WeakMap<Task, string>()
function formatFilepath(path: string) {
const lastSlash = Math.max(path.lastIndexOf('/') + 1, 0)
const basename = path.slice(lastSlash)
let firstDot = basename.indexOf('.')
if (firstDot < 0)
firstDot = basename.length
firstDot += lastSlash
return c.dim(path.slice(0, lastSlash)) + path.slice(lastSlash, firstDot) + c.dim(path.slice(firstDot))
}
function renderHookState(task: Task, hookName: keyof SuiteHooks, level = 0) {
const state = task.result?.hooks?.[hookName]
if (state && state === 'run')
return `${' '.repeat(level)} ${getHookStateSymbol(task, hookName)} ${c.dim(`[ ${hookName} ]`)}`
return ''
}
export function renderTree(tasks: Task[], options: ListRendererOptions, level = 0) {
let output: string[] = []
for (const task of tasks) {
let suffix = ''
const prefix = ` ${getStateSymbol(task)} `
if (task.type === 'suite')
suffix += c.dim(` (${getTests(task).length})`)
if (task.mode === 'skip' || task.mode === 'todo')
suffix += ` ${c.dim(c.gray('[skipped]'))}`
if (task.result?.duration != null) {
if (task.result.duration > DURATION_LONG)
suffix += c.yellow(` ${Math.round(task.result.duration)}${c.dim('ms')}`)
}
if (options.showHeap && task.result?.heap != null)
suffix += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`)
let name = task.name
if (level === 0)
name = formatFilepath(name)
output.push(' '.repeat(level) + prefix + name + suffix)
if ((task.result?.state !== 'pass') && outputMap.get(task) != null) {
let data: string | undefined = outputMap.get(task)
if (typeof data === 'string') {
data = stripAnsi(data.trim().split('\n').filter(Boolean).pop()!)
if (data === '')
data = undefined
}
if (data != null) {
const out = `${' '.repeat(level)}${F_RIGHT} ${data}`
output.push(` ${c.gray(cliTruncate(out, getCols(-3)))}`)
}
}
output = output.concat(renderHookState(task, 'beforeAll', level + 1))
output = output.concat(renderHookState(task, 'beforeEach', level + 1))
if (task.type === 'suite' && task.tasks.length > 0) {
if ((task.result?.state === 'fail' || task.result?.state === 'run' || options.renderSucceed))
output = output.concat(renderTree(task.tasks, options, level + 1))
}
output = output.concat(renderHookState(task, 'afterAll', level + 1))
output = output.concat(renderHookState(task, 'afterEach', level + 1))
}
// TODO: moving windows
return output.filter(Boolean).join('\n')
}
export const createListRenderer = (_tasks: Task[], options: ListRendererOptions) => {
let tasks = _tasks
let timer: any
const log = createLogUpdate(options.outputStream)
function update() {
log(renderTree(tasks, options))
}
return {
start() {
if (timer)
return this
timer = setInterval(update, 200)
return this
},
update(_tasks: Task[]) {
tasks = _tasks
update()
return this
},
async stop() {
if (timer) {
clearInterval(timer)
timer = undefined
}
log.clear()
options.outputStream.write(`${renderTree(tasks, options)}\n`)
return this
},
clear() {
log.clear()
},
}
}