Skip to content

Commit

Permalink
feat: allow configuring expect options in the config (#5729)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va authored May 31, 2024
1 parent ddb09eb commit fc53f56
Show file tree
Hide file tree
Showing 20 changed files with 177 additions and 9 deletions.
35 changes: 35 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2287,3 +2287,38 @@ If you just need to configure snapshots feature, use [`snapshotFormat`](#snapsho
- **Type:** `Partial<NodeJS.ProcessEnv>`

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

### expect

- **Type:** `ExpectOptions`

#### expect.requireAssertions

- **Type:** `boolean`
- **Default:** `false`

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.

::: tip
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.

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

#### expect.poll

Global configuration options for [`expect.poll`](/api/expect#poll). These are the same options you can pass down to `expect.poll(condition, options)`.

##### expect.poll.interval

- **Type:** `number`
- **Default:** `50`

Polling interval in milliseconds

##### expect.poll.timeout

- **Type:** `number`
- **Default:** `1000`

Polling timeout in milliseconds
3 changes: 3 additions & 0 deletions docs/guide/cli-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@
| `--slowTestThreshold <threshold>` | Threshold in milliseconds for a test to be considered slow (default: `300`) |
| `--teardownTimeout <timeout>` | Default timeout of a teardown function in milliseconds (default: `10000`) |
| `--maxConcurrency <number>` | Maximum number of concurrent tests in a suite (default: `5`) |
| `--expect.requireAssertions` | Require that all tests have at least one assertion |
| `--expect.poll.interval <interval>` | Poll interval in milliseconds for `expect.poll()` assertions (default: `50`) |
| `--expect.poll.timeout <timeout>` | Poll timeout in milliseconds for `expect.poll()` assertions (default: `1000`) |
| `--run` | Disable watch mode |
| `--no-color` | Removes colors from the console output |
| `--clearScreen` | Clear terminal screen when re-running tests during watch mode (default: `true`) |
Expand Down
9 changes: 8 additions & 1 deletion packages/vitest/src/integrations/chai/poll.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as chai from 'chai'
import type { ExpectStatic } from '@vitest/expect'
import { getSafeTimers } from '@vitest/utils'
import { getWorkerState } from '../../utils'

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

export function createExpectPoll(expect: ExpectStatic): ExpectStatic['poll'] {
return function poll(fn, options = {}) {
const { interval = 50, timeout = 1000, message } = options
const state = getWorkerState()
const defaults = state.config.expect?.poll ?? {}
const {
interval = defaults.interval ?? 50,
timeout = defaults.timeout ?? 1000,
message,
} = options
// @ts-expect-error private poll access
const assertion = expect(null, message).withContext({ poll: true }) as Assertion
const proxy: any = new Proxy(assertion, {
Expand Down
33 changes: 33 additions & 0 deletions packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -585,6 +585,39 @@ export const cliOptionsConfig: VitestCLIOptions = {
description: 'Maximum number of concurrent tests in a suite (default: `5`)',
argument: '<number>',
},
expect: {
description: 'Configuration options for `expect()` matches',
argument: '', // no displayed
subcommands: {
requireAssertions: {
description: 'Require that all tests have at least one assertion',
},
poll: {
description: 'Default options for `expect.poll()`',
argument: '',
subcommands: {
interval: {
description: 'Poll interval in milliseconds for `expect.poll()` assertions (default: `50`)',
argument: '<interval>',
},
timeout: {
description: 'Poll timeout in milliseconds for `expect.poll()` assertions (default: `1000`)',
argument: '<timeout>',
},
},
transform(value) {
if (typeof value !== 'object')
throw new Error(`Unexpected value for --expect.poll: ${value}. If you need to configure timeout, use --expect.poll.timeout=<timeout>`)
return value
},
},
},
transform(value) {
if (typeof value !== 'object')
throw new Error(`Unexpected value for --expect: ${value}. If you need to configure expect options, use --expect.{name}=<value> syntax`)
return value
},
},

// CLI only options
run: {
Expand Down
2 changes: 2 additions & 0 deletions packages/vitest/src/node/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ export function resolveConfig(
throw new Error(`You cannot set "coverage.reportsDirectory" as ${reportsDirectory}. Vitest needs to be able to remove this directory before test run`)
}

resolved.expect ??= {}

resolved.deps ??= {}
resolved.deps.moduleDirectories ??= []
resolved.deps.moduleDirectories = resolved.deps.moduleDirectories.map((dir) => {
Expand Down
7 changes: 7 additions & 0 deletions packages/vitest/src/runtime/runners/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export class VitestTestRunner implements VitestRunner {
private __vitest_executor!: VitestExecutor
private cancelRun = false

private assertionsErrors = new WeakMap<Readonly<Task>, Error>()

constructor(public config: ResolvedConfig) {}

importFile(filepath: string, source: VitestRunnerImportSource): unknown {
Expand Down Expand Up @@ -123,9 +125,14 @@ export class VitestTestRunner implements VitestRunner {
throw expectedAssertionsNumberErrorGen!()
if (isExpectingAssertions === true && assertionCalls === 0)
throw isExpectingAssertionsError
if (this.config.expect.requireAssertions && assertionCalls === 0)
throw this.assertionsErrors.get(test)
}

extendTaskContext<T extends Test | Custom>(context: TaskContext<T>): ExtendedContext<T> {
// create error during the test initialization so we have a nice stack trace
if (this.config.expect.requireAssertions)
this.assertionsErrors.set(context.task, new Error('expected any number of assertion, but got none'))
let _expect: ExpectStatic | undefined
Object.defineProperty(context, 'expect', {
get() {
Expand Down
26 changes: 26 additions & 0 deletions packages/vitest/src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,31 @@ export interface InlineConfig {
waitForDebugger?: boolean
}

/**
* Configuration options for expect() matches.
*/
expect?: {
/**
* Throw an error if tests don't have any expect() assertions.
*/
requireAssertions?: boolean
/**
* Default options for expect.poll()
*/
poll?: {
/**
* Timeout in milliseconds
* @default 1000
*/
timeout?: number
/**
* Polling interval in milliseconds
* @default 50
*/
interval?: number
}
}

/**
* Modify default Chai config. Vitest uses Chai for `expect` and `assert` matches.
* https://github.com/chaijs/chai/blob/4.x.x/lib/chai/config.js
Expand Down Expand Up @@ -974,6 +999,7 @@ export type RuntimeConfig = Pick<
| 'restoreMocks'
| 'fakeTimers'
| 'maxConcurrency'
| 'expect'
> & {
sequence?: {
concurrent?: boolean
Expand Down
5 changes: 4 additions & 1 deletion test/cli/fixtures/fails/concurrent-suite-deadlock.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createDefer } from '@vitest/utils'
import { describe, test, vi } from 'vitest'
import { describe, test, vi, expect } from 'vitest'

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

describe('1st suite', () => {
test('a', async () => {
expect(1).toBe(1)
defers[0].resolve()
await defers[2]
})

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

describe('2nd suite', () => {
test('c', async () => {
expect(1).toBe(1)
await defers[1]
defers[2].resolve()
})
Expand Down
5 changes: 4 additions & 1 deletion test/cli/fixtures/fails/concurrent-test-deadlock.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { describe, test, vi } from 'vitest'
import { describe, expect, test, vi } from 'vitest'
import { createDefer } from '@vitest/utils'

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

test('a', async () => {
expect(1).toBe(1)
defers[0].resolve()
await defers[2]
})

test('b', async () => {
expect(1).toBe(1)
await defers[0]
defers[1].resolve()
await defers[2]
})

test('c', async () => {
expect(1).toBe(1)
await defers[1]
defers[2].resolve()
})
Expand Down
3 changes: 3 additions & 0 deletions test/cli/fixtures/fails/no-assertions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { it } from 'vitest'

it('test without assertions')
5 changes: 4 additions & 1 deletion test/cli/fixtures/fails/test-extend/fixture-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ describe('error thrown in beforeEach fixtures', () => {
// eslint-disable-next-line unused-imports/no-unused-vars
beforeEach<{ a: never }>(({ a }) => {})

myTest('error is handled', () => {})
myTest('error is handled', () => {
expect(1).toBe(1)
})
})

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

myTest('fixture errors', () => {
expect(1).toBe(1)
expectTypeOf(1).toEqualTypeOf<number>()
})
})
Expand Down
6 changes: 3 additions & 3 deletions test/cli/fixtures/fails/test-timeout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ test('hi', async () => {
await new Promise(resolve => setTimeout(resolve, 1000))
}, 10)

suite('suite timeout', () => {
suite('suite timeout', {
timeout: 100,
}, () => {
test('hi', async () => {
await new Promise(resolve => setTimeout(resolve, 500))
})
}, {
timeout: 100,
})

suite('suite timeout simple input', () => {
Expand Down
3 changes: 2 additions & 1 deletion test/cli/fixtures/fails/unhandled.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// @vitest-environment jsdom

import { test } from 'vitest'
import { expect, test } from 'vitest'

test('unhandled exception', () => {
expect(1).toBe(1)
addEventListener('custom', () => {
throw new Error('some error')
})
Expand Down
3 changes: 3 additions & 0 deletions test/cli/fixtures/fails/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,8 @@ export default defineConfig({
isolate: false,
},
},
expect: {
requireAssertions: true,
}
},
})
5 changes: 5 additions & 0 deletions test/cli/fixtures/stacktraces/require-assertions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { test } from 'vitest'

test('assertion is not called', () => {
// no expect
})
3 changes: 3 additions & 0 deletions test/cli/fixtures/stacktraces/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,8 @@ export default defineConfig({
pool: 'forks',
include: ['**/*.{test,spec}.{imba,?(c|m)[jt]s?(x)}'],
setupFiles: ['./setup.js'],
expect: {
requireAssertions: true,
},
},
})
2 changes: 2 additions & 0 deletions test/cli/test/__snapshots__/fails.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ exports[`should fail mock-import-proxy-module.test.ts > mock-import-proxy-module
exports[`should fail nested-suite.test.ts > nested-suite.test.ts 1`] = `"AssertionError: expected true to be false // Object.is equality"`;
exports[`should fail no-assertions.test.ts > no-assertions.test.ts 1`] = `"Error: expected any number of assertion, but got none"`;
exports[`should fail primitive-error.test.ts > primitive-error.test.ts 1`] = `"Unknown Error: 42"`;
exports[`should fail snapshot-with-not.test.ts > snapshot-with-not.test.ts 1`] = `
Expand Down
11 changes: 11 additions & 0 deletions test/cli/test/__snapshots__/stacktraces.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,17 @@ exports[`stacktraces should respect sourcemaps > mocked-imported.test.ts > mocke
"
`;
exports[`stacktraces should respect sourcemaps > require-assertions.test.js > require-assertions.test.js 1`] = `
" ❯ require-assertions.test.js:3:1
1| import { test } from 'vitest'
2|
3| test('assertion is not called', () => {
| ^
4| // no expect
5| })
"
`;
exports[`stacktraces should respect sourcemaps > reset-modules.test.ts > reset-modules.test.ts 1`] = `
" ❯ reset-modules.test.ts:16:26
14| expect(2 + 1).eq(3)
Expand Down
18 changes: 18 additions & 0 deletions test/core/test/cli-test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,24 @@ test('merge-reports', () => {
expect(getCLIOptions('--merge-reports different-folder')).toEqual({ mergeReports: 'different-folder' })
})

test('configure expect', () => {
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>]`)
expect(() => getCLIOptions('vitest --expect=1000')).toThrowErrorMatchingInlineSnapshot(`[Error: Unexpected value for --expect: true. If you need to configure expect options, use --expect.{name}=<value> syntax]`)
expect(getCLIOptions('vitest --expect.poll.interval=100 --expect.poll.timeout=300')).toEqual({
expect: {
poll: {
interval: 100,
timeout: 300,
},
},
})
expect(getCLIOptions('vitest --expect.requireAssertions')).toEqual({
expect: {
requireAssertions: true,
},
})
})

test('public parseCLI works correctly', () => {
expect(parseCLI('vitest dev')).toEqual({
filter: [],
Expand Down
2 changes: 1 addition & 1 deletion test/core/test/web-worker-node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ it('doesn\'t trigger events, if closed', async () => {
worker.port.close()
await new Promise((resolve) => {
worker.port.addEventListener('message', () => {
expect.fail('should not trigger message')
expect.unreachable('should not trigger message')
})
worker.port.postMessage('event')
setTimeout(resolve, 100)
Expand Down

0 comments on commit fc53f56

Please sign in to comment.