Skip to content

Commit

Permalink
feat(config): add diff option (#4063)
Browse files Browse the repository at this point in the history
Co-authored-by: Vladimir Sheremet <sleuths.slews0s@icloud.com>
  • Loading branch information
fenghan34 and sheremet-va committed Sep 18, 2023
1 parent 725a014 commit b50cf7a
Show file tree
Hide file tree
Showing 20 changed files with 119 additions and 10 deletions.
30 changes: 30 additions & 0 deletions docs/config/index.md
Expand Up @@ -1641,3 +1641,33 @@ export default defineConfig({
})
```

### diff

- **Type:** `string`
- **CLI:** `--diff=<value>`
- **Version:** Since Vitest 0.34.5

Path to a diff config that will be used to generate diff interface. Useful if you want to customize diff display.

:::code-group
```ts [vitest.diff.ts]
import type { DiffOptions } from 'vitest'
import c from 'picocolors'

export default {
aIndicator: c.bold('--'),
bIndicator: c.bold('++'),
omitAnnotationLines: true,
} satisfies DiffOptions
```

```ts [vitest.config.js]
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
diff: './vitest.diff.ts'
}
})
```
:::
4 changes: 4 additions & 0 deletions packages/browser/src/client/main.ts
@@ -1,6 +1,7 @@
import { createClient } from '@vitest/ws-client'
import type { ResolvedConfig } from 'vitest'
import type { CancelReason, VitestRunner } from '@vitest/runner'
import type { VitestExecutor } from 'vitest/src/runtime/execute'
import { createBrowserRunner } from './runner'
import { importId } from './utils'
import { setupConsoleLogSpy } from './logger'
Expand Down Expand Up @@ -101,6 +102,7 @@ async function runTests(paths: string[], config: ResolvedConfig) {
const {
startTests,
setupCommonEnv,
loadDiffConfig,
takeCoverageInsideWorker,
} = await importId('vitest/browser') as typeof import('vitest/browser')

Expand All @@ -122,6 +124,8 @@ async function runTests(paths: string[], config: ResolvedConfig) {
config.snapshotOptions.snapshotEnvironment = new BrowserSnapshotEnvironment()

try {
runner.config.diffOptions = await loadDiffConfig(config, executor as VitestExecutor)

await setupCommonEnv(config)
const files = paths.map((path) => {
return (`${config.root}/${path}`).replace(/\/+/g, '/')
Expand Down
13 changes: 7 additions & 6 deletions packages/runner/src/run.ts
@@ -1,6 +1,7 @@
import limit from 'p-limit'
import { getSafeTimers, shuffle } from '@vitest/utils'
import { processError } from '@vitest/utils/error'
import type { DiffOptions } from '@vitest/utils/diff'
import type { VitestRunner } from './types/runner'
import type { File, HookCleanupCallback, HookListener, SequenceHooks, Suite, SuiteHooks, Task, TaskMeta, TaskResult, TaskResultPack, TaskState, Test } from './types'
import { partitionSuiteChildren } from './utils/suite'
Expand Down Expand Up @@ -173,7 +174,7 @@ export async function runTest(test: Test, runner: VitestRunner) {
}
}
catch (e) {
failTask(test.result, e)
failTask(test.result, e, runner.config.diffOptions)
}

// skipped with new PendingError
Expand All @@ -189,7 +190,7 @@ export async function runTest(test: Test, runner: VitestRunner) {
await callCleanupHooks(beforeEachCleanups)
}
catch (e) {
failTask(test.result, e)
failTask(test.result, e, runner.config.diffOptions)
}

if (test.result.state === 'pass')
Expand Down Expand Up @@ -233,7 +234,7 @@ export async function runTest(test: Test, runner: VitestRunner) {
updateTask(test, runner)
}

function failTask(result: TaskResult, err: unknown) {
function failTask(result: TaskResult, err: unknown, diffOptions?: DiffOptions) {
if (err instanceof PendingError) {
result.state = 'skip'
return
Expand All @@ -244,7 +245,7 @@ function failTask(result: TaskResult, err: unknown) {
? err
: [err]
for (const e of errors) {
const error = processError(e)
const error = processError(e, diffOptions)
result.error ??= error
result.errors ??= []
result.errors.push(error)
Expand Down Expand Up @@ -316,15 +317,15 @@ export async function runSuite(suite: Suite, runner: VitestRunner) {
}
}
catch (e) {
failTask(suite.result, e)
failTask(suite.result, e, runner.config.diffOptions)
}

try {
await callSuiteHook(suite, suite, 'afterAll', runner, [suite])
await callCleanupHooks(beforeAllCleanups)
}
catch (e) {
failTask(suite.result, e)
failTask(suite.result, e, runner.config.diffOptions)
}

if (suite.mode === 'run') {
Expand Down
2 changes: 2 additions & 0 deletions packages/runner/src/types/runner.ts
@@ -1,3 +1,4 @@
import type { DiffOptions } from '@vitest/utils/diff'
import type { File, SequenceHooks, SequenceSetupFiles, Suite, TaskResultPack, Test, TestContext } from './tasks'

export interface VitestRunnerConfig {
Expand All @@ -21,6 +22,7 @@ export interface VitestRunnerConfig {
testTimeout: number
hookTimeout: number
retry: number
diffOptions?: DiffOptions
}

export type VitestRunnerImportSource = 'collect' | 'setup'
Expand Down
6 changes: 3 additions & 3 deletions packages/utils/src/error.ts
@@ -1,4 +1,4 @@
import { diff } from './diff'
import { type DiffOptions, diff } from './diff'
import { format } from './display'
import { deepClone, getOwnProperties, getType } from './helpers'
import { stringify } from './stringify'
Expand Down Expand Up @@ -86,7 +86,7 @@ function normalizeErrorMessage(message: string) {
return message.replace(/__vite_ssr_import_\d+__\./g, '')
}

export function processError(err: any) {
export function processError(err: any, diffOptions?: DiffOptions) {
if (!err || typeof err !== 'object')
return { message: err }
// stack is not serialized in worker communication
Expand All @@ -101,7 +101,7 @@ export function processError(err: any) {
const clonedExpected = deepClone(err.expected, { forceWritable: true })

const { replacedActual, replacedExpected } = replaceAsymmetricMatcher(clonedActual, clonedExpected)
err.diff = diff(replacedExpected, replacedActual)
err.diff = diff(replacedExpected, replacedActual, diffOptions)
}

if (typeof err.expected !== 'string')
Expand Down
2 changes: 1 addition & 1 deletion packages/vitest/src/browser.ts
@@ -1,3 +1,3 @@
export { startTests } from '@vitest/runner'
export { setupCommonEnv } from './runtime/setup.common'
export { setupCommonEnv, loadDiffConfig } from './runtime/setup.common'
export { takeCoverageInsideWorker, stopCoverageInsideWorker, getCoverageProvider, startCoverageInsideWorker } from './integrations/coverage'
1 change: 1 addition & 0 deletions packages/vitest/src/node/cli.ts
Expand Up @@ -51,6 +51,7 @@ cli
.option('--test-timeout <time>', 'Default timeout of a test in milliseconds (default: 5000)')
.option('--bail <number>', 'Stop test execution when given number of tests have failed', { default: 0 })
.option('--retry <times>', 'Retry the test specific number of times if it fails', { default: 0 })
.option('--diff <path>', 'Path to a diff config that will be used to generate diff interface')
.help()

cli
Expand Down
7 changes: 7 additions & 0 deletions packages/vitest/src/node/config.ts
Expand Up @@ -289,6 +289,13 @@ export function resolveConfig(
...resolved.setupFiles,
]

if (resolved.diff) {
resolved.diff = normalize(
resolveModule(resolved.diff, { paths: [resolved.root] })
?? resolve(resolved.root, resolved.diff))
resolved.forceRerunTriggers.push(resolved.diff)
}

// the server has been created, we don't need to override vite.server options
resolved.api = resolveApiServerConfig(options)

Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/runtime/entry.ts
Expand Up @@ -20,6 +20,7 @@ export async function run(files: string[], config: ResolvedConfig, environment:
setupChaiConfig(config.chaiConfig)

const runner = await resolveTestRunner(config, executor)

workerState.onCancel.then(reason => runner.onCancel?.(reason))

workerState.durations.prepare = performance.now() - workerState.durations.prepare
Expand Down
3 changes: 3 additions & 0 deletions packages/vitest/src/runtime/runners/index.ts
Expand Up @@ -6,6 +6,7 @@ import { distDir } from '../../paths'
import { getWorkerState } from '../../utils/global'
import { rpc } from '../rpc'
import { takeCoverageInsideWorker } from '../../integrations/coverage'
import { loadDiffConfig } from '../setup.common'

const runnersFile = resolve(distDir, 'runners.js')

Expand Down Expand Up @@ -37,6 +38,8 @@ export async function resolveTestRunner(config: ResolvedConfig, executor: Vitest
if (!testRunner.importFile)
throw new Error('Runner must implement "importFile" method.')

testRunner.config.diffOptions = await loadDiffConfig(config, executor)

// patch some methods, so custom runners don't need to call RPC
const originalOnTaskUpdate = testRunner.onTaskUpdate
testRunner.onTaskUpdate = async (task) => {
Expand Down
14 changes: 14 additions & 0 deletions packages/vitest/src/runtime/setup.common.ts
@@ -1,6 +1,8 @@
import { setSafeTimers } from '@vitest/utils'
import { resetRunOnceCounter } from '../integrations/run-once'
import type { ResolvedConfig } from '../types'
import type { DiffOptions } from '../types/matcher-utils'
import type { VitestExecutor } from './execute'

let globalSetup = false
export async function setupCommonEnv(config: ResolvedConfig) {
Expand All @@ -21,3 +23,15 @@ function setupDefines(defines: Record<string, any>) {
for (const key in defines)
(globalThis as any)[key] = defines[key]
}

export async function loadDiffConfig(config: ResolvedConfig, executor: VitestExecutor) {
if (typeof config.diff !== 'string')
return

const diffModule = await executor.executeId(config.diff)

if (diffModule && typeof diffModule.default === 'object' && diffModule.default != null)
return diffModule.default as DiffOptions
else
throw new Error(`invalid diff config file ${config.diff}. Must have a default export with config object`)
}
5 changes: 5 additions & 0 deletions packages/vitest/src/types/config.ts
Expand Up @@ -539,6 +539,11 @@ export interface InlineConfig {
*/
snapshotFormat?: PrettyFormatOptions

/**
* Path to a module which has a default export of diff config.
*/
diff?: string

/**
* Resolve custom snapshot path
*/
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/types/index.ts
Expand Up @@ -13,6 +13,7 @@ export type * from './worker'
export type * from './general'
export type * from './coverage'
export type * from './benchmark'
export type { DiffOptions } from '@vitest/utils/diff'
export type {
EnhancedSpy,
MockedFunction,
Expand Down
4 changes: 4 additions & 0 deletions test/browser/custom-diff-config.ts
@@ -0,0 +1,4 @@
export default {
aAnnotation: 'Expected to be',
bAnnotation: 'But got',
}
2 changes: 2 additions & 0 deletions test/browser/specs/runner.test.mjs
Expand Up @@ -34,6 +34,8 @@ await test('tests are actually running', async () => {
await test('correctly prints error', () => {
assert.match(stderr, /expected 1 to be 2/, 'prints failing error')
assert.match(stderr, /- 2\s+\+ 1/, 'prints failing diff')
assert.match(stderr, /Expected to be/, 'prints \`Expected to be\`')
assert.match(stderr, /But got/, 'prints \`But got\`')
})

await test('logs are redirected to stdout', async () => {
Expand Down
1 change: 1 addition & 0 deletions test/browser/vitest.config.mts
Expand Up @@ -20,6 +20,7 @@ export default defineConfig({
},
open: false,
isolate: false,
diff: './custom-diff-config.ts',
outputFile: './browser.json',
reporters: ['json', {
onInit: noop,
Expand Down
5 changes: 5 additions & 0 deletions test/reporters/fixtures/custom-diff-config.test.ts
@@ -0,0 +1,5 @@
import { expect, test } from 'vitest'

test('', () => {
expect({ foo: 1 }).toMatchInlineSnapshot('xxx')
})
4 changes: 4 additions & 0 deletions test/reporters/fixtures/custom-diff-config.ts
@@ -0,0 +1,4 @@
export default {
aAnnotation: 'Expected to be',
bAnnotation: 'But got',
}
1 change: 1 addition & 0 deletions test/reporters/fixtures/invalid-diff-config.ts
@@ -0,0 +1 @@
export const diffOptions = {}
23 changes: 23 additions & 0 deletions test/reporters/tests/custom-diff-config.spec.ts
@@ -0,0 +1,23 @@
import { expect, test } from 'vitest'
import { resolve } from 'pathe'
import { runVitest } from '../../test-utils'

test('custom diff config', async () => {
const filename = resolve('./fixtures/custom-diff-config.test.ts')
const diff = resolve('./fixtures/custom-diff-config.ts')
const { stderr } = await runVitest({ root: './fixtures', diff }, [filename])

expect(stderr).toBeTruthy()
expect(stderr).toContain('Expected to be')
expect(stderr).toContain('But got')
})

test('invalid diff config file', async () => {
const filename = resolve('./fixtures/custom-diff-config.test.ts')
const diff = resolve('./fixtures/invalid-diff-config.ts')
const { stderr } = await runVitest({ root: './fixtures', diff }, [filename])

expect(stderr).toBeTruthy()
expect(stderr).toContain('invalid diff config file')
expect(stderr).toContain('Must have a default export with config object')
})

0 comments on commit b50cf7a

Please sign in to comment.