@@ -125,6 +138,9 @@ function column(stack: ParsedStack) {
@click.passive="openInEditor(stack.file, line(stack), column(stack))"
/>
+
+ {{ `\n${diff(task.result)}` }}
+
diff --git a/packages/ui/client/composables/diff.ts b/packages/ui/client/composables/diff.ts
new file mode 100644
index 000000000000..a8b0792b2280
--- /dev/null
+++ b/packages/ui/client/composables/diff.ts
@@ -0,0 +1,96 @@
+import * as diff from 'diff'
+
+export interface DiffOptions {
+ outputTruncateLength?: number
+ outputDiffLines?: number
+ showLegend?: boolean
+}
+
+function formatLine(line: string, maxWidth: number) {
+ return line.slice(0, maxWidth) + (line.length > maxWidth ? '…' : '')
+}
+
+export function unifiedDiff(actual: string, expected: string, options: DiffOptions = {}) {
+ if (actual === expected)
+ return ''
+
+ const { outputTruncateLength = 80, outputDiffLines, showLegend = true } = options
+
+ const indent = ' '
+ const diffLimit = outputDiffLines || 15
+
+ const counts = {
+ '+': 0,
+ '-': 0,
+ }
+ let previousState: '-' | '+' | null = null
+ let previousCount = 0
+ function preprocess(line: string) {
+ if (!line || line.match(/\\ No newline/))
+ return
+
+ const char = line[0] as '+' | '-'
+ if ('-+'.includes(char)) {
+ if (previousState !== char) {
+ previousState = char
+ previousCount = 0
+ }
+ previousCount++
+ counts[char]++
+ if (previousCount === diffLimit)
+ return `${char} ...`
+ else if (previousCount > diffLimit)
+ return
+ }
+ return line
+ }
+
+ const msg = diff.createPatch('string', expected, actual)
+ const lines = msg.split('\n').slice(5).map(preprocess).filter(Boolean) as string[]
+ const isCompact = counts['+'] === 1 && counts['-'] === 1 && lines.length === 2
+
+ let formatted = lines.map((line: string) => {
+ line = line.replace(/\\"/g, '"')
+ if (line[0] === '-') {
+ line = formatLine(line.slice(1), outputTruncateLength)
+ if (isCompact)
+ return line
+ return `- ${formatLine(line, outputTruncateLength)}`
+ }
+ if (line[0] === '+') {
+ line = formatLine(line.slice(1), outputTruncateLength)
+ if (isCompact)
+ return line
+ return `+ ${formatLine(line, outputTruncateLength)}`
+ }
+ if (line.match(/@@/))
+ return '--'
+ return ` ${line}`
+ })
+
+ if (showLegend) {
+ // Compact mode
+ if (isCompact) {
+ formatted = [
+ `- Expected ${formatted[0]}`,
+ `+ Received ${formatted[1]}`,
+ ]
+ }
+ else {
+ if (formatted[0].includes('"'))
+ formatted[0] = formatted[0].replace('"', '')
+
+ const last = formatted.length - 1
+ if (formatted[last].endsWith('"'))
+ formatted[last] = formatted[last].slice(0, formatted[last].length - 1)
+
+ formatted.unshift(
+ `- Expected - ${counts['-']}`,
+ `+ Received + ${counts['+']}`,
+ '',
+ )
+ }
+ }
+
+ return formatted.map(i => indent + i).join('\n')
+}
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 5bf693d62493..35585c7f8708 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -55,6 +55,7 @@
"codemirror-theme-vars": "^0.1.1",
"cypress": "^11.0.1",
"d3-graph-controller": "^2.3.22",
+ "diff": "^5.1.0",
"flatted": "^3.2.7",
"floating-vue": "^2.0.0-y.0",
"picocolors": "^1.0.0",
diff --git a/packages/vitest/src/integrations/chai/jest-expect.ts b/packages/vitest/src/integrations/chai/jest-expect.ts
index 8eb7f0d60b61..3c703654755e 100644
--- a/packages/vitest/src/integrations/chai/jest-expect.ts
+++ b/packages/vitest/src/integrations/chai/jest-expect.ts
@@ -5,7 +5,7 @@ import { isMockFunction } from '../spy'
import { addSerializer } from '../snapshot/port/plugins'
import type { Constructable, Test } from '../../types'
import { assertTypes } from '../../utils'
-import { unifiedDiff } from '../../node/diff'
+import { unifiedDiff } from '../../utils/diff'
import type { ChaiPlugin, MatcherState } from '../../types/chai'
import { arrayBufferEquality, generateToBeMessage, iterableEquality, equals as jestEquals, sparseArrayEquality, subsetEquality, typeEquality } from './jest-utils'
import type { AsymmetricMatcher } from './jest-asymmetric-matchers'
diff --git a/packages/vitest/src/integrations/chai/jest-matcher-utils.ts b/packages/vitest/src/integrations/chai/jest-matcher-utils.ts
index e0f017f37883..e12424394beb 100644
--- a/packages/vitest/src/integrations/chai/jest-matcher-utils.ts
+++ b/packages/vitest/src/integrations/chai/jest-matcher-utils.ts
@@ -4,7 +4,7 @@
import c from 'picocolors'
import type { PrettyFormatOptions } from 'pretty-format'
import { format as prettyFormat, plugins as prettyFormatPlugins } from 'pretty-format'
-import { unifiedDiff } from '../../node/diff'
+import { unifiedDiff } from '../../utils/diff'
import type { DiffOptions, MatcherHintOptions } from '../../types/matcher-utils'
export const EXPECTED_COLOR = c.green
diff --git a/packages/vitest/src/node/error.ts b/packages/vitest/src/node/error.ts
index cb06ed529dc6..74a3287c6f94 100644
--- a/packages/vitest/src/node/error.ts
+++ b/packages/vitest/src/node/error.ts
@@ -8,8 +8,8 @@ import { lineSplitRE, parseStacktrace, posToNumber } from '../utils/source-map'
import { F_POINTER } from '../utils/figures'
import { stringify } from '../integrations/chai/jest-matcher-utils'
import { TypeCheckError } from '../typecheck/typechecker'
+import { type DiffOptions, unifiedDiff } from '../utils/diff'
import type { Vitest } from './core'
-import { type DiffOptions, unifiedDiff } from './diff'
import { divider } from './reporters/renderers/utils'
import type { Logger } from './logger'
diff --git a/packages/vitest/src/node/diff.ts b/packages/vitest/src/utils/diff.ts
similarity index 100%
rename from packages/vitest/src/node/diff.ts
rename to packages/vitest/src/utils/diff.ts
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 751d78f0f580..5880b5477d51 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -697,6 +697,7 @@ importers:
codemirror-theme-vars: ^0.1.1
cypress: ^11.0.1
d3-graph-controller: ^2.3.22
+ diff: ^5.1.0
flatted: ^3.2.7
floating-vue: ^2.0.0-y.0
picocolors: ^1.0.0
@@ -730,6 +731,7 @@ importers:
codemirror-theme-vars: 0.1.1
cypress: 11.0.1
d3-graph-controller: 2.3.22
+ diff: 5.1.0
flatted: 3.2.7
floating-vue: 2.0.0-y.0_vue@3.2.41
picocolors: 1.0.0