Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: deprecate "error" on result, store errors in "errors" #2586

Merged
merged 2 commits into from Jan 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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