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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const diffOptions = {}
23 changes: 23 additions & 0 deletions test/reporters/tests/custom-diff-config.spec.ts
Original file line number Diff line number Diff line change
@@ -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')
})