fix(ui): fix duplicate colored error message (#10258) · vitest-dev/vitest@035e3bb (original) (raw)
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -2,8 +2,6 @@ | ||
| 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,9 +51,7 @@ const failed = computed(() => { | ||
| 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,23 +66,14 @@ const failed = computed(() => { | ||
| 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" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -32,6 +32,10 @@ const diff = computed(() => | ||
| 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,7 +46,7 @@ function showCode(stack: ParsedStack) { | ||
| 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" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -3,8 +3,6 @@ import type { RunnerTestCase } from 'vitest' | ||
| 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,7 +17,7 @@ const failed = computed(() => { | ||
| 19 | 17 | if (!props.test.result | |
| 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,14 +45,7 @@ const meta = computed(() => { | ||
| 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" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,14 +1,6 @@ | ||
| 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) |
| @@ -60,51 +52,3 @@ export function parseError(e: unknown) { | ||
| 60 | 52 | |
| 61 | 53 | return error |
| 62 | 54 | } |
| 63 | - | |
| 64 | -function createHtmlError(filter: Convert, error: TestError) { | |
| 65 | -let htmlError = '' | |
| 66 | -if (error.message?.includes('\x1B')) { | |
| 67 | -htmlError = `${error.name}: ${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 = `${error.name}: ${ | |
| 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 | | |
| 99 | -return t | |
| 100 | -} | |
| 101 | -const errors = result.errors | |
| 102 | -?.map(error => createHtmlError(filter, error)) | |
| 103 | -.filter(error => error != null) | |
| 104 | -.join(' ') |
|
| 105 | -if (errors?.length) { | |
| 106 | -result.htmlError = errors | |
| 107 | -} | |
| 108 | -return t | |
| 109 | -}) | |
| 110 | -} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -5,6 +5,6 @@ | ||
| 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 | ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -4,3 +4,11 @@ import { expect, it } from "vitest" | ||
| 4 | 4 | it('escape html in error diff', () => { |
| 5 | 5 | expect('').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 | +}) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| 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,9 +53,7 @@ test.describe('html report', () => { | ||
| 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,3 +263,8 @@ test.describe('html report', () => { | ||
| 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.failFail{options.fail} Fail options.failFail{options.pass + options.fail} Total`) | |
| 270 | +} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| 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,9 +72,7 @@ test.describe('ui', () => { | ||
| 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,6 +253,9 @@ test.describe('ui', () => { | ||
| 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 + ') |
| 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 }) => { |
| @@ -376,6 +378,16 @@ test.describe('ui', () => { | ||
| 376 | 378 | }) |
| 377 | 379 | }) |
| 378 | 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.failFail{options.fail} Fail options.failFail{options.pass + options.fail} Total`) | |
| 389 | +} | |
| 390 | + | |
| 379 | 391 | test.describe('standalone', () => { |
| 380 | 392 | let vitest: Vitest | undefined |
| 381 | 393 |