Skip to content

Commit afd1f3e

Browse files
authored
feat(expect): provide task in MatchState (#9022)
1 parent e7ad907 commit afd1f3e

File tree

5 files changed

+306
-3
lines changed

5 files changed

+306
-3
lines changed

docs/guide/extending-matchers.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ This contains a set of utility functions that you can use to display messages.
112112

113113
Full name of the current test (including describe block).
114114

115+
### `task`
116+
117+
Contains a reference to [the `Test` runner task](/api/advanced/runner#tasks) when available.
118+
119+
::: warning
120+
When using the global `expect` with concurrent tests, `this.task` is `undefined`. Use `context.expect` instead to ensure `task` is available in custom matchers.
121+
:::
122+
115123
### `testPath`
116124

117125
Path to the current test.

packages/expect/src/jest-extend.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Test } from '@vitest/runner'
12
import type {
23
ChaiPlugin,
34
ExpectStatic,
@@ -14,9 +15,7 @@ import {
1415
getMatcherUtils,
1516
stringify,
1617
} from './jest-matcher-utils'
17-
1818
import { equals, iterableEquality, subsetEquality } from './jest-utils'
19-
2019
import { getState } from './state'
2120
import { wrapAssertion } from './utils'
2221

@@ -35,9 +34,15 @@ function getMatcherState(
3534
iterableEquality,
3635
subsetEquality,
3736
}
37+
let task: Test | undefined = util.flag(assertion, 'vitest-test')
38+
39+
if (task?.type !== 'test') {
40+
task = undefined
41+
}
3842

3943
const matcherState: MatcherState = {
4044
...getState(expect),
45+
task,
4146
customTesters: getCustomEqualityTesters(),
4247
isNot,
4348
utils: jestUtils,

packages/expect/src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*
77
*/
88

9+
import type { Test } from '@vitest/runner'
910
import type { MockInstance } from '@vitest/spy'
1011
import type { Constructable } from '@vitest/utils'
1112
import type { Formatter } from 'tinyrainbow'
@@ -73,6 +74,7 @@ export interface MatcherState {
7374
}
7475
soft?: boolean
7576
poll?: boolean
77+
task?: Readonly<Test>
7678
}
7779

7880
export interface SyncExpectationResult {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { getWorkerState } from '../../runtime/utils'
1515
import { createExpectPoll } from './poll'
1616
import './setup'
1717

18-
export function createExpect(test?: TaskPopulated): ExpectStatic {
18+
export function createExpect(test?: Test | TaskPopulated): ExpectStatic {
1919
const expect = ((value: any, message?: string): Assertion => {
2020
const { assertionCalls } = getState(expect)
2121
setState({ assertionCalls: assertionCalls + 1 }, expect)

test/cli/test/expect-task.test.ts

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
import { test } from 'vitest'
2+
import { runInlineTests } from '../../test-utils'
3+
4+
const toMatchTest = /* ts */`
5+
export function toMatchTest(this, expected) {
6+
if (this.task?.name !== expected) {
7+
return { pass: false, message: () => 'Active: "' + this.task?.name + '"\\nExpected: "' + expected + '"' }
8+
}
9+
10+
return { pass: true, message: () => undefined }
11+
}
12+
13+
export function delay() {
14+
return new Promise(resolve => {
15+
setTimeout(resolve, 100)
16+
})
17+
}
18+
`
19+
20+
const globals = /* ts */`
21+
import { test, describe } from 'vitest'
22+
import { delay, toMatchTest } from './to-match-test.ts'
23+
24+
expect.extend({ toMatchTest })
25+
26+
describe('tests', { /* options */ }, async () => {
27+
test('first', async () => {
28+
await delay()
29+
30+
expect('first').toMatchTest()
31+
})
32+
33+
test('second', () => {
34+
expect('second').toMatchTest()
35+
})
36+
})
37+
`
38+
39+
const globalImport = /* ts */`
40+
import { test, describe, expect } from 'vitest'
41+
import { delay, toMatchTest } from './to-match-test.ts'
42+
43+
expect.extend({ toMatchTest })
44+
45+
describe('tests', { /* options */ }, async () => {
46+
test('first', async () => {
47+
await delay()
48+
49+
expect('first').toMatchTest()
50+
})
51+
52+
test('second', () => {
53+
expect('second').toMatchTest()
54+
})
55+
})
56+
`
57+
58+
const fromContextGlobalExtend = /* ts */`
59+
import { test, describe, expect } from 'vitest'
60+
import { delay, toMatchTest } from './to-match-test.ts'
61+
62+
expect.extend({ toMatchTest })
63+
64+
describe('tests', { /* options */ }, async () => {
65+
test('first', async ({ expect }) => {
66+
await delay()
67+
68+
expect('first').toMatchTest()
69+
})
70+
71+
test('second', ({ expect }) => {
72+
expect('second').toMatchTest()
73+
})
74+
})
75+
`
76+
77+
const fromContextLocalExtend = /* ts */`
78+
import { test, describe } from 'vitest'
79+
import { delay, toMatchTest } from './to-match-test.ts'
80+
81+
describe('tests', { /* options */ }, async () => {
82+
test('first', async ({ expect }) => {
83+
expect.extend({ toMatchTest })
84+
85+
await delay()
86+
87+
expect('first').toMatchTest()
88+
})
89+
90+
test('second', ({ expect }) => {
91+
expect.extend({ toMatchTest })
92+
93+
expect('second').toMatchTest()
94+
})
95+
})
96+
`
97+
98+
const testBoundGlobalExtend = /* ts */`
99+
import { test, describe, expect, createExpect } from 'vitest'
100+
import { delay, toMatchTest } from './to-match-test.ts'
101+
102+
expect.extend({ toMatchTest })
103+
104+
describe('tests', { /* options */ }, async () => {
105+
test('first', async ({ task }) => {
106+
const expect = createExpect(task)
107+
108+
await delay()
109+
110+
expect('first').toMatchTest()
111+
})
112+
113+
test('second', ({ task }) => {
114+
const expect = createExpect(task)
115+
116+
expect('second').toMatchTest()
117+
})
118+
})
119+
`
120+
121+
const testBoundLocalExtend = /* ts */`
122+
import { test, describe, createExpect } from 'vitest'
123+
import { delay, toMatchTest } from './to-match-test.ts'
124+
125+
describe('tests', { /* options */ }, async () => {
126+
test('first', async ({ task }) => {
127+
const expect = createExpect(task)
128+
expect.extend({ toMatchTest })
129+
130+
await delay()
131+
132+
expect('first').toMatchTest()
133+
})
134+
135+
test('second', ({ task }) => {
136+
const expect = createExpect(task)
137+
expect.extend({ toMatchTest })
138+
139+
expect('second').toMatchTest()
140+
})
141+
})
142+
`
143+
144+
function withConcurrency(test: string): string {
145+
return test.replace('/* options */', 'concurrent: true')
146+
}
147+
148+
describe('serial', { concurrent: true }, () => {
149+
test.for([
150+
{
151+
name: 'globals',
152+
test: globals,
153+
options: { globals: true },
154+
},
155+
{
156+
name: 'global import',
157+
test: globalImport,
158+
},
159+
{
160+
name: 'context destructuring & global extend',
161+
test: fromContextGlobalExtend,
162+
},
163+
{
164+
name: 'context destructuring & local extend',
165+
test: fromContextLocalExtend,
166+
},
167+
{
168+
name: 'test-bound extend & global extend',
169+
test: testBoundGlobalExtend,
170+
},
171+
{
172+
name: 'test-bound extend & local extend',
173+
test: testBoundLocalExtend,
174+
},
175+
] as const)('works with $name', async ({ options, test }, { expect }) => {
176+
const { stdout } = await runInlineTests(
177+
{
178+
'basic.test.ts': test,
179+
'to-match-test.ts': toMatchTest,
180+
},
181+
{ reporters: ['tap'], ...options },
182+
)
183+
184+
expect(stdout.replace(/[\d.]+ms/g, '<time>')).toMatchInlineSnapshot(`
185+
"TAP version 13
186+
1..1
187+
ok 1 - basic.test.ts # time=<time> {
188+
1..1
189+
ok 1 - tests # time=<time> {
190+
1..2
191+
ok 1 - first # time=<time>
192+
ok 2 - second # time=<time>
193+
}
194+
}
195+
"
196+
`)
197+
})
198+
})
199+
200+
describe('concurrent', { concurrent: true }, () => {
201+
// when using globals or global `expect`, context is "lost" or not tracked in concurrent mode
202+
test.for([
203+
{
204+
name: 'globals',
205+
test: withConcurrency(globals),
206+
options: { globals: true },
207+
},
208+
{
209+
name: 'global import',
210+
test: withConcurrency(globalImport),
211+
},
212+
] as const)('fails with $name', async ({ options, test }, { expect }) => {
213+
const { stdout, ctx } = await runInlineTests(
214+
{
215+
'basic.test.ts': test,
216+
'to-match-test.ts': toMatchTest,
217+
},
218+
{ reporters: ['tap'], ...options },
219+
)
220+
221+
expect(
222+
stdout
223+
.replace(/[\d.]+m?s/g, '<time>')
224+
.replace(ctx!.config.root, '<root>')
225+
.replace(/:\d+:\d+/, ':<line>:<column>'),
226+
).toMatchInlineSnapshot(`
227+
"TAP version 13
228+
1..1
229+
not ok 1 - basic.test.ts # time=<time> {
230+
1..1
231+
not ok 1 - tests # time=<time> {
232+
1..2
233+
not ok 1 - first # time=<time>
234+
---
235+
error:
236+
name: "Error"
237+
message: "Active: \\"undefined\\"
238+
Expected: \\"first\\""
239+
at: "<root>/basic.test.ts:<line>:<column>"
240+
...
241+
ok 2 - second # time=<time>
242+
}
243+
}
244+
"
245+
`)
246+
})
247+
248+
test.for([
249+
{
250+
name: 'context destructuring & global extend',
251+
test: withConcurrency(fromContextGlobalExtend),
252+
},
253+
{
254+
name: 'context destructuring & local extend',
255+
test: withConcurrency(fromContextLocalExtend),
256+
},
257+
{
258+
name: 'test-bound extend & global extend',
259+
test: withConcurrency(testBoundGlobalExtend),
260+
},
261+
{
262+
name: 'test-bound extend & local extend',
263+
test: withConcurrency(testBoundLocalExtend),
264+
},
265+
])('works with $name', async ({ test }, { expect }) => {
266+
const { stdout } = await runInlineTests(
267+
{
268+
'basic.test.ts': test,
269+
'to-match-test.ts': toMatchTest,
270+
},
271+
{ reporters: ['tap'] },
272+
)
273+
274+
expect(stdout.replace(/[\d.]+m?s/g, '<time>')).toMatchInlineSnapshot(`
275+
"TAP version 13
276+
1..1
277+
ok 1 - basic.test.ts # time=<time> {
278+
1..1
279+
ok 1 - tests # time=<time> {
280+
1..2
281+
ok 1 - first # time=<time>
282+
ok 2 - second # time=<time>
283+
}
284+
}
285+
"
286+
`)
287+
})
288+
})

0 commit comments

Comments
 (0)