Skip to content

Commit e641a11

Browse files
authoredJan 1, 2023
feat!: deprecate "error" on result, store errors in "errors" (#2586)
1 parent 894f155 commit e641a11

File tree

18 files changed

+242
-147
lines changed

18 files changed

+242
-147
lines changed
 

‎packages/ui/client/components/views/ViewEditor.vue

+30-27
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type CodeMirror from 'codemirror'
44
import { createTooltip, destroyTooltip } from 'floating-vue'
55
import { openInEditor } from '../../composables/error'
66
import { client } from '~/composables/client'
7-
import type { File } from '#types'
7+
import type { ErrorWithDiff, File, ParsedStack } from '#types'
88
99
const props = defineProps<{
1010
file?: File
@@ -60,6 +60,34 @@ watch(draft, (d) => {
6060
emit('draft', d)
6161
}, { immediate: true })
6262
63+
function createErrorElement(e: ErrorWithDiff) {
64+
const stacks = (e?.stacks || []).filter(i => i.file && i.file === props.file?.filepath)
65+
const stack = stacks?.[0]
66+
if (!stack)
67+
return
68+
const div = document.createElement('div')
69+
div.className = 'op80 flex gap-x-2 items-center'
70+
const pre = document.createElement('pre')
71+
pre.className = 'c-red-600 dark:c-red-400'
72+
pre.textContent = `${' '.repeat(stack.column)}^ ${e?.nameStr}: ${e?.message}`
73+
div.appendChild(pre)
74+
const span = document.createElement('span')
75+
span.className = 'i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em'
76+
span.tabIndex = 0
77+
span.ariaLabel = 'Open in Editor'
78+
const tooltip = createTooltip(span, {
79+
content: 'Open in Editor',
80+
placement: 'bottom',
81+
}, false)
82+
const el: EventListener = async () => {
83+
await openInEditor(stack.file, stack.line, stack.column)
84+
}
85+
div.appendChild(span)
86+
listeners.push([span, el, () => destroyTooltip(span)])
87+
handles.push(cm.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10'))
88+
widgets.push(cm.value!.addLineWidget(stack.line - 1, div))
89+
}
90+
6391
watch([cm, failed], ([cmValue]) => {
6492
if (!cmValue) {
6593
clearListeners()
@@ -76,32 +104,7 @@ watch([cm, failed], ([cmValue]) => {
76104
cmValue.on('changes', codemirrorChanges)
77105
78106
failed.value.forEach((i) => {
79-
const e = i.result?.error
80-
const stacks = (e?.stacks || []).filter(i => i.file && i.file === props.file?.filepath)
81-
if (stacks.length) {
82-
const stack = stacks[0]
83-
const div = document.createElement('div')
84-
div.className = 'op80 flex gap-x-2 items-center'
85-
const pre = document.createElement('pre')
86-
pre.className = 'c-red-600 dark:c-red-400'
87-
pre.textContent = `${' '.repeat(stack.column)}^ ${e?.nameStr}: ${e?.message}`
88-
div.appendChild(pre)
89-
const span = document.createElement('span')
90-
span.className = 'i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em'
91-
span.tabIndex = 0
92-
span.ariaLabel = 'Open in Editor'
93-
const tooltip = createTooltip(span, {
94-
content: 'Open in Editor',
95-
placement: 'bottom',
96-
}, false)
97-
const el: EventListener = async () => {
98-
await openInEditor(stacks[0].file, stack.line, stack.column)
99-
}
100-
div.appendChild(span)
101-
listeners.push([span, el, () => destroyTooltip(span)])
102-
handles.push(cm.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10'))
103-
widgets.push(cm.value!.addLineWidget(stack.line - 1, div))
104-
}
107+
i.result?.errors?.forEach(createErrorElement)
105108
})
106109
if (!hasBeenEdited.value)
107110
cmValue.clearHistory() // Prevent getting access to initial state

‎packages/ui/client/components/views/ViewReport.cy.tsx

+12-9
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ const makeTextStack = () => ({
1717
// 5 Stacks
1818
const textStacks = Array.from(new Array(5)).map(makeTextStack)
1919

20+
const error = {
21+
name: 'Do some test',
22+
stacks: textStacks,
23+
message: 'Error: Transform failed with 1 error:',
24+
}
25+
2026
const fileWithTextStacks = {
2127
id: 'f-1',
2228
name: 'test/plain-stack-trace.ts',
@@ -25,11 +31,8 @@ const fileWithTextStacks = {
2531
filepath: 'test/plain-stack-trace.ts',
2632
result: {
2733
state: 'fail',
28-
error: {
29-
name: 'Do some test',
30-
stacks: textStacks,
31-
message: 'Error: Transform failed with 1 error:',
32-
},
34+
error,
35+
errors: [error],
3336
},
3437
tasks: [],
3538
}
@@ -67,11 +70,11 @@ describe('ViewReport', () => {
6770
filepath: 'test/plain-stack-trace.ts',
6871
result: {
6972
state: 'fail',
70-
error: {
73+
errors: [{
7174
name: 'Do some test',
7275
stack: '\x1B[33mtest/plain-stack-trace.ts\x1B[0m',
7376
message: 'Error: Transform failed with 1 error:',
74-
},
77+
}],
7578
},
7679
tasks: [],
7780
}
@@ -104,11 +107,11 @@ describe('ViewReport', () => {
104107
filepath: 'test/plain-stack-trace.ts',
105108
result: {
106109
state: 'fail',
107-
error: {
110+
errors: [{
108111
name: 'Do some test',
109112
stack: '\x1B[33mtest/plain-stack-trace.ts\x1B[0m',
110113
message: '\x1B[44mError: Transform failed with 1 error:\x1B[0m',
111-
},
114+
}],
112115
},
113116
tasks: [],
114117
}

‎packages/ui/client/components/views/ViewReport.vue

+41-60
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<script setup lang="ts">
2-
import { unifiedDiff } from '../../composables/diff'
3-
import { openInEditor, shouldOpenInEditor } from '../../composables/error'
4-
import type { ErrorWithDiff, File, ParsedStack, Suite, Task } from '#types'
5-
import { config } from '~/composables/client'
2+
import type Convert from 'ansi-to-html'
3+
import ViewReportError from './ViewReportError.vue'
4+
import type { ErrorWithDiff, File, Suite, Task } from '#types'
65
import { isDark } from '~/composables/dark'
76
import { createAnsiToHtmlFilter } from '~/composables/error'
7+
import { config } from '~/composables/client'
88
99
const props = defineProps<{
1010
file?: File
@@ -18,7 +18,7 @@ function collectFailed(task: Task, level: number): LeveledTask[] {
1818
if (task.result?.state !== 'fail')
1919
return []
2020
21-
if (task.type === 'test' || task.type === 'benchmark')
21+
if (task.type === 'test' || task.type === 'benchmark' || task.type === 'typecheck')
2222
return [{ ...task, level }]
2323
else
2424
return [{ ...task, level }, ...task.tasks.flatMap(t => collectFailed(t, level + 1))]
@@ -33,29 +33,36 @@ function escapeHtml(unsafe: string) {
3333
.replace(/'/g, '&#039;')
3434
}
3535
36+
function createHtmlError(filter: Convert, error: ErrorWithDiff) {
37+
let htmlError = ''
38+
if (error.message.includes('\x1B'))
39+
htmlError = `<b>${error.nameStr || error.name}</b>: ${filter.toHtml(escapeHtml(error.message))}`
40+
41+
const startStrWithX1B = error.stackStr?.includes('\x1B')
42+
if (startStrWithX1B || error.stack?.includes('\x1B')) {
43+
if (htmlError.length > 0)
44+
htmlError += filter.toHtml(escapeHtml((startStrWithX1B ? error.stackStr : error.stack) as string))
45+
else
46+
htmlError = `<b>${error.nameStr || error.name}</b>: ${error.message}${filter.toHtml(escapeHtml((startStrWithX1B ? error.stackStr : error.stack) as string))}`
47+
}
48+
49+
if (htmlError.length > 0)
50+
return htmlError
51+
return null
52+
}
53+
3654
function mapLeveledTaskStacks(dark: boolean, tasks: LeveledTask[]) {
3755
const filter = createAnsiToHtmlFilter(dark)
3856
return tasks.map((t) => {
3957
const result = t.result
40-
if (result) {
41-
const error = result.error
42-
if (error) {
43-
let htmlError = ''
44-
if (error.message.includes('\x1B'))
45-
htmlError = `<b>${error.nameStr || error.name}</b>: ${filter.toHtml(escapeHtml(error.message))}`
46-
47-
const startStrWithX1B = error.stackStr?.includes('\x1B')
48-
if (startStrWithX1B || error.stack?.includes('\x1B')) {
49-
if (htmlError.length > 0)
50-
htmlError += filter.toHtml(escapeHtml((startStrWithX1B ? error.stackStr : error.stack) as string))
51-
else
52-
htmlError = `<b>${error.nameStr || error.name}</b>: ${error.message}${filter.toHtml(escapeHtml((startStrWithX1B ? error.stackStr : error.stack) as string))}`
53-
}
54-
55-
if (htmlError.length > 0)
56-
result.htmlError = htmlError
57-
}
58-
}
58+
if (!result)
59+
return t
60+
const errors = result.errors
61+
?.map(error => createHtmlError(filter, error))
62+
.filter(error => error != null)
63+
.join('<br><br>')
64+
if (errors?.length)
65+
result.htmlError = errors
5966
return t
6067
})
6168
}
@@ -64,7 +71,7 @@ const failed = computed(() => {
6471
const file = props.file
6572
const failedFlatMap = file?.tasks?.flatMap(t => collectFailed(t, 0)) ?? []
6673
const result = file?.result
67-
const fileError = result?.error
74+
const fileError = result?.errors?.[0]
6875
// we must check also if the test cannot compile
6976
if (fileError) {
7077
// create a dummy one
@@ -81,24 +88,6 @@ const failed = computed(() => {
8188
}
8289
return failedFlatMap.length > 0 ? mapLeveledTaskStacks(isDark.value, failedFlatMap) : failedFlatMap
8390
})
84-
85-
function relative(p: string) {
86-
if (p.startsWith(config.value.root))
87-
return p.slice(config.value.root.length)
88-
return p
89-
}
90-
91-
interface Diff { error: NonNullable<Pick<ErrorWithDiff, 'expected' | 'actual'>> }
92-
type ResultWithDiff = Task['result'] & Diff
93-
function isDiffShowable(result?: Task['result']): result is ResultWithDiff {
94-
return result && result?.error?.expected && result?.error?.actual
95-
}
96-
97-
function diff(result: ResultWithDiff): string {
98-
return unifiedDiff(result.error.actual, result.error.expected, {
99-
outputTruncateLength: 80,
100-
})
101-
}
10291
</script>
10392

10493
<template>
@@ -117,23 +106,15 @@ function diff(result: ResultWithDiff): string {
117106
<div v-if="task.result?.htmlError" class="scrolls scrolls-rounded task-error">
118107
<pre v-html="task.result.htmlError" />
119108
</div>
120-
<div v-else-if="task.result?.error" class="scrolls scrolls-rounded task-error">
121-
<pre><b>{{ task.result.error.name || task.result.error.nameStr }}</b>: {{ task.result.error.message }}</pre>
122-
<div v-for="(stack, i) of task.result.error.stacks" :key="i" class="op80 flex gap-x-2 items-center" data-testid="stack">
123-
<pre> - {{ relative(stack.file) }}:{{ stack.line }}:{{ stack.column }}</pre>
124-
<div
125-
v-if="shouldOpenInEditor(stack.file, props.file?.name)"
126-
v-tooltip.bottom="'Open in Editor'"
127-
class="i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em"
128-
tabindex="0"
129-
aria-label="Open in Editor"
130-
@click.passive="openInEditor(stack.file, stack.line, stack.column)"
131-
/>
132-
</div>
133-
<pre v-if="isDiffShowable(task.result)">
134-
{{ `\n${diff(task.result)}` }}
135-
</pre>
136-
</div>
109+
<template v-else-if="task.result?.errors">
110+
<ViewReportError
111+
v-for="(error, idx) of task.result.errors"
112+
:key="idx"
113+
:error="error"
114+
:filename="file?.name"
115+
:root="config.root"
116+
/>
117+
</template>
137118
</div>
138119
</div>
139120
</template>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<script setup lang="ts">
2+
import type { ErrorWithDiff } from '#types'
3+
import { unifiedDiff } from '~/composables/diff'
4+
import { openInEditor, shouldOpenInEditor } from '~/composables/error'
5+
6+
const props = defineProps<{
7+
root: string
8+
filename?: string
9+
error: ErrorWithDiff
10+
}>()
11+
12+
function relative(p: string) {
13+
if (p.startsWith(props.root))
14+
return p.slice(props.root.length)
15+
return p
16+
}
17+
18+
const isDiffShowable = computed(() => {
19+
return props.error?.expected && props.error?.actual
20+
})
21+
22+
function diff() {
23+
return unifiedDiff(props.error.actual, props.error.expected, {
24+
outputTruncateLength: 80,
25+
})
26+
}
27+
</script>
28+
29+
<template>
30+
<div class="scrolls scrolls-rounded task-error">
31+
<pre><b>{{ error.name || error.nameStr }}</b>: {{ error.message }}</pre>
32+
<div v-for="(stack, i) of error.stacks" :key="i" class="op80 flex gap-x-2 items-center" data-testid="stack">
33+
<pre> - {{ relative(stack.file) }}:{{ stack.line }}:{{ stack.column }}</pre>
34+
<div
35+
v-if="shouldOpenInEditor(stack.file, filename)"
36+
v-tooltip.bottom="'Open in Editor'"
37+
class="i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em"
38+
tabindex="0"
39+
aria-label="Open in Editor"
40+
@click.passive="openInEditor(stack.file, stack.line, stack.column)"
41+
/>
42+
</div>
43+
<pre v-if="isDiffShowable">
44+
{{ `\n${diff()}` }}
45+
</pre>
46+
</div>
47+
</template>
48+
49+
<style scoped>
50+
.task-error {
51+
--cm-ttc-c-thumb: #CCC;
52+
}
53+
html.dark .task-error {
54+
--cm-ttc-c-thumb: #444;
55+
}
56+
</style>

‎packages/vitest/src/api/setup.ts

+4
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,12 @@ class WebSocketReporter implements Reporter {
124124
return
125125

126126
packs.forEach(([, result]) => {
127+
// TODO remove after "error" deprecation is removed
127128
if (result?.error)
128129
result.error.stacks = parseStacktrace(result.error)
130+
result?.errors?.forEach((error) => {
131+
error.stacks = parseStacktrace(error)
132+
})
129133
})
130134

131135
this.clients.forEach((client) => {

‎packages/vitest/src/node/reporters/base.ts

+11-8
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ export abstract class BaseReporter implements Reporter {
8585
// print short errors, full errors will be at the end in summary
8686
for (const test of failed) {
8787
logger.log(c.red(` ${pointer} ${getFullName(test)}`))
88-
logger.log(c.red(` ${F_RIGHT} ${(test.result!.error as any)?.message}`))
88+
test.result?.errors?.forEach((e) => {
89+
logger.log(c.red(` ${F_RIGHT} ${(e as any)?.message}`))
90+
})
8991
}
9092
}
9193
}
@@ -258,7 +260,7 @@ export abstract class BaseReporter implements Reporter {
258260
const suites = getSuites(files)
259261
const tests = getTests(files)
260262

261-
const failedSuites = suites.filter(i => i.result?.error)
263+
const failedSuites = suites.filter(i => i.result?.errors)
262264
const failedTests = tests.filter(i => i.result?.state === 'fail')
263265
const failedTotal = failedSuites.length + failedTests.length
264266

@@ -310,12 +312,13 @@ export abstract class BaseReporter implements Reporter {
310312
const errorsQueue: [error: ErrorWithDiff | undefined, tests: Task[]][] = []
311313
for (const task of tasks) {
312314
// merge identical errors
313-
const error = task.result?.error
314-
const errorItem = error?.stackStr && errorsQueue.find(i => i[0]?.stackStr === error.stackStr)
315-
if (errorItem)
316-
errorItem[1].push(task)
317-
else
318-
errorsQueue.push([error, [task]])
315+
task.result?.errors?.forEach((error) => {
316+
const errorItem = error?.stackStr && errorsQueue.find(i => i[0]?.stackStr === error.stackStr)
317+
if (errorItem)
318+
errorItem[1].push(task)
319+
else
320+
errorsQueue.push([error, [task]])
321+
})
319322
}
320323
for (const [error, tasks] of errorsQueue) {
321324
for (const task of tasks) {

‎packages/vitest/src/node/reporters/json.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class JsonReporter implements Reporter {
7676
const numTotalTestSuites = suites.length
7777
const tests = getTests(files)
7878
const numTotalTests = tests.length
79-
const numFailedTestSuites = suites.filter(s => s.result?.error).length
79+
const numFailedTestSuites = suites.filter(s => s.result?.errors).length
8080
const numPassedTestSuites = numTotalTestSuites - numFailedTestSuites
8181
const numPendingTestSuites = suites.filter(s => s.result?.state === 'run').length
8282
const numFailedTests = tests.filter(t => t.result?.state === 'fail').length
@@ -109,7 +109,7 @@ export class JsonReporter implements Reporter {
109109
status: StatusMap[t.result?.state || t.mode] || 'skipped',
110110
title: t.name,
111111
duration: t.result?.duration,
112-
failureMessages: t.result?.error?.message == null ? [] : [t.result.error.message],
112+
failureMessages: t.result?.errors?.map(e => e.message) || [],
113113
location: await this.getFailureLocation(t),
114114
} as FormattedAssertionResult
115115
}))
@@ -128,7 +128,7 @@ export class JsonReporter implements Reporter {
128128
t.result?.state === 'fail')
129129
? 'failed'
130130
: 'passed',
131-
message: file.result?.error?.message ?? '',
131+
message: file.result?.errors?.[0]?.message ?? '',
132132
name: file.filepath,
133133
})
134134
}
@@ -179,7 +179,7 @@ export class JsonReporter implements Reporter {
179179
}
180180

181181
protected async getFailureLocation(test: Task): Promise<Callsite | undefined> {
182-
const error = test.result?.error
182+
const error = test.result?.errors?.[0]
183183
if (!error)
184184
return
185185

‎packages/vitest/src/node/reporters/junit.ts

+12-11
Original file line numberDiff line numberDiff line change
@@ -167,17 +167,18 @@ export class JUnitReporter implements Reporter {
167167
await this.logger.log('<skipped/>')
168168

169169
if (task.result?.state === 'fail') {
170-
const error = task.result.error
171-
172-
await this.writeElement('failure', {
173-
message: error?.message,
174-
type: error?.name ?? error?.nameStr,
175-
}, async () => {
176-
if (!error)
177-
return
178-
179-
await this.writeErrorDetails(error)
180-
})
170+
const promises = task.result.errors?.map(async (error) => {
171+
await this.writeElement('failure', {
172+
message: error?.message,
173+
type: error?.name ?? error?.nameStr,
174+
}, async () => {
175+
if (!error)
176+
return
177+
178+
await this.writeErrorDetails(error)
179+
})
180+
}) || []
181+
await Promise.all(promises)
181182
}
182183
})
183184
}

‎packages/vitest/src/node/reporters/tap.ts

+16-15
Original file line numberDiff line numberDiff line change
@@ -66,27 +66,28 @@ export class TapReporter implements Reporter {
6666
else {
6767
this.logger.log(`${ok} ${id} - ${tapString(task.name)}${comment}`)
6868

69-
if (task.result?.state === 'fail' && task.result.error) {
69+
if (task.result?.state === 'fail' && task.result.errors) {
7070
this.logger.indent()
7171

72-
const error = task.result.error
73-
const stacks = parseStacktrace(error)
74-
const stack = stacks[0]
72+
task.result.errors.forEach((error) => {
73+
const stacks = parseStacktrace(error)
74+
const stack = stacks[0]
7575

76-
this.logger.log('---')
77-
this.logger.log('error:')
76+
this.logger.log('---')
77+
this.logger.log('error:')
7878

79-
this.logger.indent()
80-
this.logErrorDetails(error)
81-
this.logger.unindent()
79+
this.logger.indent()
80+
this.logErrorDetails(error)
81+
this.logger.unindent()
8282

83-
if (stack)
84-
this.logger.log(`at: ${yamlString(`${stack.file}:${stack.line}:${stack.column}`)}`)
83+
if (stack)
84+
this.logger.log(`at: ${yamlString(`${stack.file}:${stack.line}:${stack.column}`)}`)
8585

86-
if (error.showDiff) {
87-
this.logger.log(`actual: ${yamlString(error.actual)}`)
88-
this.logger.log(`expected: ${yamlString(error.expected)}`)
89-
}
86+
if (error.showDiff) {
87+
this.logger.log(`actual: ${yamlString(error.actual)}`)
88+
this.logger.log(`expected: ${yamlString(error.expected)}`)
89+
}
90+
})
9091

9192
this.logger.log('...')
9293
this.logger.unindent()

‎packages/vitest/src/node/reporters/verbose.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ export class VerboseReporter extends DefaultReporter {
2121
if (this.ctx.config.logHeapUsage && task.result.heap != null)
2222
title += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`)
2323
this.ctx.logger.log(title)
24-
if (task.result.state === 'fail')
25-
this.ctx.logger.log(c.red(` ${F_RIGHT} ${(task.result.error as any)?.message}`))
24+
if (task.result.state === 'fail') {
25+
task.result.errors?.forEach((error) => {
26+
this.ctx.logger.log(c.red(` ${F_RIGHT} ${error?.message}`))
27+
})
28+
}
2629
}
2730
}
2831
}

‎packages/vitest/src/runtime/collect.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,11 @@ export async function collectTests(paths: string[], config: ResolvedConfig): Pro
8282
file.collectDuration = now() - collectStart
8383
}
8484
catch (e) {
85+
const error = processError(e)
8586
file.result = {
8687
state: 'fail',
87-
error: processError(e),
88+
error,
89+
errors: [error],
8890
}
8991
if (config.browser)
9092
console.error(e)

‎packages/vitest/src/runtime/run.ts

+22-8
Original file line numberDiff line numberDiff line change
@@ -181,17 +181,21 @@ export async function runTest(test: Test) {
181181
test.result.state = 'pass'
182182
}
183183
catch (e) {
184+
const error = processError(e)
184185
test.result.state = 'fail'
185-
test.result.error = processError(e)
186+
test.result.error = error
187+
test.result.errors = [error]
186188
}
187189

188190
try {
189191
await callSuiteHook(test.suite, test, 'afterEach', [test.context, test.suite])
190192
await callCleanupHooks(beforeEachCleanups)
191193
}
192194
catch (e) {
195+
const error = processError(e)
193196
test.result.state = 'fail'
194-
test.result.error = processError(e)
197+
test.result.error = error
198+
test.result.errors = [error]
195199
}
196200

197201
if (test.result.state === 'pass')
@@ -207,12 +211,15 @@ export async function runTest(test: Test) {
207211
// if test is marked to be failed, flip the result
208212
if (test.fails) {
209213
if (test.result.state === 'pass') {
214+
const error = processError(new Error('Expect test to fail'))
210215
test.result.state = 'fail'
211-
test.result.error = processError(new Error('Expect test to fail'))
216+
test.result.error = error
217+
test.result.errors = [error]
212218
}
213219
else {
214220
test.result.state = 'pass'
215221
test.result.error = undefined
222+
test.result.errors = undefined
216223
}
217224
}
218225

@@ -302,8 +309,10 @@ export async function runSuite(suite: Suite) {
302309
await callCleanupHooks(beforeAllCleanups)
303310
}
304311
catch (e) {
312+
const error = processError(e)
305313
suite.result.state = 'fail'
306-
suite.result.error = processError(e)
314+
suite.result.error = error
315+
suite.result.errors = [error]
307316
}
308317
}
309318
suite.result.duration = now() - start
@@ -314,8 +323,11 @@ export async function runSuite(suite: Suite) {
314323
if (suite.mode === 'run') {
315324
if (!hasTests(suite)) {
316325
suite.result.state = 'fail'
317-
if (!suite.result.error)
318-
suite.result.error = new Error(`No test found in suite ${suite.name}`)
326+
if (!suite.result.error) {
327+
const error = processError(new Error(`No test found in suite ${suite.name}`))
328+
suite.result.error = error
329+
suite.result.errors = [error]
330+
}
319331
}
320332
else if (hasFailed(suite)) {
321333
suite.result.state = 'fail'
@@ -446,10 +458,12 @@ async function runSuites(suites: Suite[]) {
446458
export async function runFiles(files: File[], config: ResolvedConfig) {
447459
for (const file of files) {
448460
if (!file.tasks.length && !config.passWithNoTests) {
449-
if (!file.result?.error) {
461+
if (!file.result?.errors?.length) {
462+
const error = processError(new Error(`No test suite found in file ${file.filepath}`))
450463
file.result = {
451464
state: 'fail',
452-
error: new Error(`No test suite found in file ${file.filepath}`),
465+
error,
466+
errors: [error],
453467
}
454468
}
455469
}

‎packages/vitest/src/types/tasks.ts

+4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ export interface TaskResult {
2323
duration?: number
2424
startTime?: number
2525
heap?: number
26+
/**
27+
* @deprecated Use "errors" instead
28+
*/
2629
error?: ErrorWithDiff
30+
errors?: ErrorWithDiff[]
2731
htmlError?: string
2832
hooks?: Partial<Record<keyof SuiteHooks, TaskState>>
2933
benchmark?: BenchmarkResult

‎packages/vitest/src/utils/tasks.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,7 @@ export function hasFailed(suite: Arrayable<Task>): boolean {
4444

4545
export function hasFailedSnapshot(suite: Arrayable<Task>): boolean {
4646
return getTests(suite).some((s) => {
47-
const message = s.result?.error?.message
48-
return message?.match(/Snapshot .* mismatched/)
47+
return s.result?.errors?.some(e => e.message.match(/Snapshot .* mismatched/))
4948
})
5049
}
5150

‎test/reporters/src/data-for-junit.ts

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ function createSuiteHavingFailedTestWithXmlInError(): File[] {
4242
result: {
4343
state: 'fail',
4444
error: errorWithXml,
45+
errors: [errorWithXml],
4546
duration: 2.123123123,
4647
},
4748
context: null as any,

‎test/reporters/src/data.ts

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ const innerTasks: Task[] = [
6363
result: {
6464
state: 'fail',
6565
error,
66+
errors: [error],
6667
duration: 1.4422860145568848,
6768
},
6869
context: null as any,

‎test/reporters/tests/__snapshots__/html.test.ts.snap

+16
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ exports[`html reporter > resolves to "failing" status for test file "json-fail"
5252
"toJSON": "Function<anonymous>",
5353
"toString": "Function<toString>",
5454
},
55+
"errors": [
56+
{
57+
"actual": "2",
58+
"constructor": "Function<AssertionError>",
59+
"expected": "1",
60+
"message": "expected 2 to deeply equal 1",
61+
"name": "AssertionError",
62+
"nameStr": "AssertionError",
63+
"operator": "strictEqual",
64+
"showDiff": true,
65+
"stack": "AssertionError: expected 2 to deeply equal 1",
66+
"stackStr": "AssertionError: expected 2 to deeply equal 1",
67+
"toJSON": "Function<anonymous>",
68+
"toString": "Function<toString>",
69+
},
70+
],
5571
"hooks": {
5672
"afterEach": "pass",
5773
"beforeEach": "pass",

‎test/reporters/tests/html.test.ts

+3
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,11 @@ describe.skipIf(skip)('html reporter', async () => {
6767
task.result.duration = 0
6868
task.result.startTime = 0
6969
expect(task.result.error).toBeDefined()
70+
expect(task.result.errors).toBeDefined()
7071
task.result.error.stack = task.result.error.stack.split('\n')[0]
72+
task.result.errors[0].stack = task.result.errors[0].stack.split('\n')[0]
7173
task.result.error.stackStr = task.result.error.stackStr.split('\n')[0]
74+
task.result.errors[0].stackStr = task.result.errors[0].stackStr.split('\n')[0]
7275
expect(task.logs).toBeDefined()
7376
expect(task.logs).toHaveLength(1)
7477
task.logs[0].taskId = 0

0 commit comments

Comments
 (0)
Please sign in to comment.