Skip to content

Commit 446308d

Browse files
authoredMar 29, 2023
feat!: use "concordance" package to display diff instead of using custom diff (#2828)
1 parent 287dc20 commit 446308d

29 files changed

+333
-437
lines changed
 

‎docs/config/index.md

+18-52
Original file line numberDiff line numberDiff line change
@@ -449,58 +449,24 @@ Custom reporters for output. Reporters can be [a Reporter instance](https://gith
449449
- `'hanging-process'` - displays a list of hanging processes, if Vitest cannot exit process safely. This might be a heavy operation, enable it only if Vitest consistently cannot exit process
450450
- path of a custom reporter (e.g. `'./path/to/reporter.ts'`, `'@scope/reporter'`)
451451

452-
### outputTruncateLength
453-
454-
- **Type:** `number`
455-
- **Default:** `stdout.columns || 80`
456-
- **CLI:** `--outputTruncateLength=<length>`, `--output-truncate-length=<length>`
457-
458-
Truncate the size of diff line up to `stdout.columns` or `80` number of characters. You may wish to tune this, depending on your terminal window width. Vitest includes `+-` characters and spaces for this. For example, you might see this diff, if you set this to `6`:
459-
460-
```diff
461-
// actual line: "Text that seems correct"
462-
- Text...
463-
+ Test...
464-
```
465-
466-
### outputDiffLines
467-
468-
- **Type:** `number`
469-
- **Default:** `15`
470-
- **CLI:** `--outputDiffLines=<lines>`, `--output-diff-lines=<lines>`
471-
472-
Limit the number of single output diff lines up to `15`. Vitest counts all `+-` lines when determining when to stop. For example, you might see diff like this, if you set this property to `3`:
473-
474-
```diff
475-
- test: 1,
476-
+ test: 2,
477-
- obj: '1',
478-
...
479-
- test2: 1,
480-
+ test2: 1,
481-
- obj2: '2',
482-
...
483-
```
484-
485-
### outputDiffMaxLines
486-
487-
- **Type:** `number`
488-
- **Default:** `50`
489-
- **CLI:** `--outputDiffMaxLines=<lines>`, `--output-diff-max-lines=<lines>`
490-
- **Version:** Since Vitest 0.26.0
491-
492-
The maximum number of lines to display in diff window. Beware that if you have a large object with many small diffs, you might not see all of them at once.
493-
494-
### outputDiffMaxSize
495-
496-
- **Type:** `number`
497-
- **Default:** `10000`
498-
- **CLI:** `--outputDiffMaxSize=<length>`, `--output-diff-max-size=<length>`
499-
- **Version:** Since Vitest 0.26.0
500-
501-
The maximum length of the stringified object before the diff happens. Vitest tries to stringify an object before doing a diff, but if the object is too large, it will reduce the depth of the object to fit within this limit. Because of this, if the object is too big or nested, you might not see the diff.
502-
503-
Increasing this limit can increase the duration of diffing.
452+
### outputDiffLines
453+
454+
- **Type:** `number`
455+
- **Default:** `15`
456+
- **CLI:** `--outputDiffLines=<lines>`, `--output-diff-lines=<lines>`
457+
458+
Limit the number of single output diff lines up to `15`. Vitest counts all `+-` lines when determining when to stop. For example, you might see diff like this, if you set this property to `3`:
459+
460+
```diff
461+
- test: 1,
462+
+ test: 2,
463+
- obj: '1',
464+
...
465+
- test2: 1,
466+
+ test2: 1,
467+
- obj2: '2',
468+
...
469+
```
504470

505471
### outputFile
506472

‎docs/guide/cli.md

-3
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,6 @@ Run only [benchmark](https://vitest.dev/guide/features.html#benchmarking-experim
7373
| `--silent` | Silent console output from tests |
7474
| `--isolate` | Isolate environment for each test file (default: `true`) |
7575
| `--reporter <name>` | Select reporter: `default`, `verbose`, `dot`, `junit`, `json`, or a path to a custom reporter |
76-
| `--outputDiffMaxSize <length>` | Object diff output max size (default: 10000) |
77-
| `--outputDiffMaxLines <lines>` | Max lines in diff output window (default: 50) |
78-
| `--outputTruncateLength <length>` | Truncate output diff lines up to `<length>` number of characters. |
7976
| `--outputDiffLines <lines>` | Limit number of output diff lines up to `<lines>`. |
8077
| `--outputFile <filename/-s>` | Write test results to a file when the `--reporter=json` or `--reporter=junit` option is also specified <br /> Via [cac's dot notation] you can specify individual outputs for multiple reporters |
8178
| `--coverage` | Enable coverage report |

‎packages/expect/src/jest-matcher-utils.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,7 @@ export function getMatcherUtils() {
104104

105105
// TODO: do something with options
106106
export function diff(a: any, b: any, options?: DiffOptions) {
107-
const c = getColors()
108-
return unifiedDiff(stringify(b), stringify(a), {
109-
colorDim: c.dim,
110-
colorSuccess: c.green,
111-
colorError: c.red,
107+
return unifiedDiff(b, a, {
112108
showLegend: options?.showLegend,
113109
})
114110
}

‎packages/runner/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
},
4040
"dependencies": {
4141
"@vitest/utils": "workspace:*",
42+
"concordance": "^5.0.4",
4243
"p-limit": "^4.0.0",
4344
"pathe": "^1.1.0"
4445
}

‎packages/runner/rollup.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const external = [
88
...builtinModules,
99
...Object.keys(pkg.dependencies || {}),
1010
...Object.keys(pkg.peerDependencies || {}),
11+
'@vitest/utils/diff',
1112
]
1213

1314
const entries = {

‎packages/runner/src/run.ts

+6-6
Original file line numberDiff line numberDiff line change
@@ -150,15 +150,15 @@ export async function runTest(test: Test, runner: VitestRunner) {
150150
test.result.state = 'pass'
151151
}
152152
catch (e) {
153-
failTask(test.result, e)
153+
failTask(test.result, e, runner)
154154
}
155155

156156
try {
157157
await callSuiteHook(test.suite, test, 'afterEach', runner, [test.context, test.suite])
158158
await callCleanupHooks(beforeEachCleanups)
159159
}
160160
catch (e) {
161-
failTask(test.result, e)
161+
failTask(test.result, e, runner)
162162
}
163163

164164
if (test.result.state === 'pass')
@@ -195,9 +195,9 @@ export async function runTest(test: Test, runner: VitestRunner) {
195195
updateTask(test, runner)
196196
}
197197

198-
function failTask(result: TaskResult, err: unknown) {
198+
function failTask(result: TaskResult, err: unknown, runner: VitestRunner) {
199199
result.state = 'fail'
200-
const error = processError(err)
200+
const error = processError(err, runner.config)
201201
result.error = error
202202
result.errors ??= []
203203
result.errors.push(error)
@@ -268,15 +268,15 @@ export async function runSuite(suite: Suite, runner: VitestRunner) {
268268
}
269269
}
270270
catch (e) {
271-
failTask(suite.result, e)
271+
failTask(suite.result, e, runner)
272272
}
273273

274274
try {
275275
await callSuiteHook(suite, suite, 'afterAll', runner, [suite])
276276
await callCleanupHooks(beforeAllCleanups)
277277
}
278278
catch (e) {
279-
failTask(suite.result, e)
279+
failTask(suite.result, e, runner)
280280
}
281281
}
282282

‎packages/runner/src/types/runner.ts

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface VitestRunnerConfig {
1313
hooks: SequenceHooks
1414
setupFiles: SequenceSetupFiles
1515
}
16+
outputDiffLines?: number
1617
maxConcurrency: number
1718
testTimeout: number
1819
hookTimeout: number

‎packages/runner/src/utils/error.ts

+8-11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { deepClone, format, getOwnProperties, getType, stringify } from '@vitest/utils'
2+
import type { DiffOptions } from '@vitest/utils/diff'
3+
import { unifiedDiff } from '@vitest/utils/diff'
24

35
export interface ParsedStack {
46
method: string
@@ -14,6 +16,7 @@ export interface ErrorWithDiff extends Error {
1416
stackStr?: string
1517
stacks?: ParsedStack[]
1618
showDiff?: boolean
19+
diff?: string
1720
actual?: any
1821
expected?: any
1922
operator?: string
@@ -102,11 +105,7 @@ function normalizeErrorMessage(message: string) {
102105
return message.replace(/__vite_ssr_import_\d+__\./g, '')
103106
}
104107

105-
interface ProcessErrorOptions {
106-
outputDiffMaxSize?: number
107-
}
108-
109-
export function processError(err: any, options: ProcessErrorOptions = {}) {
108+
export function processError(err: any, options: DiffOptions = {}) {
110109
if (!err || typeof err !== 'object')
111110
return err
112111
// stack is not serialized in worker communication
@@ -121,15 +120,13 @@ export function processError(err: any, options: ProcessErrorOptions = {}) {
121120

122121
const { replacedActual, replacedExpected } = replaceAsymmetricMatcher(clonedActual, clonedExpected)
123122

124-
err.actual = replacedActual
125-
err.expected = replacedExpected
126-
127-
const maxDiffSize = options.outputDiffMaxSize ?? 10000
123+
if (err.showDiff || (err.showDiff === undefined && err.expected !== undefined && err.actual !== undefined))
124+
err.diff = unifiedDiff(replacedActual, replacedExpected, options)
128125

129126
if (typeof err.expected !== 'string')
130-
err.expected = stringify(err.expected, 10, { maxLength: maxDiffSize })
127+
err.expected = stringify(err.expected, 10)
131128
if (typeof err.actual !== 'string')
132-
err.actual = stringify(err.actual, 10, { maxLength: maxDiffSize })
129+
err.actual = stringify(err.actual, 10)
133130

134131
// some Error implementations don't allow rewriting message
135132
try {

‎packages/ui/client/components/views/ViewReportError.vue

+1-3
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ const isDiffShowable = computed(() => {
2020
})
2121
2222
function diff() {
23-
return unifiedDiff(props.error.actual, props.error.expected, {
24-
outputTruncateLength: 80,
25-
})
23+
return unifiedDiff(props.error.actual, props.error.expected)
2624
}
2725
</script>
2826

+1-96
Original file line numberDiff line numberDiff line change
@@ -1,96 +1 @@
1-
import * as diff from 'diff'
2-
3-
export interface DiffOptions {
4-
outputTruncateLength?: number
5-
outputDiffLines?: number
6-
showLegend?: boolean
7-
}
8-
9-
function formatLine(line: string, maxWidth: number) {
10-
return line.slice(0, maxWidth) + (line.length > maxWidth ? '…' : '')
11-
}
12-
13-
export function unifiedDiff(actual: string, expected: string, options: DiffOptions = {}) {
14-
if (actual === expected)
15-
return ''
16-
17-
const { outputTruncateLength = 80, outputDiffLines, showLegend = true } = options
18-
19-
const indent = ' '
20-
const diffLimit = outputDiffLines || 15
21-
22-
const counts = {
23-
'+': 0,
24-
'-': 0,
25-
}
26-
let previousState: '-' | '+' | null = null
27-
let previousCount = 0
28-
function preprocess(line: string) {
29-
if (!line || line.match(/\\ No newline/))
30-
return
31-
32-
const char = line[0] as '+' | '-'
33-
if ('-+'.includes(char)) {
34-
if (previousState !== char) {
35-
previousState = char
36-
previousCount = 0
37-
}
38-
previousCount++
39-
counts[char]++
40-
if (previousCount === diffLimit)
41-
return `${char} ...`
42-
else if (previousCount > diffLimit)
43-
return
44-
}
45-
return line
46-
}
47-
48-
const msg = diff.createPatch('string', expected, actual)
49-
const lines = msg.split('\n').slice(5).map(preprocess).filter(Boolean) as string[]
50-
const isCompact = counts['+'] === 1 && counts['-'] === 1 && lines.length === 2
51-
52-
let formatted = lines.map((line: string) => {
53-
line = line.replace(/\\"/g, '"')
54-
if (line[0] === '-') {
55-
line = formatLine(line.slice(1), outputTruncateLength)
56-
if (isCompact)
57-
return line
58-
return `- ${formatLine(line, outputTruncateLength)}`
59-
}
60-
if (line[0] === '+') {
61-
line = formatLine(line.slice(1), outputTruncateLength)
62-
if (isCompact)
63-
return line
64-
return `+ ${formatLine(line, outputTruncateLength)}`
65-
}
66-
if (line.match(/@@/))
67-
return '--'
68-
return ` ${line}`
69-
})
70-
71-
if (showLegend) {
72-
// Compact mode
73-
if (isCompact) {
74-
formatted = [
75-
`- Expected ${formatted[0]}`,
76-
`+ Received ${formatted[1]}`,
77-
]
78-
}
79-
else {
80-
if (formatted[0].includes('"'))
81-
formatted[0] = formatted[0].replace('"', '')
82-
83-
const last = formatted.length - 1
84-
if (formatted[last].endsWith('"'))
85-
formatted[last] = formatted[last].slice(0, formatted[last].length - 1)
86-
87-
formatted.unshift(
88-
`- Expected - ${counts['-']}`,
89-
`+ Received + ${counts['+']}`,
90-
'',
91-
)
92-
}
93-
}
94-
95-
return formatted.map(i => indent + i).join('\n')
96-
}
1+
export { unifiedDiff } from '@vitest/utils/diff'

‎packages/ui/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"prepublishOnly": "pnpm build"
4040
},
4141
"dependencies": {
42+
"@vitest/utils": "workspace:*",
4243
"fast-glob": "^3.2.12",
4344
"flatted": "^3.2.7",
4445
"pathe": "^1.1.0",
@@ -64,7 +65,6 @@
6465
"codemirror-theme-vars": "^0.1.1",
6566
"cypress": "^12.3.0",
6667
"d3-graph-controller": "^2.5.1",
67-
"diff": "^5.1.0",
6868
"floating-vue": "^2.0.0-y.0",
6969
"rollup": "^2.79.1",
7070
"splitpanes": "^3.1.5",

‎packages/utils/package.json

+1-5
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,8 @@
3737
"prepublishOnly": "pnpm build"
3838
},
3939
"dependencies": {
40-
"cli-truncate": "^3.1.0",
41-
"diff": "^5.1.0",
40+
"concordance": "^5.0.4",
4241
"loupe": "^2.3.6",
4342
"pretty-format": "^27.5.1"
44-
},
45-
"devDependencies": {
46-
"@types/diff": "^5.0.2"
4743
}
4844
}

‎packages/utils/src/colors.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ const colorsMap = {
2929

3030
type ColorName = keyof typeof colorsMap
3131
type ColorsMethods = {
32-
[Key in ColorName]: (input: unknown) => string
32+
[Key in ColorName]: {
33+
(input: unknown): string
34+
open: string
35+
close: string
36+
}
3337
}
3438

3539
type Colors = ColorsMethods & {

‎packages/utils/src/descriptors.ts

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import concordance, { type DisplayOptions } from 'concordance'
2+
import { getColors } from './colors'
3+
4+
export const getConcordanceTheme = () => {
5+
const c = getColors()
6+
7+
// this theme is taken from ava: https://github.com/avajs/ava/blob/main/lib/concordance-options.js
8+
// no adjustments were made so far except for the diff padding
9+
return {
10+
boolean: c.yellow,
11+
circular: c.gray('[Circular]'),
12+
date: {
13+
invalid: c.red('invalid'),
14+
value: c.blue,
15+
},
16+
diffGutters: {
17+
actual: ` ${c.red('-')} `,
18+
expected: ` ${c.green('+')} `,
19+
padding: ' ',
20+
},
21+
error: {
22+
ctor: { open: `${c.gray.open}(`, close: `)${c.gray.close}` },
23+
name: c.magenta,
24+
},
25+
function: {
26+
name: c.blue,
27+
stringTag: c.magenta,
28+
},
29+
global: c.magenta,
30+
item: { after: c.gray(',') },
31+
list: { openBracket: c.gray('['), closeBracket: c.gray(']') },
32+
mapEntry: { after: c.gray(',') },
33+
maxDepth: c.gray('…'),
34+
null: c.yellow,
35+
number: c.yellow,
36+
object: {
37+
openBracket: c.gray('{'),
38+
closeBracket: c.gray('}'),
39+
ctor: c.magenta,
40+
stringTag: { open: `${c.magenta.open}@`, close: c.magenta.close },
41+
secondaryStringTag: { open: `${c.gray.open}@`, close: c.gray.close },
42+
},
43+
property: {
44+
after: c.gray(','),
45+
keyBracket: { open: c.gray('['), close: c.gray(']') },
46+
valueFallback: c.gray('…'),
47+
},
48+
regexp: {
49+
source: { open: `${c.blue.open}/`, close: `/${c.blue.close}` },
50+
flags: c.yellow,
51+
},
52+
stats: { separator: c.gray('---') },
53+
string: {
54+
open: c.blue.open,
55+
close: c.blue.close,
56+
line: { open: c.blue('\''), close: c.blue('\'') },
57+
multiline: { start: c.blue('`'), end: c.blue('`') },
58+
controlPicture: c.gray,
59+
diff: {
60+
insert: {
61+
open: c.bgGreen.open + c.black.open,
62+
close: c.black.close + c.bgGreen.close,
63+
},
64+
delete: {
65+
open: c.bgRed.open + c.black.open,
66+
close: c.black.close + c.bgRed.close,
67+
},
68+
equal: c.blue,
69+
insertLine: {
70+
open: c.green.open,
71+
close: c.green.close,
72+
},
73+
deleteLine: {
74+
open: c.red.open,
75+
close: c.red.close,
76+
},
77+
},
78+
},
79+
symbol: c.yellow,
80+
typedArray: {
81+
bytes: c.yellow,
82+
},
83+
undefined: c.yellow,
84+
}
85+
}
86+
87+
export function diffDescriptors(actual: unknown, expected: unknown, options: DisplayOptions) {
88+
return concordance.diff(expected, actual, options)
89+
}
90+
91+
export function formatDescriptor(value: unknown, options: DisplayOptions) {
92+
return concordance.formatDescriptor(value, options)
93+
}

‎packages/utils/src/diff.ts

+41-100
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
1-
import * as diff from 'diff'
2-
import cliTruncate from 'cli-truncate'
3-
4-
export function formatLine(line: string, outputTruncateLength?: number) {
5-
return cliTruncate(line, (outputTruncateLength ?? (process.stdout?.columns || 80)) - 4)
6-
}
7-
8-
type Color = (str: string) => string
1+
import { getColors } from './colors'
2+
import { diffDescriptors, getConcordanceTheme } from './descriptors'
93

104
export interface DiffOptions {
11-
outputDiffMaxLines?: number
12-
outputTruncateLength?: number
13-
outputDiffLines?: number
145
showLegend?: boolean
15-
16-
colorSuccess?: Color
17-
colorError?: Color
18-
colorDim?: Color
6+
outputDiffLines?: number
197
}
208

219
/**
@@ -26,107 +14,60 @@ export interface DiffOptions {
2614
* @param {String} expected
2715
* @return {string} The diff.
2816
*/
17+
export function unifiedDiff(actual: unknown, expected: unknown, options: DiffOptions = {}) {
18+
const theme = getConcordanceTheme()
19+
const diff = diffDescriptors(actual, expected, { theme })
2920

30-
export function unifiedDiff(actual: string, expected: string, options: DiffOptions = {}) {
31-
if (actual === expected)
32-
return ''
33-
34-
const { outputTruncateLength, outputDiffLines, outputDiffMaxLines, showLegend = true } = options
35-
36-
const indent = ' '
37-
const diffLimit = outputDiffLines || 15
38-
const diffMaxLines = outputDiffMaxLines || 50
21+
const { showLegend = true, outputDiffLines = 15 } = options
3922

4023
const counts = {
4124
'+': 0,
4225
'-': 0,
4326
}
44-
let previousState: '-' | '+' | null = null
45-
let previousCount = 0
46-
47-
const str = (str: string) => str
48-
const dim = options.colorDim || str
49-
const green = options.colorSuccess || str
50-
const red = options.colorError || str
51-
function preprocess(line: string) {
52-
if (!line || line.match(/\\ No newline/))
53-
return
54-
55-
const char = line[0] as '+' | '-'
56-
if ('-+'.includes(char)) {
57-
if (previousState !== char) {
58-
previousState = char
59-
previousCount = 0
60-
}
61-
previousCount++
62-
counts[char]++
63-
if (previousCount === diffLimit)
64-
return dim(`${char} ...`)
65-
else if (previousCount > diffLimit)
66-
return
27+
const c = getColors()
28+
const plus = theme.diffGutters.actual
29+
const minus = ` ${c.green('+')}`
30+
31+
const lines = diff.split(/\r?\n/g)
32+
let firstErrorLine: number | null = null
33+
lines.forEach((line, index) => {
34+
if (line.startsWith(plus)) {
35+
firstErrorLine ??= index
36+
counts['+']++
6737
}
68-
return line
69-
}
70-
71-
const msg = diff.createPatch('string', expected, actual)
72-
let lines = msg.split('\n').slice(5).map(preprocess).filter(Boolean) as string[]
73-
let moreLines = 0
74-
const isCompact = counts['+'] === 1 && counts['-'] === 1 && lines.length === 2
75-
76-
if (lines.length > diffMaxLines) {
77-
const firstDiff = lines.findIndex(line => line[0] === '-' || line[0] === '+')
78-
const displayLines = lines.slice(firstDiff - 2, diffMaxLines)
79-
const lastDisplayedIndex = firstDiff - 2 + diffMaxLines
80-
if (lastDisplayedIndex < lines.length)
81-
moreLines = lines.length - lastDisplayedIndex
82-
lines = displayLines
83-
}
84-
85-
let formatted = lines.map((line: string) => {
86-
line = line.replace(/\\"/g, '"')
87-
if (line[0] === '-') {
88-
line = formatLine(line.slice(1), outputTruncateLength)
89-
if (isCompact)
90-
return green(line)
91-
return green(`- ${formatLine(line, outputTruncateLength)}`)
92-
}
93-
if (line[0] === '+') {
94-
line = formatLine(line.slice(1), outputTruncateLength)
95-
if (isCompact)
96-
return red(line)
97-
return red(`+ ${formatLine(line, outputTruncateLength)}`)
38+
else if (line.startsWith(minus)) {
39+
firstErrorLine ??= index
40+
counts['-']++
9841
}
99-
if (line.match(/@@/))
100-
return '--'
101-
return ` ${line}`
10242
})
43+
const isCompact = counts['+'] === 1 && counts['-'] === 1 && lines.length === 2
10344

104-
if (moreLines)
105-
formatted.push(dim(`... ${moreLines} more lines`))
45+
let legend = ''
10646

10747
if (showLegend) {
108-
// Compact mode
109-
if (isCompact) {
110-
formatted = [
111-
`${green('- Expected')} ${formatted[0]}`,
112-
`${red('+ Received')} ${formatted[1]}`,
113-
]
48+
if (!isCompact) {
49+
legend = ` ${c.green(`- Expected - ${counts['-']}`)}
50+
${c.red(`+ Received + ${counts['+']}`)}
51+
52+
`
11453
}
11554
else {
116-
if (formatted[0].includes('"'))
117-
formatted[0] = formatted[0].replace('"', '')
55+
legend = ' Difference:\n\n'
56+
}
57+
}
11858

119-
const last = formatted.length - 1
120-
if (formatted[last].endsWith('"'))
121-
formatted[last] = formatted[last].slice(0, formatted[last].length - 1)
59+
if (firstErrorLine != null && outputDiffLines) {
60+
const start = Math.max(0, firstErrorLine - 1)
61+
const end = Math.min(lines.length, firstErrorLine + outputDiffLines)
62+
const linesAfterCount = lines.length - end
12263

123-
formatted.unshift(
124-
green(`- Expected - ${counts['-']}`),
125-
red(`+ Received + ${counts['+']}`),
126-
'',
127-
)
128-
}
64+
const linesBefore = start ? ` ${c.gray(`... ${start} more line${start > 1 ? 's' : ''}\n`)}` : ''
65+
const linesAfter = linesAfterCount ? `\n ${c.gray(`... ${linesAfterCount} more line${linesAfterCount > 1 ? 's' : ''}\n`)}` : ''
66+
const diffOutput = lines.slice(start, end).map(line => line.replace(/\s*$/, '')).join('\n')
67+
const helperBunner = linesAfter && (counts['+'] + counts['-'] > outputDiffLines) ? `\n Use ${c.gray('test.outputDiffLines')} to increase the number of lines shown.` : ''
68+
69+
return legend + linesBefore + diffOutput + linesAfter + helperBunner
12970
}
13071

131-
return formatted.map(i => i ? (indent + i) : i).join('\n')
72+
return legend + diff
13273
}

‎packages/utils/src/external.d.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
declare module 'concordance' {
2+
interface DisplayOptions {
3+
theme?: any
4+
maxDepth?: number
5+
}
6+
7+
export function diff(expected: unknown, actual: unknown, options?: DisplayOptions): string
8+
export function formatDescriptor(descriptor: unknown, options?: DisplayOptions): string
9+
}

‎packages/utils/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export * from './display'
77
export * from './constants'
88
export * from './colors'
99
export * from './error'
10+
export * from './descriptors'

‎packages/vitest/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
"acorn-walk": "^8.2.0",
146146
"cac": "^6.7.14",
147147
"chai": "^4.3.7",
148+
"concordance": "^5.0.4",
148149
"debug": "^4.3.4",
149150
"local-pkg": "^0.4.2",
150151
"pathe": "^1.1.0",
@@ -166,15 +167,14 @@
166167
"@types/diff": "^5.0.2",
167168
"@types/istanbul-lib-coverage": "^2.0.4",
168169
"@types/istanbul-reports": "^3.0.1",
169-
"@types/jsdom": "^21.1.0",
170+
"@types/jsdom": "^20.0.1",
170171
"@types/micromatch": "^4.0.2",
171172
"@types/natural-compare": "^1.4.1",
172173
"@types/prompts": "^2.4.2",
173174
"@types/sinonjs__fake-timers": "^8.1.2",
174175
"birpc": "^0.2.3",
175176
"chai-subset": "^1.6.0",
176177
"cli-truncate": "^3.1.0",
177-
"diff": "^5.1.0",
178178
"event-target-polyfill": "^0.0.3",
179179
"execa": "^7.0.0",
180180
"expect-type": "^0.15.0",

‎packages/vitest/src/node/cli.ts

-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ cli
2525
.option('--silent', 'Silent console output from tests')
2626
.option('--isolate', 'Isolate environment for each test file (default: true)')
2727
.option('--reporter <name>', 'Specify reporters')
28-
.option('--outputDiffMaxSize <length>', 'Object diff output max size (default: 10000)')
29-
.option('--outputDiffMaxLines <length>', 'Max lines in diff output window (default: 50)')
30-
.option('--outputTruncateLength <length>', 'Diff output line length (default: 80)')
3128
.option('--outputDiffLines <lines>', 'Number of lines in single diff (default: 15)')
3229
.option('--outputFile <filename/-s>', 'Write test results to a file when supporter reporter is also specified, use cac\'s dot notation for individual outputs of multiple reporters')
3330
.option('--coverage', 'Enable coverage report')

‎packages/vitest/src/node/error.ts

+4-19
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { existsSync, readFileSync } from 'fs'
33
import { normalize, relative } from 'pathe'
44
import c from 'picocolors'
55
import cliTruncate from 'cli-truncate'
6-
import { type DiffOptions, unifiedDiff } from '@vitest/utils/diff'
76
import { stringify } from '@vitest/utils'
87
import type { ErrorWithDiff, ParsedStack } from '../types'
98
import { lineSplitRE, parseErrorStacktrace, positionToOffset } from '../utils/source-map'
@@ -96,16 +95,8 @@ export async function printError(error: unknown, ctx: Vitest, options: PrintErro
9695
handleImportOutsideModuleError(e.stack || e.stackStr || '', ctx)
9796

9897
// E.g. AssertionError from assert does not set showDiff but has both actual and expected properties
99-
if (e.showDiff || (e.showDiff === undefined && e.actual && e.expected)) {
100-
displayDiff(stringify(e.actual), stringify(e.expected), ctx.logger.console, {
101-
outputTruncateLength: ctx.config.outputTruncateLength,
102-
outputDiffLines: ctx.config.outputDiffLines,
103-
outputDiffMaxLines: ctx.config.outputDiffMaxLines,
104-
colorDim: c.dim,
105-
colorError: c.red,
106-
colorSuccess: c.green,
107-
})
108-
}
98+
if (e.diff)
99+
displayDiff(e.diff, ctx.logger.console)
109100
}
110101

111102
function printErrorType(type: string, ctx: Vitest) {
@@ -178,14 +169,8 @@ function handleImportOutsideModuleError(stack: string, ctx: Vitest) {
178169
}\n`)))
179170
}
180171

181-
export function displayDiff(actual: string, expected: string, console: Console, options: Omit<DiffOptions, 'showLegend'> = {}) {
182-
const diff = unifiedDiff(actual, expected, options)
183-
const dim = options.colorDim || ((str: string) => str)
184-
const black = options.colorDim ? c.black : (str: string) => str
185-
if (diff)
186-
console.error(diff + '\n')
187-
else if (actual && expected && actual !== '"undefined"' && expected !== '"undefined"')
188-
console.error(dim('Could not display diff. It\'s possible objects are too large to compare.\nTry increasing ') + black('--outputDiffMaxSize') + dim(' option.\n'))
172+
export function displayDiff(diff: string, console: Console) {
173+
console.error(diff)
189174
}
190175

191176
function printErrorMessage(error: ErrorWithDiff, logger: Logger) {

‎packages/vitest/src/types/config.ts

-21
Original file line numberDiff line numberDiff line change
@@ -206,33 +206,12 @@ export interface InlineConfig {
206206
*/
207207
reporters?: Arrayable<BuiltinReporters | 'html' | Reporter | Omit<string, BuiltinReporters>>
208208

209-
/**
210-
* Truncates lines in the output to the given length.
211-
* @default stdout.columns || 80
212-
*/
213-
outputTruncateLength?: number
214-
215209
/**
216210
* Maximum number of line to show in a single diff.
217211
* @default 15
218212
*/
219213
outputDiffLines?: number
220214

221-
/**
222-
* The maximum number of characters allowed in a single object before doing a diff.
223-
* Vitest tries to stringify an object before doing a diff, but if the object is too large,
224-
* it will reduce the depth of the object to fit within this limit.
225-
* Because of this if object is too big or nested, you might not see the diff.
226-
* @default 10000
227-
*/
228-
outputDiffMaxSize?: number
229-
230-
/**
231-
* Maximum number of lines in a diff overall.
232-
* @default 50
233-
*/
234-
outputDiffMaxLines?: number
235-
236215
/**
237216
* Write test results to a file when the --reporter=json` or `--reporter=junit` option is also specified.
238217
* Also definable individually per reporter by using an object instead.

‎pnpm-lock.yaml

+68-25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎test/core/test/__snapshots__/mocked.test.ts.snap

+17-17
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ exports[`mocked function which fails on toReturnWith > just one call 1`] = `
66
Received:
77
1st spy call return:
88
9-
1
10-
2
9+
- 1
10+
+ 2
1111
1212
1313
Number of calls: 1
@@ -20,18 +20,18 @@ exports[`mocked function which fails on toReturnWith > multi calls 1`] = `
2020
Received:
2121
1st spy call return:
2222
23-
1
24-
2
23+
- 1
24+
+ 2
2525
2626
2nd spy call return:
2727
28-
1
29-
2
28+
- 1
29+
+ 2
3030
3131
3rd spy call return:
3232
33-
1
34-
2
33+
- 1
34+
+ 2
3535
3636
3737
Number of calls: 3
@@ -44,23 +44,23 @@ exports[`mocked function which fails on toReturnWith > oject type 1`] = `
4444
Received:
4545
1st spy call return:
4646
47-
Object {
48-
- \\"a\\": \\"1\\",
49-
+ \\"a\\": \\"4\\",
47+
{
48+
- a: '1',
49+
+ a: '4',
5050
}
5151
5252
2nd spy call return:
5353
54-
Object {
55-
- \\"a\\": \\"1\\",
56-
+ \\"a\\": \\"4\\",
54+
{
55+
- a: '1',
56+
+ a: '4',
5757
}
5858
5959
3rd spy call return:
6060
61-
Object {
62-
- \\"a\\": \\"1\\",
63-
+ \\"a\\": \\"4\\",
61+
{
62+
- a: '1',
63+
+ a: '4',
6464
}
6565
6666

‎test/core/test/diff.test.ts

+31-58
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,54 @@
11
import { expect, test, vi } from 'vitest'
2-
import { stringify } from '@vitest/utils'
2+
import { getDefaultColors, setupColors } from '@vitest/utils'
33
import { displayDiff } from 'vitest/src/node/error'
4+
import { unifiedDiff } from '@vitest/utils/diff'
45

5-
test('displays an error for large objects', () => {
6-
const objectA = new Array(1000).fill(0).map((_, i) => ({ i, long: 'a'.repeat(i) }))
7-
const objectB = new Array(1000).fill(0).map((_, i) => ({ i, long: 'b'.repeat(i) }))
8-
const console = { log: vi.fn(), error: vi.fn() }
9-
displayDiff(stringify(objectA), stringify(objectB), console as any)
10-
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(`
11-
"Could not display diff. It's possible objects are too large to compare.
12-
Try increasing --outputDiffMaxSize option.
13-
"
14-
`)
15-
})
16-
17-
test('displays an error for large objects', () => {
18-
const console = { log: vi.fn(), error: vi.fn() }
19-
displayDiff(stringify('undefined'), stringify('undefined'), console as any)
20-
expect(console.error).not.toHaveBeenCalled()
21-
})
22-
23-
test('displays diff', () => {
6+
test('displays object diff', () => {
247
const objectA = { a: 1, b: 2 }
258
const objectB = { a: 1, b: 3 }
269
const console = { log: vi.fn(), error: vi.fn() }
27-
displayDiff(stringify(objectA), stringify(objectB), console as any)
10+
setupColors(getDefaultColors())
11+
displayDiff(unifiedDiff(objectA, objectB), console as any)
2812
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(`
2913
" - Expected - 1
3014
+ Received + 1
3115
32-
Object {
33-
\\"a\\": 1,
34-
- \\"b\\": 3,
35-
+ \\"b\\": 2,
36-
}
37-
"
16+
... 1 more line
17+
a: 1,
18+
- b: 3,
19+
+ b: 2,
20+
}"
3821
`)
3922
})
4023

41-
test('displays long diff', () => {
42-
const objectA = { a: 1, b: 2, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10, k: 11, l: 12, m: 13, n: 14, o: 15, p: 16, q: 17, r: 18, s: 19, t: 20, u: 21, v: 22, w: 23, x: 24, y: 25, z: 26 }
43-
const objectB = { a: 1, b: 3, k: 11, l: 12, m: 13, n: 14, p: 16, o: 17, r: 18, s: 23, t: 88, u: 21, v: 44, w: 23, x: 24, y: 25, z: 26 }
24+
test('display one line string diff', () => {
25+
const string1 = 'string1'
26+
const string2 = 'string2'
4427
const console = { log: vi.fn(), error: vi.fn() }
45-
displayDiff(stringify(objectA), stringify(objectB), console as any, { outputDiffMaxLines: 5 })
28+
setupColors(getDefaultColors())
29+
displayDiff(unifiedDiff(string1, string2), console as any)
4630
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(`
47-
" - Expected - 5
48-
+ Received + 13
31+
" Difference:
4932
50-
Object {
51-
\\"a\\": 1,
52-
- \\"b\\": 3,
53-
+ \\"b\\": 2,
54-
+ \\"d\\": 4,
55-
... 26 more lines
56-
"
33+
- 'string2'
34+
+ 'string1'"
5735
`)
5836
})
5937

60-
test('displays truncated diff', () => {
61-
const stringA = `Lorem ipsum dolor sit amet, consectetur adipiscing elit.
62-
Suspendisse viverra sapien ac venenatis lacinia.
63-
Morbi consectetur arcu nec lorem lacinia tempus.`
64-
const objectB = `Quisque hendrerit metus id dapibus pulvinar.
65-
Quisque pellentesque enim a elit faucibus cursus.
66-
Sed in tellus aliquet mauris interdum semper a in lacus.`
38+
test('display multiline line string diff', () => {
39+
const string1 = 'string1\nstring2\nstring3'
40+
const string2 = 'string2\nstring2\nstring1'
6741
const console = { log: vi.fn(), error: vi.fn() }
68-
displayDiff((stringA), (objectB), console as any, { outputTruncateLength: 14 })
42+
setupColors(getDefaultColors())
43+
displayDiff(unifiedDiff(string1, string2), console as any)
6944
expect(console.error.mock.calls[0][0]).toMatchInlineSnapshot(`
70-
" - Expected - 3
71-
+ Received + 3
72-
73-
- Quisque h…
74-
- Quisque p…
75-
- Sed in te…
76-
+ Lorem ips…
77-
+ Suspendis…
78-
+ Morbi con…
79-
"
45+
" - Expected - 2
46+
+ Received + 2
47+
48+
+ string1
49+
\`string2
50+
- string2
51+
- string1\`
52+
+ string3\`"
8053
`)
8154
})

‎test/core/test/jest-matcher-utils.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { getDefaultColors, setupColors } from '@vitest/utils'
12
import { describe, expect, it } from 'vitest'
23

34
describe('jest-matcher-utils', () => {
@@ -11,9 +12,11 @@ describe('jest-matcher-utils', () => {
1112
})
1213

1314
it('diff', () => {
15+
setupColors(getDefaultColors())
16+
1417
expect(() => {
1518
// @ts-expect-error "toBeJestEqual" is a custom matcher we just created
1619
expect('a').toBeJestEqual('b')
17-
}).toThrowError(/Expected.*"b".*Received.*"a"/ms)
20+
}).toThrowError(/Difference:.*\- 'b'.*\+ 'a'/ms)
1821
})
1922
})

‎test/core/test/mocked.test.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { assert, describe, expect, test, vi, vitest } from 'vitest'
1+
import { afterEach, assert, beforeEach, describe, expect, test, vi, vitest } from 'vitest'
22
// @ts-expect-error not typed module
33
import { value as virtualValue } from 'virtual-module'
4+
import { createColors, getDefaultColors, setupColors } from '@vitest/utils'
45
import { two } from '../src/submodule'
56
import * as mocked from '../src/mockedA'
67
import { mockedB } from '../src/mockedB'
@@ -135,6 +136,13 @@ test('async functions should be mocked', () => {
135136
})
136137

137138
describe('mocked function which fails on toReturnWith', () => {
139+
beforeEach(() => {
140+
setupColors(getDefaultColors())
141+
})
142+
afterEach(() => {
143+
setupColors(createColors(true))
144+
})
145+
138146
test('zero call', () => {
139147
const mock = vi.fn(() => 1)
140148
expect(() => expect(mock).toReturnWith(2)).toThrowErrorMatchingSnapshot()

‎test/core/test/setup.ts

+1-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
import { getDefaultColors, setupColors } from '@vitest/utils'
2-
import { beforeEach, vi } from 'vitest'
1+
import { vi } from 'vitest'
32

43
vi.mock('../src/global-mock', () => ({ mocked: true }))
5-
6-
beforeEach(() => {
7-
setupColors(getDefaultColors())
8-
})

‎test/reporters/tests/__snapshots__/html.test.ts.snap

+8
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ exports[`html reporter > resolves to "failing" status for test file "json-fail"
4141
"error": {
4242
"actual": "2",
4343
"constructor": "Function<AssertionError>",
44+
"diff": " Difference:
45+
46+
- 1
47+
+ 2",
4448
"expected": "1",
4549
"message": "expected 2 to deeply equal 1",
4650
"name": "AssertionError",
@@ -56,6 +60,10 @@ exports[`html reporter > resolves to "failing" status for test file "json-fail"
5660
{
5761
"actual": "2",
5862
"constructor": "Function<AssertionError>",
63+
"diff": " Difference:
64+
65+
- 1
66+
+ 2",
5967
"expected": "1",
6068
"message": "expected 2 to deeply equal 1",
6169
"name": "AssertionError",

‎test/watch/fixtures/vitest.config.ts

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ process.stdin.setRawMode = () => process.stdin
77
export default defineConfig({
88
test: {
99
watch: true,
10-
outputTruncateLength: 999,
1110

1211
// This configuration is edited by tests
1312
reporters: 'verbose',

0 commit comments

Comments
 (0)
Please sign in to comment.