Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat!: deprecate "error" on result, store errors in "errors" (#2586)
  • Loading branch information
sheremet-va committed Jan 1, 2023
1 parent 894f155 commit e641a11
Show file tree
Hide file tree
Showing 18 changed files with 242 additions and 147 deletions.
57 changes: 30 additions & 27 deletions packages/ui/client/components/views/ViewEditor.vue
Expand Up @@ -4,7 +4,7 @@ import type CodeMirror from 'codemirror'
import { createTooltip, destroyTooltip } from 'floating-vue'
import { openInEditor } from '../../composables/error'
import { client } from '~/composables/client'
import type { File } from '#types'
import type { ErrorWithDiff, File, ParsedStack } from '#types'
const props = defineProps<{
file?: File
Expand Down Expand Up @@ -60,6 +60,34 @@ watch(draft, (d) => {
emit('draft', d)
}, { immediate: true })
function createErrorElement(e: ErrorWithDiff) {
const stacks = (e?.stacks || []).filter(i => i.file && i.file === props.file?.filepath)
const stack = stacks?.[0]
if (!stack)
return
const div = document.createElement('div')
div.className = 'op80 flex gap-x-2 items-center'
const pre = document.createElement('pre')
pre.className = 'c-red-600 dark:c-red-400'
pre.textContent = `${' '.repeat(stack.column)}^ ${e?.nameStr}: ${e?.message}`
div.appendChild(pre)
const span = document.createElement('span')
span.className = 'i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em'
span.tabIndex = 0
span.ariaLabel = 'Open in Editor'
const tooltip = createTooltip(span, {
content: 'Open in Editor',
placement: 'bottom',
}, false)
const el: EventListener = async () => {
await openInEditor(stack.file, stack.line, stack.column)
}
div.appendChild(span)
listeners.push([span, el, () => destroyTooltip(span)])
handles.push(cm.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10'))
widgets.push(cm.value!.addLineWidget(stack.line - 1, div))
}
watch([cm, failed], ([cmValue]) => {
if (!cmValue) {
clearListeners()
Expand All @@ -76,32 +104,7 @@ watch([cm, failed], ([cmValue]) => {
cmValue.on('changes', codemirrorChanges)
failed.value.forEach((i) => {
const e = i.result?.error
const stacks = (e?.stacks || []).filter(i => i.file && i.file === props.file?.filepath)
if (stacks.length) {
const stack = stacks[0]
const div = document.createElement('div')
div.className = 'op80 flex gap-x-2 items-center'
const pre = document.createElement('pre')
pre.className = 'c-red-600 dark:c-red-400'
pre.textContent = `${' '.repeat(stack.column)}^ ${e?.nameStr}: ${e?.message}`
div.appendChild(pre)
const span = document.createElement('span')
span.className = 'i-carbon-launch c-red-600 dark:c-red-400 hover:cursor-pointer min-w-1em min-h-1em'
span.tabIndex = 0
span.ariaLabel = 'Open in Editor'
const tooltip = createTooltip(span, {
content: 'Open in Editor',
placement: 'bottom',
}, false)
const el: EventListener = async () => {
await openInEditor(stacks[0].file, stack.line, stack.column)
}
div.appendChild(span)
listeners.push([span, el, () => destroyTooltip(span)])
handles.push(cm.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10'))
widgets.push(cm.value!.addLineWidget(stack.line - 1, div))
}
i.result?.errors?.forEach(createErrorElement)
})
if (!hasBeenEdited.value)
cmValue.clearHistory() // Prevent getting access to initial state
Expand Down
21 changes: 12 additions & 9 deletions packages/ui/client/components/views/ViewReport.cy.tsx
Expand Up @@ -17,6 +17,12 @@ const makeTextStack = () => ({
// 5 Stacks
const textStacks = Array.from(new Array(5)).map(makeTextStack)

const error = {
name: 'Do some test',
stacks: textStacks,
message: 'Error: Transform failed with 1 error:',
}

const fileWithTextStacks = {
id: 'f-1',
name: 'test/plain-stack-trace.ts',
Expand All @@ -25,11 +31,8 @@ const fileWithTextStacks = {
filepath: 'test/plain-stack-trace.ts',
result: {
state: 'fail',
error: {
name: 'Do some test',
stacks: textStacks,
message: 'Error: Transform failed with 1 error:',
},
error,
errors: [error],
},
tasks: [],
}
Expand Down Expand Up @@ -67,11 +70,11 @@ describe('ViewReport', () => {
filepath: 'test/plain-stack-trace.ts',
result: {
state: 'fail',
error: {
errors: [{
name: 'Do some test',
stack: '\x1B[33mtest/plain-stack-trace.ts\x1B[0m',
message: 'Error: Transform failed with 1 error:',
},
}],
},
tasks: [],
}
Expand Down Expand Up @@ -104,11 +107,11 @@ describe('ViewReport', () => {
filepath: 'test/plain-stack-trace.ts',
result: {
state: 'fail',
error: {
errors: [{
name: 'Do some test',
stack: '\x1B[33mtest/plain-stack-trace.ts\x1B[0m',
message: '\x1B[44mError: Transform failed with 1 error:\x1B[0m',
},
}],
},
tasks: [],
}
Expand Down
101 changes: 41 additions & 60 deletions packages/ui/client/components/views/ViewReport.vue
@@ -1,10 +1,10 @@
<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 type Convert from 'ansi-to-html'
import ViewReportError from './ViewReportError.vue'
import type { ErrorWithDiff, File, Suite, Task } from '#types'
import { isDark } from '~/composables/dark'
import { createAnsiToHtmlFilter } from '~/composables/error'
import { config } from '~/composables/client'
const props = defineProps<{
file?: File
Expand All @@ -18,7 +18,7 @@ function collectFailed(task: Task, level: number): LeveledTask[] {
if (task.result?.state !== 'fail')
return []
if (task.type === 'test' || task.type === 'benchmark')
if (task.type === 'test' || task.type === 'benchmark' || task.type === 'typecheck')
return [{ ...task, level }]
else
return [{ ...task, level }, ...task.tasks.flatMap(t => collectFailed(t, level + 1))]
Expand All @@ -33,29 +33,36 @@ function escapeHtml(unsafe: string) {
.replace(/'/g, '&#039;')
}
function createHtmlError(filter: Convert, error: ErrorWithDiff) {
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)
return htmlError
return null
}
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
}
}
if (!result)
return t
const errors = result.errors
?.map(error => createHtmlError(filter, error))
.filter(error => error != null)
.join('<br><br>')
if (errors?.length)
result.htmlError = errors
return t
})
}
Expand All @@ -64,7 +71,7 @@ const failed = computed(() => {
const file = props.file
const failedFlatMap = file?.tasks?.flatMap(t => collectFailed(t, 0)) ?? []
const result = file?.result
const fileError = result?.error
const fileError = result?.errors?.[0]
// we must check also if the test cannot compile
if (fileError) {
// create a dummy one
Expand All @@ -81,24 +88,6 @@ const failed = computed(() => {
}
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>
Expand All @@ -117,23 +106,15 @@ function diff(result: ResultWithDiff): string {
<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>
<template v-else-if="task.result?.errors">
<ViewReportError
v-for="(error, idx) of task.result.errors"
:key="idx"
:error="error"
:filename="file?.name"
:root="config.root"
/>
</template>
</div>
</div>
</template>
Expand Down
56 changes: 56 additions & 0 deletions packages/ui/client/components/views/ViewReportError.vue
@@ -0,0 +1,56 @@
<script setup lang="ts">
import type { ErrorWithDiff } from '#types'
import { unifiedDiff } from '~/composables/diff'
import { openInEditor, shouldOpenInEditor } from '~/composables/error'
const props = defineProps<{
root: string
filename?: string
error: ErrorWithDiff
}>()
function relative(p: string) {
if (p.startsWith(props.root))
return p.slice(props.root.length)
return p
}
const isDiffShowable = computed(() => {
return props.error?.expected && props.error?.actual
})
function diff() {
return unifiedDiff(props.error.actual, props.error.expected, {
outputTruncateLength: 80,
})
}
</script>

<template>
<div class="scrolls scrolls-rounded task-error">
<pre><b>{{ error.name || error.nameStr }}</b>: {{ error.message }}</pre>
<div v-for="(stack, i) of 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, filename)"
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">
{{ `\n${diff()}` }}
</pre>
</div>
</template>

<style scoped>
.task-error {
--cm-ttc-c-thumb: #CCC;
}
html.dark .task-error {
--cm-ttc-c-thumb: #444;
}
</style>
4 changes: 4 additions & 0 deletions packages/vitest/src/api/setup.ts
Expand Up @@ -124,8 +124,12 @@ class WebSocketReporter implements Reporter {
return

packs.forEach(([, result]) => {
// TODO remove after "error" deprecation is removed
if (result?.error)
result.error.stacks = parseStacktrace(result.error)
result?.errors?.forEach((error) => {
error.stacks = parseStacktrace(error)
})
})

this.clients.forEach((client) => {
Expand Down
19 changes: 11 additions & 8 deletions packages/vitest/src/node/reporters/base.ts
Expand Up @@ -85,7 +85,9 @@ export abstract class BaseReporter implements Reporter {
// print short errors, full errors will be at the end in summary
for (const test of failed) {
logger.log(c.red(` ${pointer} ${getFullName(test)}`))
logger.log(c.red(` ${F_RIGHT} ${(test.result!.error as any)?.message}`))
test.result?.errors?.forEach((e) => {
logger.log(c.red(` ${F_RIGHT} ${(e as any)?.message}`))
})
}
}
}
Expand Down Expand Up @@ -258,7 +260,7 @@ export abstract class BaseReporter implements Reporter {
const suites = getSuites(files)
const tests = getTests(files)

const failedSuites = suites.filter(i => i.result?.error)
const failedSuites = suites.filter(i => i.result?.errors)
const failedTests = tests.filter(i => i.result?.state === 'fail')
const failedTotal = failedSuites.length + failedTests.length

Expand Down Expand Up @@ -310,12 +312,13 @@ export abstract class BaseReporter implements Reporter {
const errorsQueue: [error: ErrorWithDiff | undefined, tests: Task[]][] = []
for (const task of tasks) {
// merge identical errors
const error = task.result?.error
const errorItem = error?.stackStr && errorsQueue.find(i => i[0]?.stackStr === error.stackStr)
if (errorItem)
errorItem[1].push(task)
else
errorsQueue.push([error, [task]])
task.result?.errors?.forEach((error) => {
const errorItem = error?.stackStr && errorsQueue.find(i => i[0]?.stackStr === error.stackStr)
if (errorItem)
errorItem[1].push(task)
else
errorsQueue.push([error, [task]])
})
}
for (const [error, tasks] of errorsQueue) {
for (const task of tasks) {
Expand Down

0 comments on commit e641a11

Please sign in to comment.