forked from vitest-dev/vitest
/
ViewReport.vue
155 lines (141 loc) · 4.96 KB
/
ViewReport.vue
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
<script setup lang="ts">
import { unifiedDiff } from '../../composables/diff'
import { openInEditor, shouldOpenInEditor } from '../../composables/error'
import type { ErrorWithDiff, File, ParsedStack, Suite, Task } from '#types'
import { config } from '~/composables/client'
import { isDark } from '~/composables/dark'
import { createAnsiToHtmlFilter } from '~/composables/error'
const props = defineProps<{
file?: File
}>()
type LeveledTask = Task & {
level: number
}
function collectFailed(task: Task, level: number): LeveledTask[] {
if (task.result?.state !== 'fail')
return []
if (task.type === 'test' || task.type === 'benchmark')
return [{ ...task, level }]
else
return [{ ...task, level }, ...task.tasks.flatMap(t => collectFailed(t, level + 1))]
}
function escapeHtml(unsafe: string) {
return unsafe
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
function mapLeveledTaskStacks(dark: boolean, tasks: LeveledTask[]) {
const filter = createAnsiToHtmlFilter(dark)
return tasks.map((t) => {
const result = t.result
if (result) {
const error = result.error
if (error) {
let htmlError = ''
if (error.message.includes('\x1B'))
htmlError = `<b>${error.nameStr || error.name}</b>: ${filter.toHtml(escapeHtml(error.message))}`
const startStrWithX1B = error.stackStr?.includes('\x1B')
if (startStrWithX1B || error.stack?.includes('\x1B')) {
if (htmlError.length > 0)
htmlError += filter.toHtml(escapeHtml((startStrWithX1B ? error.stackStr : error.stack) as string))
else
htmlError = `<b>${error.nameStr || error.name}</b>: ${error.message}${filter.toHtml(escapeHtml((startStrWithX1B ? error.stackStr : error.stack) as string))}`
}
if (htmlError.length > 0)
result.htmlError = htmlError
}
}
return t
})
}
const failed = computed(() => {
const file = props.file
const failedFlatMap = file?.tasks?.flatMap(t => collectFailed(t, 0)) ?? []
const result = file?.result
const fileError = result?.error
// we must check also if the test cannot compile
if (fileError) {
// create a dummy one
const fileErrorTask: Suite & { level: number } = {
id: file!.id,
name: file!.name,
level: 0,
type: 'suite',
mode: 'run',
tasks: [],
result,
}
failedFlatMap.unshift(fileErrorTask)
}
return failedFlatMap.length > 0 ? mapLeveledTaskStacks(isDark.value, failedFlatMap) : failedFlatMap
})
function relative(p: string) {
if (p.startsWith(config.value.root))
return p.slice(config.value.root.length)
return p
}
interface Diff { error: NonNullable<Pick<ErrorWithDiff, 'expected' | 'actual'>> }
type ResultWithDiff = Task['result'] & Diff
function isDiffShowable(result?: Task['result']): result is ResultWithDiff {
return result && result?.error?.expected && result?.error?.actual
}
function diff(result: ResultWithDiff): string {
return unifiedDiff(result.error.actual, result.error.expected, {
outputTruncateLength: 80,
})
}
</script>
<template>
<div h-full class="scrolls">
<template v-if="failed.length">
<div v-for="task of failed" :key="task.id">
<div
bg="red-500/10"
text="red-500 sm"
p="x3 y2"
m-2
rounded
:style="{ 'margin-left': `${task.result?.htmlError ? 0.5 : (2 * task.level + 0.5)}rem` }"
>
{{ task.name }}
<div v-if="task.result?.htmlError" class="scrolls scrolls-rounded task-error">
<pre v-html="task.result.htmlError" />
</div>
<div v-else-if="task.result?.error" class="scrolls scrolls-rounded task-error">
<pre><b>{{ task.result.error.name || task.result.error.nameStr }}</b>: {{ task.result.error.message }}</pre>
<div v-for="(stack, i) of task.result.error.stacks" :key="i" class="op80 flex gap-x-2 items-center" data-testid="stack">
<pre> - {{ relative(stack.file) }}:{{ stack.line }}:{{ stack.column }}</pre>
<div
v-if="shouldOpenInEditor(stack.file, props.file?.name)"
v-tooltip.bottom="'Open in Editor'"
class="i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em"
tabindex="0"
aria-label="Open in Editor"
@click.passive="openInEditor(stack.file, stack.line, stack.column)"
/>
</div>
<pre v-if="isDiffShowable(task.result)">
{{ `\n${diff(task.result)}` }}
</pre>
</div>
</div>
</div>
</template>
<template v-else>
<div bg="green-500/10" text="green-500 sm" p="x4 y2" m-2 rounded>
All tests passed in this file
</div>
</template>
</div>
</template>
<style scoped>
.task-error {
--cm-ttc-c-thumb: #CCC;
}
html.dark .task-error {
--cm-ttc-c-thumb: #444;
}
</style>