[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(ui): fix duplicate colored error message (#10258)

Co-authored-by: Codex <noreply@openai.com>

authored by

Hiroshi Ogawa
Codex
and committed by
GitHub
(May 11, 2026, 5:49 PM +0200) 035e3bb7 29cb06b3

+42 -92
+1 -1
test/ui/README.md
··· 5 5 pnpm test 6 6 7 7 # run fixture projects 8 - pnpm test-fixtures 8 + pnpm test-fixtures --ui 9 9 pnpm test-fixtures --root fixtures-trace 10 10 ```
+8
test/ui/fixtures/error.test.ts
··· 4 4 it('escape html in error diff', () => { 5 5 expect('<style>* {border: 2px solid green};</style>').toBe("") 6 6 }) 7 + 8 + it('colored error message', () => { 9 + const blue = '\x1B[34m' 10 + const reset = '\x1B[0m' 11 + const message = `${blue}this-is-blue${reset}` 12 + const error = new Error(message) 13 + throw error 14 + })
+7 -3
test/ui/test/html-report.spec.ts
··· 1 + import type { Page } from '@playwright/test' 1 2 import type { PreviewServer } from 'vite' 2 3 import { readFileSync } from 'node:fs' 3 4 import { Writable } from 'node:stream' ··· 52 53 await page.goto(pageUrl) 53 54 54 55 // dashboard 55 - await expect(page.getByTestId('pass-entry')).toContainText('17 Pass') 56 - await expect(page.getByTestId('fail-entry')).toContainText('2 Fail') 57 - await expect(page.getByTestId('total-entry')).toContainText('19 Total') 56 + await assertTestCounts(page, { pass: 17, fail: 3 }) 58 57 59 58 // unhandled errors 60 59 await expect(page.getByTestId('unhandled-errors')).toContainText( ··· 264 263 }) 265 264 }) 266 265 }) 266 + 267 + async function assertTestCounts(page: Page, options: { pass: number; fail: number }) { 268 + await expect.soft(page.getByTestId('tests-entry')) 269 + .toContainText(`${options.pass} Pass ${options.fail} Fail ${options.pass + options.fail} Total`) 270 + }
+15 -3
test/ui/test/ui.spec.ts
··· 1 + import type { Page } from '@playwright/test' 1 2 import type { Vitest } from 'vitest/node' 2 3 import { readFileSync } from 'node:fs' 3 4 import { Writable } from 'node:stream' ··· 71 72 await page.goto(pageUrl) 72 73 73 74 // dashboard 74 - await expect(page.getByTestId('pass-entry')).toContainText('17 Pass') 75 - await expect(page.getByTestId('fail-entry')).toContainText('2 Fail') 76 - await expect(page.getByTestId('total-entry')).toContainText('19 Total') 75 + await assertTestCounts(page, { pass: 17, fail: 3 }) 77 76 78 77 // unhandled errors 79 78 await expect(page.getByTestId('unhandled-errors')).toContainText( ··· 254 253 await item.hover() 255 254 await item.getByTestId('btn-open-details').click({ force: true }) 256 255 await expect(page.getByTestId('diff')).toContainText('- Expected + Received + <style>* {border: 2px solid green};</style>') 256 + 257 + await getExplorerItem(page, 'colored error message').click() 258 + await expect(page.getByTestId('report')).toHaveText('Error: this-is-blue - /fixtures/error.test.ts:12:17') 257 259 }) 258 260 259 261 test('file-filter', async ({ page }) => { ··· 375 377 }) 376 378 }) 377 379 }) 380 + 381 + // TODO: consolidate in https://github.com/vitest-dev/vitest/pull/10237 382 + function getExplorerItem(page: Page, name: string) { 383 + return page.getByTestId('explorer-item').and(page.getByLabel(name, { exact: true })) 384 + } 385 + 386 + async function assertTestCounts(page: Page, options: { pass: number; fail: number }) { 387 + await expect.soft(page.getByTestId('tests-entry')) 388 + .toContainText(`${options.pass} Pass ${options.fail} Fail ${options.pass + options.fail} Total`) 389 + } 378 390 379 391 test.describe('standalone', () => { 380 392 let vitest: Vitest | undefined
+1 -57
packages/ui/client/composables/error.ts
··· 1 - import type Convert from 'ansi-to-html' 2 - import type { RunnerTask, TestError } from 'vitest' 1 + import type { TestError } from 'vitest' 3 2 import { parseStacktrace } from '@vitest/utils/source-map' 4 3 import Filter from 'ansi-to-html' 5 - import { escapeHtml } from '~/utils/escape' 6 - 7 - declare module '@vitest/runner' { 8 - interface TaskResult { 9 - htmlError?: string 10 - } 11 - } 12 4 13 5 export function isTestFile(name: string, fileName?: string) { 14 6 return fileName && name.endsWith(fileName) ··· 59 51 }) 60 52 61 53 return error 62 - } 63 - 64 - function createHtmlError(filter: Convert, error: TestError) { 65 - let htmlError = '' 66 - if (error.message?.includes('\x1B')) { 67 - htmlError = `<b>${error.name}</b>: ${filter.toHtml( 68 - escapeHtml(error.message), 69 - )}` 70 - } 71 - 72 - const startStrWithX1B = error.stack?.includes('\x1B') 73 - if (startStrWithX1B) { 74 - if (htmlError.length > 0) { 75 - htmlError += filter.toHtml( 76 - escapeHtml((error.stack) as string), 77 - ) 78 - } 79 - else { 80 - htmlError = `<b>${error.name}</b>: ${ 81 - error.message 82 - }${filter.toHtml( 83 - escapeHtml((error.stack) as string), 84 - )}` 85 - } 86 - } 87 - 88 - if (htmlError.length > 0) { 89 - return htmlError 90 - } 91 - return null 92 - } 93 - 94 - export function mapLeveledTaskStacks(dark: boolean, tasks: RunnerTask[]) { 95 - const filter = createAnsiToHtmlFilter(dark) 96 - return tasks.map((t) => { 97 - const result = t.result 98 - if (!result || result.htmlError) { 99 - return t 100 - } 101 - const errors = result.errors 102 - ?.map(error => createHtmlError(filter, error)) 103 - .filter(error => error != null) 104 - .join('<br><br>') 105 - if (errors?.length) { 106 - result.htmlError = errors 107 - } 108 - return t 109 - }) 110 54 }
+3 -16
packages/ui/client/components/views/ViewReport.vue
··· 2 2 import type { RunnerTask, RunnerTestFile, RunnerTestSuite } from 'vitest' 3 3 import { computed } from 'vue' 4 4 import { config } from '~/composables/client' 5 - import { isDark } from '~/composables/dark' 6 - import { mapLeveledTaskStacks } from '~/composables/error' 7 5 import FailureScreenshot from '../FailureScreenshot.vue' 8 6 import ViewReportError from './ViewReportError.vue' 9 7 ··· 53 51 } 54 52 failedFlatMap.unshift(fileErrorTask) 55 53 } 56 - return failedFlatMap.length > 0 57 - ? mapLeveledTaskStacks(isDark.value, failedFlatMap) 58 - : failedFlatMap 54 + return failedFlatMap 59 55 }) 60 56 </script> 61 57 ··· 70 66 m-2 71 67 rounded 72 68 :style="{ 73 - 'margin-left': `${ 74 - task.result?.htmlError ? 0.5 : 2 * (task as LeveledTask).level + 0.5 75 - }rem`, 69 + 'margin-left': `${2 * (task as LeveledTask).level + 0.5}rem`, 76 70 }" 77 71 > 78 72 <div flex="~ gap-2 items-center"> 79 73 <span>{{ task.name }}</span> 80 74 <FailureScreenshot :task="task" /> 81 75 </div> 82 - <div 83 - v-if="task.result?.htmlError" 84 - class="scrolls scrolls-rounded task-error" 85 - data-testid="task-error" 86 - > 87 - <pre v-html="task.result.htmlError" /> 88 - </div> 89 - <template v-else-if="task.result?.errors && config.root"> 76 + <template v-if="task.result?.errors && config.root"> 90 77 <ViewReportError 91 78 v-for="(error, idx) of task.result.errors" 92 79 :key="idx"
+5 -1
packages/ui/client/components/views/ViewReportError.vue
··· 32 32 : undefined, 33 33 ) 34 34 35 + const message = computed(() => 36 + filter.value.toHtml(escapeHtml(props.error.message || '')), 37 + ) 38 + 35 39 function showCode(stack: ParsedStack) { 36 40 if (isTestFile(stack.file, props.filename)) { 37 41 return showLocationSource(props.fileId, stack) ··· 42 46 43 47 <template> 44 48 <div class="scrolls scrolls-rounded task-error"> 45 - <pre><b>{{ error.name }}</b>: {{ error.message }}</pre> 49 + <pre><b>{{ error.name }}</b>: <span v-html="message" /></pre> 46 50 <div 47 51 v-for="(stack, i) of error.stacks" 48 52 :key="i"
+2 -11
packages/ui/client/components/views/ViewTestReport.vue
··· 3 3 import { computed } from 'vue' 4 4 import { getAttachmentUrl, sanitizeFilePath } from '~/composables/attachments' 5 5 import { config } from '~/composables/client' 6 - import { isDark } from '~/composables/dark' 7 - import { mapLeveledTaskStacks } from '~/composables/error' 8 6 import { getLocationString, openLocation } from '~/composables/location' 9 7 import AnnotationAttachmentImage from '../AnnotationAttachmentImage.vue' 10 8 import Artifacts from '../artifacts/Artifacts.vue' ··· 19 17 if (!props.test.result || !props.test.result.errors?.length) { 20 18 return null 21 19 } 22 - return mapLeveledTaskStacks(isDark.value, [props.test])[0] as RunnerTestCase | null 20 + return props.test 23 21 }) 24 22 25 23 // filter out internal TaskMeta ··· 47 45 rounded 48 46 > 49 47 <FailureScreenshot :task="test" /> 50 - <div 51 - v-if="test.result?.htmlError" 52 - class="scrolls scrolls-rounded task-error" 53 - data-testid="task-error" 54 - > 55 - <pre v-html="test.result.htmlError" /> 56 - </div> 57 - <template v-else-if="test.result?.errors && config.root"> 48 + <template v-if="test.result?.errors && config.root"> 58 49 <ViewReportError 59 50 v-for="(error, idx) of test.result.errors" 60 51 :key="idx"