[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.

feat(config): add `diff` option (#4063)

Co-authored-by: Vladimir Sheremet <sleuths.slews0s@icloud.com>

authored by

Han
Vladimir Sheremet
and committed by
GitHub
(Sep 18, 2023, 5:47 PM +0200) b50cf7ad 725a0146

+119 -10
+30
docs/config/index.md
··· 1641 1641 }) 1642 1642 ``` 1643 1643 1644 + ### diff 1645 + 1646 + - **Type:** `string` 1647 + - **CLI:** `--diff=<value>` 1648 + - **Version:** Since Vitest 0.34.5 1649 + 1650 + Path to a diff config that will be used to generate diff interface. Useful if you want to customize diff display. 1651 + 1652 + :::code-group 1653 + ```ts [vitest.diff.ts] 1654 + import type { DiffOptions } from 'vitest' 1655 + import c from 'picocolors' 1656 + 1657 + export default { 1658 + aIndicator: c.bold('--'), 1659 + bIndicator: c.bold('++'), 1660 + omitAnnotationLines: true, 1661 + } satisfies DiffOptions 1662 + ``` 1663 + 1664 + ```ts [vitest.config.js] 1665 + import { defineConfig } from 'vitest/config' 1666 + 1667 + export default defineConfig({ 1668 + test: { 1669 + diff: './vitest.diff.ts' 1670 + } 1671 + }) 1672 + ``` 1673 + :::
+4
packages/browser/src/client/main.ts
··· 1 1 import { createClient } from '@vitest/ws-client' 2 2 import type { ResolvedConfig } from 'vitest' 3 3 import type { CancelReason, VitestRunner } from '@vitest/runner' 4 + import type { VitestExecutor } from 'vitest/src/runtime/execute' 4 5 import { createBrowserRunner } from './runner' 5 6 import { importId } from './utils' 6 7 import { setupConsoleLogSpy } from './logger' ··· 101 102 const { 102 103 startTests, 103 104 setupCommonEnv, 105 + loadDiffConfig, 104 106 takeCoverageInsideWorker, 105 107 } = await importId('vitest/browser') as typeof import('vitest/browser') 106 108 ··· 122 124 config.snapshotOptions.snapshotEnvironment = new BrowserSnapshotEnvironment() 123 125 124 126 try { 127 + runner.config.diffOptions = await loadDiffConfig(config, executor as VitestExecutor) 128 + 125 129 await setupCommonEnv(config) 126 130 const files = paths.map((path) => { 127 131 return (`${config.root}/${path}`).replace(/\/+/g, '/')
+7 -6
packages/runner/src/run.ts
··· 1 1 import limit from 'p-limit' 2 2 import { getSafeTimers, shuffle } from '@vitest/utils' 3 3 import { processError } from '@vitest/utils/error' 4 + import type { DiffOptions } from '@vitest/utils/diff' 4 5 import type { VitestRunner } from './types/runner' 5 6 import type { File, HookCleanupCallback, HookListener, SequenceHooks, Suite, SuiteHooks, Task, TaskMeta, TaskResult, TaskResultPack, TaskState, Test } from './types' 6 7 import { partitionSuiteChildren } from './utils/suite' ··· 173 174 } 174 175 } 175 176 catch (e) { 176 - failTask(test.result, e) 177 + failTask(test.result, e, runner.config.diffOptions) 177 178 } 178 179 179 180 // skipped with new PendingError ··· 189 190 await callCleanupHooks(beforeEachCleanups) 190 191 } 191 192 catch (e) { 192 - failTask(test.result, e) 193 + failTask(test.result, e, runner.config.diffOptions) 193 194 } 194 195 195 196 if (test.result.state === 'pass') ··· 233 234 updateTask(test, runner) 234 235 } 235 236 236 - function failTask(result: TaskResult, err: unknown) { 237 + function failTask(result: TaskResult, err: unknown, diffOptions?: DiffOptions) { 237 238 if (err instanceof PendingError) { 238 239 result.state = 'skip' 239 240 return ··· 244 245 ? err 245 246 : [err] 246 247 for (const e of errors) { 247 - const error = processError(e) 248 + const error = processError(e, diffOptions) 248 249 result.error ??= error 249 250 result.errors ??= [] 250 251 result.errors.push(error) ··· 316 317 } 317 318 } 318 319 catch (e) { 319 - failTask(suite.result, e) 320 + failTask(suite.result, e, runner.config.diffOptions) 320 321 } 321 322 322 323 try { ··· 324 325 await callCleanupHooks(beforeAllCleanups) 325 326 } 326 327 catch (e) { 327 - failTask(suite.result, e) 328 + failTask(suite.result, e, runner.config.diffOptions) 328 329 } 329 330 330 331 if (suite.mode === 'run') {
+2
packages/runner/src/types/runner.ts
··· 1 + import type { DiffOptions } from '@vitest/utils/diff' 1 2 import type { File, SequenceHooks, SequenceSetupFiles, Suite, TaskResultPack, Test, TestContext } from './tasks' 2 3 3 4 export interface VitestRunnerConfig { ··· 21 22 testTimeout: number 22 23 hookTimeout: number 23 24 retry: number 25 + diffOptions?: DiffOptions 24 26 } 25 27 26 28 export type VitestRunnerImportSource = 'collect' | 'setup'
+3 -3
packages/utils/src/error.ts
··· 1 - import { diff } from './diff' 1 + import { type DiffOptions, diff } from './diff' 2 2 import { format } from './display' 3 3 import { deepClone, getOwnProperties, getType } from './helpers' 4 4 import { stringify } from './stringify' ··· 86 86 return message.replace(/__vite_ssr_import_\d+__\./g, '') 87 87 } 88 88 89 - export function processError(err: any) { 89 + export function processError(err: any, diffOptions?: DiffOptions) { 90 90 if (!err || typeof err !== 'object') 91 91 return { message: err } 92 92 // stack is not serialized in worker communication ··· 101 101 const clonedExpected = deepClone(err.expected, { forceWritable: true }) 102 102 103 103 const { replacedActual, replacedExpected } = replaceAsymmetricMatcher(clonedActual, clonedExpected) 104 - err.diff = diff(replacedExpected, replacedActual) 104 + err.diff = diff(replacedExpected, replacedActual, diffOptions) 105 105 } 106 106 107 107 if (typeof err.expected !== 'string')
+1 -1
packages/vitest/src/browser.ts
··· 1 1 export { startTests } from '@vitest/runner' 2 - export { setupCommonEnv } from './runtime/setup.common' 2 + export { setupCommonEnv, loadDiffConfig } from './runtime/setup.common' 3 3 export { takeCoverageInsideWorker, stopCoverageInsideWorker, getCoverageProvider, startCoverageInsideWorker } from './integrations/coverage'
+1
packages/vitest/src/node/cli.ts
··· 51 51 .option('--test-timeout <time>', 'Default timeout of a test in milliseconds (default: 5000)') 52 52 .option('--bail <number>', 'Stop test execution when given number of tests have failed', { default: 0 }) 53 53 .option('--retry <times>', 'Retry the test specific number of times if it fails', { default: 0 }) 54 + .option('--diff <path>', 'Path to a diff config that will be used to generate diff interface') 54 55 .help() 55 56 56 57 cli
+7
packages/vitest/src/node/config.ts
··· 289 289 ...resolved.setupFiles, 290 290 ] 291 291 292 + if (resolved.diff) { 293 + resolved.diff = normalize( 294 + resolveModule(resolved.diff, { paths: [resolved.root] }) 295 + ?? resolve(resolved.root, resolved.diff)) 296 + resolved.forceRerunTriggers.push(resolved.diff) 297 + } 298 + 292 299 // the server has been created, we don't need to override vite.server options 293 300 resolved.api = resolveApiServerConfig(options) 294 301
+1
packages/vitest/src/runtime/entry.ts
··· 20 20 setupChaiConfig(config.chaiConfig) 21 21 22 22 const runner = await resolveTestRunner(config, executor) 23 + 23 24 workerState.onCancel.then(reason => runner.onCancel?.(reason)) 24 25 25 26 workerState.durations.prepare = performance.now() - workerState.durations.prepare
+3
packages/vitest/src/runtime/runners/index.ts
··· 6 6 import { getWorkerState } from '../../utils/global' 7 7 import { rpc } from '../rpc' 8 8 import { takeCoverageInsideWorker } from '../../integrations/coverage' 9 + import { loadDiffConfig } from '../setup.common' 9 10 10 11 const runnersFile = resolve(distDir, 'runners.js') 11 12 ··· 36 37 37 38 if (!testRunner.importFile) 38 39 throw new Error('Runner must implement "importFile" method.') 40 + 41 + testRunner.config.diffOptions = await loadDiffConfig(config, executor) 39 42 40 43 // patch some methods, so custom runners don't need to call RPC 41 44 const originalOnTaskUpdate = testRunner.onTaskUpdate
+14
packages/vitest/src/runtime/setup.common.ts
··· 1 1 import { setSafeTimers } from '@vitest/utils' 2 2 import { resetRunOnceCounter } from '../integrations/run-once' 3 3 import type { ResolvedConfig } from '../types' 4 + import type { DiffOptions } from '../types/matcher-utils' 5 + import type { VitestExecutor } from './execute' 4 6 5 7 let globalSetup = false 6 8 export async function setupCommonEnv(config: ResolvedConfig) { ··· 21 23 for (const key in defines) 22 24 (globalThis as any)[key] = defines[key] 23 25 } 26 + 27 + export async function loadDiffConfig(config: ResolvedConfig, executor: VitestExecutor) { 28 + if (typeof config.diff !== 'string') 29 + return 30 + 31 + const diffModule = await executor.executeId(config.diff) 32 + 33 + if (diffModule && typeof diffModule.default === 'object' && diffModule.default != null) 34 + return diffModule.default as DiffOptions 35 + else 36 + throw new Error(`invalid diff config file ${config.diff}. Must have a default export with config object`) 37 + }
+5
packages/vitest/src/types/config.ts
··· 540 540 snapshotFormat?: PrettyFormatOptions 541 541 542 542 /** 543 + * Path to a module which has a default export of diff config. 544 + */ 545 + diff?: string 546 + 547 + /** 543 548 * Resolve custom snapshot path 544 549 */ 545 550 resolveSnapshotPath?: (path: string, extension: string) => string
+1
packages/vitest/src/types/index.ts
··· 13 13 export type * from './general' 14 14 export type * from './coverage' 15 15 export type * from './benchmark' 16 + export type { DiffOptions } from '@vitest/utils/diff' 16 17 export type { 17 18 EnhancedSpy, 18 19 MockedFunction,
+4
test/browser/custom-diff-config.ts
··· 1 + export default { 2 + aAnnotation: 'Expected to be', 3 + bAnnotation: 'But got', 4 + }
+2
test/browser/specs/runner.test.mjs
··· 34 34 await test('correctly prints error', () => { 35 35 assert.match(stderr, /expected 1 to be 2/, 'prints failing error') 36 36 assert.match(stderr, /- 2\s+\+ 1/, 'prints failing diff') 37 + assert.match(stderr, /Expected to be/, 'prints \`Expected to be\`') 38 + assert.match(stderr, /But got/, 'prints \`But got\`') 37 39 }) 38 40 39 41 await test('logs are redirected to stdout', async () => {
+1
test/browser/vitest.config.mts
··· 20 20 }, 21 21 open: false, 22 22 isolate: false, 23 + diff: './custom-diff-config.ts', 23 24 outputFile: './browser.json', 24 25 reporters: ['json', { 25 26 onInit: noop,
+5
test/reporters/fixtures/custom-diff-config.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + 3 + test('', () => { 4 + expect({ foo: 1 }).toMatchInlineSnapshot('xxx') 5 + })
+4
test/reporters/fixtures/custom-diff-config.ts
··· 1 + export default { 2 + aAnnotation: 'Expected to be', 3 + bAnnotation: 'But got', 4 + }
+1
test/reporters/fixtures/invalid-diff-config.ts
··· 1 + export const diffOptions = {}
+23
test/reporters/tests/custom-diff-config.spec.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { resolve } from 'pathe' 3 + import { runVitest } from '../../test-utils' 4 + 5 + test('custom diff config', async () => { 6 + const filename = resolve('./fixtures/custom-diff-config.test.ts') 7 + const diff = resolve('./fixtures/custom-diff-config.ts') 8 + const { stderr } = await runVitest({ root: './fixtures', diff }, [filename]) 9 + 10 + expect(stderr).toBeTruthy() 11 + expect(stderr).toContain('Expected to be') 12 + expect(stderr).toContain('But got') 13 + }) 14 + 15 + test('invalid diff config file', async () => { 16 + const filename = resolve('./fixtures/custom-diff-config.test.ts') 17 + const diff = resolve('./fixtures/invalid-diff-config.ts') 18 + const { stderr } = await runVitest({ root: './fixtures', diff }, [filename]) 19 + 20 + expect(stderr).toBeTruthy() 21 + expect(stderr).toContain('invalid diff config file') 22 + expect(stderr).toContain('Must have a default export with config object') 23 + })