diff --git a/docs/guide/extending-matchers.md b/docs/guide/extending-matchers.md index 0394e0c9c..9bc38b2e9 100644 --- a/docs/guide/extending-matchers.md +++ b/docs/guide/extending-matchers.md @@ -112,6 +112,14 @@ This contains a set of utility functions that you can use to display messages. Full name of the current test (including describe block). +### `task` + +Contains a reference to [the `Test` runner task](/api/advanced/runner#tasks) when available. + +::: warning +When using the global `expect` with concurrent tests, `this.task` is `undefined`. Use `context.expect` instead to ensure `task` is available in custom matchers. +::: + ### `testPath` Path to the current test. diff --git a/packages/expect/src/jest-extend.ts b/packages/expect/src/jest-extend.ts index ec474b10b..31dc54b1a 100644 --- a/packages/expect/src/jest-extend.ts +++ b/packages/expect/src/jest-extend.ts @@ -1,3 +1,4 @@ +import type { Test } from '@vitest/runner' import type { ChaiPlugin, ExpectStatic, @@ -14,9 +15,7 @@ import { getMatcherUtils, stringify, } from './jest-matcher-utils' - import { equals, iterableEquality, subsetEquality } from './jest-utils' - import { getState } from './state' import { wrapAssertion } from './utils' @@ -35,9 +34,15 @@ function getMatcherState( iterableEquality, subsetEquality, } + let task: Test | undefined = util.flag(assertion, 'vitest-test') + + if (task?.type !== 'test') { + task = undefined + } const matcherState: MatcherState = { ...getState(expect), + task, customTesters: getCustomEqualityTesters(), isNot, utils: jestUtils, diff --git a/packages/expect/src/types.ts b/packages/expect/src/types.ts index ae0954015..89ddc1d04 100644 --- a/packages/expect/src/types.ts +++ b/packages/expect/src/types.ts @@ -6,6 +6,7 @@ * */ +import type { Test } from '@vitest/runner' import type { MockInstance } from '@vitest/spy' import type { Constructable } from '@vitest/utils' import type { Formatter } from 'tinyrainbow' @@ -73,6 +74,7 @@ export interface MatcherState { } soft?: boolean poll?: boolean + task?: Readonly } export interface SyncExpectationResult { diff --git a/packages/vitest/src/integrations/chai/index.ts b/packages/vitest/src/integrations/chai/index.ts index 99cd0b5ec..1e46e9e4b 100644 --- a/packages/vitest/src/integrations/chai/index.ts +++ b/packages/vitest/src/integrations/chai/index.ts @@ -15,7 +15,7 @@ import { getWorkerState } from '../../runtime/utils' import { createExpectPoll } from './poll' import './setup' -export function createExpect(test?: TaskPopulated): ExpectStatic { +export function createExpect(test?: Test | TaskPopulated): ExpectStatic { const expect = ((value: any, message?: string): Assertion => { const { assertionCalls } = getState(expect) setState({ assertionCalls: assertionCalls + 1 }, expect) diff --git a/test/cli/test/expect-task.test.ts b/test/cli/test/expect-task.test.ts new file mode 100644 index 000000000..93993c637 --- /dev/null +++ b/test/cli/test/expect-task.test.ts @@ -0,0 +1,288 @@ +import { test } from 'vitest' +import { runInlineTests } from '../../test-utils' + +const toMatchTest = /* ts */` +export function toMatchTest(this, expected) { + if (this.task?.name !== expected) { + return { pass: false, message: () => 'Active: "' + this.task?.name + '"\\nExpected: "' + expected + '"' } + } + + return { pass: true, message: () => undefined } +} + +export function delay() { + return new Promise(resolve => { + setTimeout(resolve, 100) + }) +} +` + +const globals = /* ts */` + import { test, describe } from 'vitest' + import { delay, toMatchTest } from './to-match-test.ts' + + expect.extend({ toMatchTest }) + + describe('tests', { /* options */ }, async () => { + test('first', async () => { + await delay() + + expect('first').toMatchTest() + }) + + test('second', () => { + expect('second').toMatchTest() + }) + }) +` + +const globalImport = /* ts */` + import { test, describe, expect } from 'vitest' + import { delay, toMatchTest } from './to-match-test.ts' + + expect.extend({ toMatchTest }) + + describe('tests', { /* options */ }, async () => { + test('first', async () => { + await delay() + + expect('first').toMatchTest() + }) + + test('second', () => { + expect('second').toMatchTest() + }) + }) +` + +const fromContextGlobalExtend = /* ts */` + import { test, describe, expect } from 'vitest' + import { delay, toMatchTest } from './to-match-test.ts' + + expect.extend({ toMatchTest }) + + describe('tests', { /* options */ }, async () => { + test('first', async ({ expect }) => { + await delay() + + expect('first').toMatchTest() + }) + + test('second', ({ expect }) => { + expect('second').toMatchTest() + }) + }) +` + +const fromContextLocalExtend = /* ts */` + import { test, describe } from 'vitest' + import { delay, toMatchTest } from './to-match-test.ts' + + describe('tests', { /* options */ }, async () => { + test('first', async ({ expect }) => { + expect.extend({ toMatchTest }) + + await delay() + + expect('first').toMatchTest() + }) + + test('second', ({ expect }) => { + expect.extend({ toMatchTest }) + + expect('second').toMatchTest() + }) + }) +` + +const testBoundGlobalExtend = /* ts */` + import { test, describe, expect, createExpect } from 'vitest' + import { delay, toMatchTest } from './to-match-test.ts' + + expect.extend({ toMatchTest }) + + describe('tests', { /* options */ }, async () => { + test('first', async ({ task }) => { + const expect = createExpect(task) + + await delay() + + expect('first').toMatchTest() + }) + + test('second', ({ task }) => { + const expect = createExpect(task) + + expect('second').toMatchTest() + }) + }) +` + +const testBoundLocalExtend = /* ts */` + import { test, describe, createExpect } from 'vitest' + import { delay, toMatchTest } from './to-match-test.ts' + + describe('tests', { /* options */ }, async () => { + test('first', async ({ task }) => { + const expect = createExpect(task) + expect.extend({ toMatchTest }) + + await delay() + + expect('first').toMatchTest() + }) + + test('second', ({ task }) => { + const expect = createExpect(task) + expect.extend({ toMatchTest }) + + expect('second').toMatchTest() + }) + }) +` + +function withConcurrency(test: string): string { + return test.replace('/* options */', 'concurrent: true') +} + +describe('serial', { concurrent: true }, () => { + test.for([ + { + name: 'globals', + test: globals, + options: { globals: true }, + }, + { + name: 'global import', + test: globalImport, + }, + { + name: 'context destructuring & global extend', + test: fromContextGlobalExtend, + }, + { + name: 'context destructuring & local extend', + test: fromContextLocalExtend, + }, + { + name: 'test-bound extend & global extend', + test: testBoundGlobalExtend, + }, + { + name: 'test-bound extend & local extend', + test: testBoundLocalExtend, + }, + ] as const)('works with $name', async ({ options, test }, { expect }) => { + const { stdout } = await runInlineTests( + { + 'basic.test.ts': test, + 'to-match-test.ts': toMatchTest, + }, + { reporters: ['tap'], ...options }, + ) + + expect(stdout.replace(/[\d.]+ms/g, '