Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config): add diff option #4063

Merged
merged 16 commits into from Sep 18, 2023
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.
sheremet-va marked this conversation as resolved.
Show resolved Hide resolved

:::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')
})