Skip to content

Commit fc53f56

Browse files
authored
feat: allow configuring expect options in the config (#5729)
1 parent ddb09eb commit fc53f56

20 files changed

+177
-9
lines changed

docs/config/index.md

+35
Original file line numberDiff line numberDiff line change
@@ -2287,3 +2287,38 @@ If you just need to configure snapshots feature, use [`snapshotFormat`](#snapsho
22872287
- **Type:** `Partial<NodeJS.ProcessEnv>`
22882288

22892289
Environment variables available on `process.env` and `import.meta.env` during tests. These variables will not be available in the main process (in `globalSetup`, for example).
2290+
2291+
### expect
2292+
2293+
- **Type:** `ExpectOptions`
2294+
2295+
#### expect.requireAssertions
2296+
2297+
- **Type:** `boolean`
2298+
- **Default:** `false`
2299+
2300+
The same as calling [`expect.hasAssertions()`](/api/expect#expect-hasassertions) at the start of every test. This makes sure that no test will pass accidentally.
2301+
2302+
::: tip
2303+
This only works with Vitest's `expect`. If you use `assert` ot `.should` assertions, they will not count, and your test will fail due to the lack of expect assertions.
2304+
2305+
You can change the value of this by calling `vi.setConfig({ expect: { requireAssertions: false } })`. The config will be applied to every subsequent `expect` call until the `vi.resetConfig` is called manually.
2306+
:::
2307+
2308+
#### expect.poll
2309+
2310+
Global configuration options for [`expect.poll`](/api/expect#poll). These are the same options you can pass down to `expect.poll(condition, options)`.
2311+
2312+
##### expect.poll.interval
2313+
2314+
- **Type:** `number`
2315+
- **Default:** `50`
2316+
2317+
Polling interval in milliseconds
2318+
2319+
##### expect.poll.timeout
2320+
2321+
- **Type:** `number`
2322+
- **Default:** `1000`
2323+
2324+
Polling timeout in milliseconds

docs/guide/cli-table.md

+3
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@
114114
| `--slowTestThreshold <threshold>` | Threshold in milliseconds for a test to be considered slow (default: `300`) |
115115
| `--teardownTimeout <timeout>` | Default timeout of a teardown function in milliseconds (default: `10000`) |
116116
| `--maxConcurrency <number>` | Maximum number of concurrent tests in a suite (default: `5`) |
117+
| `--expect.requireAssertions` | Require that all tests have at least one assertion |
118+
| `--expect.poll.interval <interval>` | Poll interval in milliseconds for `expect.poll()` assertions (default: `50`) |
119+
| `--expect.poll.timeout <timeout>` | Poll timeout in milliseconds for `expect.poll()` assertions (default: `1000`) |
117120
| `--run` | Disable watch mode |
118121
| `--no-color` | Removes colors from the console output |
119122
| `--clearScreen` | Clear terminal screen when re-running tests during watch mode (default: `true`) |

packages/vitest/src/integrations/chai/poll.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as chai from 'chai'
22
import type { ExpectStatic } from '@vitest/expect'
33
import { getSafeTimers } from '@vitest/utils'
4+
import { getWorkerState } from '../../utils'
45

56
// these matchers are not supported because they don't make sense with poll
67
const unsupported = [
@@ -26,7 +27,13 @@ const unsupported = [
2627

2728
export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] {
2829
return function poll(fn, options = {}) {
29-
const { interval = 50, timeout = 1000, message } = options
30+
const state = getWorkerState()
31+
const defaults = state.config.expect?.poll ?? {}
32+
const {
33+
interval = defaults.interval ?? 50,
34+
timeout = defaults.timeout ?? 1000,
35+
message,
36+
} = options
3037
// @ts-expect-error private poll access
3138
const assertion = expect(null, message).withContext({ poll: true }) as Assertion
3239
const proxy: any = new Proxy(assertion, {

packages/vitest/src/node/cli/cli-config.ts

+33
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,39 @@ export const cliOptionsConfig: VitestCLIOptions = {
585585
description: 'Maximum number of concurrent tests in a suite (default: `5`)',
586586
argument: '<number>',
587587
},
588+
expect: {
589+
description: 'Configuration options for `expect()` matches',
590+
argument: '', // no displayed
591+
subcommands: {
592+
requireAssertions: {
593+
description: 'Require that all tests have at least one assertion',
594+
},
595+
poll: {
596+
description: 'Default options for `expect.poll()`',
597+
argument: '',
598+
subcommands: {
599+
interval: {
600+
description: 'Poll interval in milliseconds for `expect.poll()` assertions (default: `50`)',
601+
argument: '<interval>',
602+
},
603+
timeout: {
604+
description: 'Poll timeout in milliseconds for `expect.poll()` assertions (default: `1000`)',
605+
argument: '<timeout>',
606+
},
607+
},
608+
transform(value) {
609+
if (typeof value !== 'object')
610+
throw new Error(`Unexpected value for --expect.poll: ${value}. If you need to configure timeout, use --expect.poll.timeout=<timeout>`)
611+
return value
612+
},
613+
},
614+
},
615+
transform(value) {
616+
if (typeof value !== 'object')
617+
throw new Error(`Unexpected value for --expect: ${value}. If you need to configure expect options, use --expect.{name}=<value> syntax`)
618+
return value
619+
},
620+
},
588621

589622
// CLI only options
590623
run: {

packages/vitest/src/node/config.ts

+2
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ export function resolveConfig(
179179
throw new Error(`You cannot set "coverage.reportsDirectory" as ${reportsDirectory}. Vitest needs to be able to remove this directory before test run`)
180180
}
181181

182+
resolved.expect ??= {}
183+
182184
resolved.deps ??= {}
183185
resolved.deps.moduleDirectories ??= []
184186
resolved.deps.moduleDirectories = resolved.deps.moduleDirectories.map((dir) => {

packages/vitest/src/runtime/runners/test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ export class VitestTestRunner implements VitestRunner {
1515
private __vitest_executor!: VitestExecutor
1616
private cancelRun = false
1717

18+
private assertionsErrors = new WeakMap<Readonly<Task>, Error>()
19+
1820
constructor(public config: ResolvedConfig) {}
1921

2022
importFile(filepath: string, source: VitestRunnerImportSource): unknown {
@@ -123,9 +125,14 @@ export class VitestTestRunner implements VitestRunner {
123125
throw expectedAssertionsNumberErrorGen!()
124126
if (isExpectingAssertions === true && assertionCalls === 0)
125127
throw isExpectingAssertionsError
128+
if (this.config.expect.requireAssertions && assertionCalls === 0)
129+
throw this.assertionsErrors.get(test)
126130
}
127131

128132
extendTaskContext<T extends Test | Custom>(context: TaskContext<T>): ExtendedContext<T> {
133+
// create error during the test initialization so we have a nice stack trace
134+
if (this.config.expect.requireAssertions)
135+
this.assertionsErrors.set(context.task, new Error('expected any number of assertion, but got none'))
129136
let _expect: ExpectStatic | undefined
130137
Object.defineProperty(context, 'expect', {
131138
get() {

packages/vitest/src/types/config.ts

+26
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,31 @@ export interface InlineConfig {
706706
waitForDebugger?: boolean
707707
}
708708

709+
/**
710+
* Configuration options for expect() matches.
711+
*/
712+
expect?: {
713+
/**
714+
* Throw an error if tests don't have any expect() assertions.
715+
*/
716+
requireAssertions?: boolean
717+
/**
718+
* Default options for expect.poll()
719+
*/
720+
poll?: {
721+
/**
722+
* Timeout in milliseconds
723+
* @default 1000
724+
*/
725+
timeout?: number
726+
/**
727+
* Polling interval in milliseconds
728+
* @default 50
729+
*/
730+
interval?: number
731+
}
732+
}
733+
709734
/**
710735
* Modify default Chai config. Vitest uses Chai for `expect` and `assert` matches.
711736
* https://github.com/chaijs/chai/blob/4.x.x/lib/chai/config.js
@@ -974,6 +999,7 @@ export type RuntimeConfig = Pick<
974999
| 'restoreMocks'
9751000
| 'fakeTimers'
9761001
| 'maxConcurrency'
1002+
| 'expect'
9771003
> & {
9781004
sequence?: {
9791005
concurrent?: boolean

test/cli/fixtures/fails/concurrent-suite-deadlock.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createDefer } from '@vitest/utils'
2-
import { describe, test, vi } from 'vitest'
2+
import { describe, test, vi, expect } from 'vitest'
33

44
// 3 tests depend on each other,
55
// so they will deadlock when maxConcurrency < 3
@@ -21,11 +21,13 @@ describe('wrapper', { concurrent: true, timeout: 500 }, () => {
2121

2222
describe('1st suite', () => {
2323
test('a', async () => {
24+
expect(1).toBe(1)
2425
defers[0].resolve()
2526
await defers[2]
2627
})
2728

2829
test('b', async () => {
30+
expect(1).toBe(1)
2931
await defers[0]
3032
defers[1].resolve()
3133
await defers[2]
@@ -34,6 +36,7 @@ describe('wrapper', { concurrent: true, timeout: 500 }, () => {
3436

3537
describe('2nd suite', () => {
3638
test('c', async () => {
39+
expect(1).toBe(1)
3740
await defers[1]
3841
defers[2].resolve()
3942
})

test/cli/fixtures/fails/concurrent-test-deadlock.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, test, vi } from 'vitest'
1+
import { describe, expect, test, vi } from 'vitest'
22
import { createDefer } from '@vitest/utils'
33

44
// 3 tests depend on each other,
@@ -20,17 +20,20 @@ describe('wrapper', { concurrent: true, timeout: 500 }, () => {
2020
]
2121

2222
test('a', async () => {
23+
expect(1).toBe(1)
2324
defers[0].resolve()
2425
await defers[2]
2526
})
2627

2728
test('b', async () => {
29+
expect(1).toBe(1)
2830
await defers[0]
2931
defers[1].resolve()
3032
await defers[2]
3133
})
3234

3335
test('c', async () => {
36+
expect(1).toBe(1)
3437
await defers[1]
3538
defers[2].resolve()
3639
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { it } from 'vitest'
2+
3+
it('test without assertions')

test/cli/fixtures/fails/test-extend/fixture-error.test.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ describe('error thrown in beforeEach fixtures', () => {
1010
// eslint-disable-next-line unused-imports/no-unused-vars
1111
beforeEach<{ a: never }>(({ a }) => {})
1212

13-
myTest('error is handled', () => {})
13+
myTest('error is handled', () => {
14+
expect(1).toBe(1)
15+
})
1416
})
1517

1618
describe('error thrown in afterEach fixtures', () => {
@@ -24,6 +26,7 @@ describe('error thrown in afterEach fixtures', () => {
2426
afterEach<{ a: never }>(({ a }) => {})
2527

2628
myTest('fixture errors', () => {
29+
expect(1).toBe(1)
2730
expectTypeOf(1).toEqualTypeOf<number>()
2831
})
2932
})

test/cli/fixtures/fails/test-timeout.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ test('hi', async () => {
44
await new Promise(resolve => setTimeout(resolve, 1000))
55
}, 10)
66

7-
suite('suite timeout', () => {
7+
suite('suite timeout', {
8+
timeout: 100,
9+
}, () => {
810
test('hi', async () => {
911
await new Promise(resolve => setTimeout(resolve, 500))
1012
})
11-
}, {
12-
timeout: 100,
1313
})
1414

1515
suite('suite timeout simple input', () => {

test/cli/fixtures/fails/unhandled.test.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
// @vitest-environment jsdom
22

3-
import { test } from 'vitest'
3+
import { expect, test } from 'vitest'
44

55
test('unhandled exception', () => {
6+
expect(1).toBe(1)
67
addEventListener('custom', () => {
78
throw new Error('some error')
89
})

test/cli/fixtures/fails/vite.config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,8 @@ export default defineConfig({
88
isolate: false,
99
},
1010
},
11+
expect: {
12+
requireAssertions: true,
13+
}
1114
},
1215
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test } from 'vitest'
2+
3+
test('assertion is not called', () => {
4+
// no expect
5+
})

test/cli/fixtures/stacktraces/vite.config.ts

+3
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,8 @@ export default defineConfig({
4545
pool: 'forks',
4646
include: ['**/*.{test,spec}.{imba,?(c|m)[jt]s?(x)}'],
4747
setupFiles: ['./setup.js'],
48+
expect: {
49+
requireAssertions: true,
50+
},
4851
},
4952
})

test/cli/test/__snapshots__/fails.test.ts.snap

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ exports[`should fail mock-import-proxy-module.test.ts > mock-import-proxy-module
3535
3636
exports[`should fail nested-suite.test.ts > nested-suite.test.ts 1`] = `"AssertionError: expected true to be false // Object.is equality"`;
3737
38+
exports[`should fail no-assertions.test.ts > no-assertions.test.ts 1`] = `"Error: expected any number of assertion, but got none"`;
39+
3840
exports[`should fail primitive-error.test.ts > primitive-error.test.ts 1`] = `"Unknown Error: 42"`;
3941
4042
exports[`should fail snapshot-with-not.test.ts > snapshot-with-not.test.ts 1`] = `

test/cli/test/__snapshots__/stacktraces.test.ts.snap

+11
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,17 @@ exports[`stacktraces should respect sourcemaps > mocked-imported.test.ts > mocke
158158
"
159159
`;
160160
161+
exports[`stacktraces should respect sourcemaps > require-assertions.test.js > require-assertions.test.js 1`] = `
162+
" ❯ require-assertions.test.js:3:1
163+
1| import { test } from 'vitest'
164+
2|
165+
3| test('assertion is not called', () => {
166+
| ^
167+
4| // no expect
168+
5| })
169+
"
170+
`;
171+
161172
exports[`stacktraces should respect sourcemaps > reset-modules.test.ts > reset-modules.test.ts 1`] = `
162173
" ❯ reset-modules.test.ts:16:26
163174
14| expect(2 + 1).eq(3)

test/core/test/cli-test.test.ts

+18
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,24 @@ test('merge-reports', () => {
316316
expect(getCLIOptions('--merge-reports different-folder')).toEqual({ mergeReports: 'different-folder' })
317317
})
318318

319+
test('configure expect', () => {
320+
expect(() => getCLIOptions('vitest --expect.poll=1000')).toThrowErrorMatchingInlineSnapshot(`[Error: Unexpected value for --expect.poll: true. If you need to configure timeout, use --expect.poll.timeout=<timeout>]`)
321+
expect(() => getCLIOptions('vitest --expect=1000')).toThrowErrorMatchingInlineSnapshot(`[Error: Unexpected value for --expect: true. If you need to configure expect options, use --expect.{name}=<value> syntax]`)
322+
expect(getCLIOptions('vitest --expect.poll.interval=100 --expect.poll.timeout=300')).toEqual({
323+
expect: {
324+
poll: {
325+
interval: 100,
326+
timeout: 300,
327+
},
328+
},
329+
})
330+
expect(getCLIOptions('vitest --expect.requireAssertions')).toEqual({
331+
expect: {
332+
requireAssertions: true,
333+
},
334+
})
335+
})
336+
319337
test('public parseCLI works correctly', () => {
320338
expect(parseCLI('vitest dev')).toEqual({
321339
filter: [],

test/core/test/web-worker-node.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ it('doesn\'t trigger events, if closed', async () => {
263263
worker.port.close()
264264
await new Promise((resolve) => {
265265
worker.port.addEventListener('message', () => {
266-
expect.fail('should not trigger message')
266+
expect.unreachable('should not trigger message')
267267
})
268268
worker.port.postMessage('event')
269269
setTimeout(resolve, 100)

0 commit comments

Comments
 (0)