···3535- **Core directory test**: `CI=true pnpm test <test-file>` (for `test/core`)
3636- **Browser tests**: `CI=true pnpm test:browser:playwright` or `CI=true pnpm test:browser:webdriverio`
37373838+When writing tests, AVOID using `toContain` for validation. Prefer using `toMatchSnapshot` to include the test error and its stack.
3939+4040+If you need to typecheck tests, run `pnpm typecheck` from the root of the workspace.
4141+3842### Testing Utilities
3943- **`runInlineTests`** from `test/test-utils/index.ts` - You must use this for complex file system setups (>1 file)
4044- **`runVitest`** from `test/test-utils/index.ts` - You can use this to run Vitest programmatically
+38-29
docs/api/test.md
···261261262262- **Alias:** `it.extend`
263263264264-Use `test.extend` to extend the test context with custom fixtures. This will return a new `test` and it's also extendable, so you can compose more fixtures or override existing ones by extending it as you need. See [Extend Test Context](/guide/test-context.html#test-extend) for more information.
264264+Use `test.extend` to extend the test context with custom fixtures. This will return a new `test` and it's also extendable, so you can compose more fixtures or override existing ones by extending it as you need. See [Extend Test Context](/guide/test-context#extend-test-context) for more information.
265265266266```ts
267267import { test as baseTest, expect } from 'vitest'
268268269269-const todos = []
270270-const archive = []
269269+export const test = baseTest
270270+ // Simple value - type is inferred as { port: number; host: string }
271271+ .extend('config', { port: 3000, host: 'localhost' })
272272+ // Function fixture - type is inferred from return value
273273+ .extend('server', async ({ config }) => {
274274+ // TypeScript knows config is { port: number; host: string }
275275+ return `http://${config.host}:${config.port}`
276276+ })
271277272272-const test = baseTest.extend({
273273- todos: async ({ task }, use) => {
274274- todos.push(1, 2, 3)
275275- await use(todos)
276276- todos.length = 0
277277- },
278278- archive,
279279-})
280280-281281-test('add item', ({ todos }) => {
282282- expect(todos.length).toBe(3)
283283-284284- todos.push(4)
285285- expect(todos.length).toBe(4)
278278+test('server uses correct port', ({ config, server }) => {
279279+ // TypeScript knows the types:
280280+ // - config is { port: number; host: string }
281281+ // - server is string
282282+ expect(server).toBe('http://localhost:3000')
283283+ expect(config.port).toBe(3000)
286284})
287285```
288286289289-## test.scoped <Version>3.1.0</Version> {#test-scoped}
287287+## test.override <Version>4.1.0</Version> {#test-override}
290288291291-- **Alias:** `it.scoped`
292292-293293-Use `test.scoped` to override fixture values for all tests within the current suite and its nested suites. This must be called at the top level of a `describe` block. See [Scoping Values to Suite](/guide/test-context.html#scoping-values-to-suite) for more information.
289289+Use `test.override` to override fixture values for all tests within the current suite and its nested suites. This must be called at the top level of a `describe` block. See [Overriding Fixture Values](/guide/test-context.html#overriding-fixture-values) for more information.
294290295291```ts
296292import { test as baseTest, describe, expect } from 'vitest'
297293298298-const test = baseTest.extend({
299299- dependency: 'default',
300300- dependant: ({ dependency }, use) => use({ dependency }),
301301-})
294294+const test = baseTest
295295+ .extend('dependency', 'default')
296296+ .extend('dependant', ({ dependency }) => dependency)
302297303298describe('use scoped values', () => {
304304- test.scoped({ dependency: 'new' })
299299+ test.override({ dependency: 'new' })
305300306301 test('uses scoped value', ({ dependant }) => {
307302 // `dependant` uses the new overridden value that is scoped
···310305 })
311306})
312307```
308308+309309+## test.scoped <Version>3.1.0</Version> <Deprecated /> {#test-scoped}
310310+311311+- **Alias:** `it.scoped`
312312+313313+::: danger DEPRECATED
314314+`test.scoped` is deprecated in favor of [`test.override`](#test-override) and will be removed in a future major version.
315315+:::
316316+317317+Alias of [`test.override`](#test-override)
313318314319## test.skip
315320···661666662667Scoped `describe`. See [describe](/api/describe) for more information.
663668669669+## test.suite <Version>4.1.0</Version> {#test-suite}
670670+671671+Alias for `suite`. See [describe](/api/describe) for more information.
672672+664673## test.beforeEach
665674666675Scoped `beforeEach` hook that inherits types from [`test.extend`](#test-extend). See [beforeEach](/api/hooks#beforeeach) for more information.
···671680672681## test.beforeAll
673682674674-Scoped `beforeAll` hook. See [beforeAll](/api/hooks#beforeall) for more information.
683683+Scoped `beforeAll` hook that inherits types from [`test.extend`](#test-extend). See [beforeAll](/api/hooks#beforeall) for more information.
675684676685## test.afterAll
677686678678-Scoped `afterAll` hook. See [afterAll](/api/hooks#afterall) for more information.
687687+Scoped `afterAll` hook that inherits types from [`test.extend`](#test-extend). See [afterAll](/api/hooks#afterall) for more information.
679688680689## test.aroundEach <Version>4.1.0</Version> {#test-aroundeach}
681690···683692684693## test.aroundAll <Version>4.1.0</Version> {#test-aroundall}
685694686686-Scoped `aroundAll` hook. See [aroundAll](/api/hooks#aroundall) for more information.
695695+Scoped `aroundAll` hook that inherits types from [`test.extend`](#test-extend). See [aroundAll](/api/hooks#aroundall) for more information.
687696688697## bench <Experimental /> {#bench}
689698
+595-234
docs/guide/test-context.md
···127127128128## Extend Test Context
129129130130-Vitest provides two different ways to help you extend the test context.
130130+Vitest allows you to extend the test context with custom fixtures using `test.extend`.
131131132132-### `test.extend`
132132+The `test.extend` method lets you create a custom test API with fixtures - reusable values that are automatically set up and torn down for your tests. Vitest supports two syntaxes: the builder pattern (recommended) and the object syntax (Playwright-compatible).
133133134134-Like [Playwright](https://playwright.dev/docs/api/class-test#test-extend), you can use this method to define your own `test` API with custom fixtures and reuse it anywhere.
134134+### Builder Pattern <Version>4.1.0</Version> {#builder-pattern}
135135136136-For example, we first create the `test` collector with two fixtures: `todos` and `archive`.
136136+The builder pattern is the recommended way to define fixtures because it provides automatic type inference. TypeScript infers the type of each fixture from its return value, so you don't need to declare types manually.
137137138138```ts [my-test.ts]
139139import { test as baseTest } from 'vitest'
140140141141-const todos = []
142142-const archive = []
143143-144144-export const test = baseTest.extend({
145145- todos: async ({}, use) => {
146146- // setup the fixture before each test function
147147- todos.push(1, 2, 3)
148148-149149- // use the fixture value
150150- await use(todos)
151151-152152- // cleanup the fixture after each test function
153153- todos.length = 0
154154- },
155155- archive
156156-})
141141+export const test = baseTest
142142+ // Simple value - type is inferred as { port: number; host: string }
143143+ .extend('config', { port: 3000, host: 'localhost' })
144144+ // Function fixture - type is inferred from return value
145145+ .extend('server', async ({ config }) => {
146146+ // TypeScript knows config is { port: number; host: string }
147147+ return `http://${config.host}:${config.port}`
148148+ })
157149```
158150159159-Then we can import and use it.
151151+Then use it in your tests:
160152161153```ts [my-test.test.ts]
162154import { expect } from 'vitest'
163155import { test } from './my-test.js'
164156165165-test('add items to todos', ({ todos }) => {
166166- expect(todos.length).toBe(3)
167167-168168- todos.push(4)
169169- expect(todos.length).toBe(4)
170170-})
171171-172172-test('move items from todos to archive', ({ todos, archive }) => {
173173- expect(todos.length).toBe(3)
174174- expect(archive.length).toBe(0)
175175-176176- archive.push(todos.pop())
177177- expect(todos.length).toBe(2)
178178- expect(archive.length).toBe(1)
157157+test('server uses correct port', ({ config, server }) => {
158158+ // TypeScript knows the types:
159159+ // - config is { port: number; host: string }
160160+ // - server is string
161161+ expect(server).toBe('http://localhost:3000')
162162+ expect(config.port).toBe(3000)
179163})
180164```
181165182182-We can also add more fixtures or override existing fixtures by extending our `test`.
166166+#### Setup and Cleanup with `onCleanup`
167167+168168+For fixtures that need setup or cleanup logic, use a function. The `onCleanup` callback registers teardown logic that runs after the fixture's scope ends:
183169184170```ts
185185-import { test as todosTest } from './my-test.js'
171171+import { test as baseTest } from 'vitest'
186172187187-export const test = todosTest.extend({
188188- settings: {
189189- // ...
190190- }
173173+export const test = baseTest
174174+ .extend('tempFile', async ({}, { onCleanup }) => {
175175+ const filePath = `/tmp/test-${Date.now()}.txt`
176176+ await fs.writeFile(filePath, 'test data')
177177+178178+ // Register cleanup - runs after test completes
179179+ onCleanup(async () => {
180180+ await fs.unlink(filePath)
181181+ })
182182+183183+ return filePath
184184+ })
185185+```
186186+187187+For more complex examples:
188188+189189+```ts
190190+const test = baseTest
191191+ .extend('database', { scope: 'file' }, async ({}, { onCleanup }) => {
192192+ const db = await createDatabase()
193193+ await db.connect()
194194+195195+ onCleanup(async () => {
196196+ await db.disconnect()
197197+ })
198198+199199+ return db
200200+ })
201201+ .extend('user', async ({ database }, { onCleanup }) => {
202202+ const user = await database.createTestUser()
203203+204204+ onCleanup(async () => {
205205+ await database.deleteUser(user.id)
206206+ })
207207+208208+ return user
209209+ })
210210+```
211211+212212+::: warning
213213+The `onCleanup` function can only be called **once per fixture**. If you need multiple cleanup operations, either combine them into a single cleanup function, or split your fixture into multiple smaller fixtures:
214214+215215+```ts
216216+// ❌ This will throw an error
217217+const test = baseTest
218218+ .extend('resources', async ({}, { onCleanup }) => {
219219+ const a = await acquireA()
220220+ onCleanup(() => releaseA(a))
221221+222222+ const b = await acquireB()
223223+ onCleanup(() => releaseB(b)) // Error: onCleanup can only be called once
224224+225225+ return { a, b }
226226+ })
227227+228228+// ✅ Split into separate fixtures (recommended)
229229+const test = baseTest
230230+ .extend('resourceA', async ({}, { onCleanup }) => {
231231+ const a = await acquireA()
232232+ onCleanup(() => releaseA(a))
233233+ return a
234234+ })
235235+ .extend('resourceB', async ({}, { onCleanup }) => {
236236+ const b = await acquireB()
237237+ onCleanup(() => releaseB(b))
238238+ return b
239239+ })
240240+```
241241+242242+Splitting into separate fixtures is the recommended approach as it provides better isolation and makes dependencies explicit.
243243+:::
244244+245245+#### Fixture Options
246246+247247+The second argument to `.extend()` accepts options:
248248+249249+```ts
250250+const test = baseTest
251251+ // Automatic fixture - runs for every test even if not used
252252+ .extend('metrics', { auto: true }, ({}, { onCleanup }) => {
253253+ const metrics = new MetricsCollector()
254254+ metrics.start()
255255+ onCleanup(() => metrics.stop())
256256+ return metrics
257257+ })
258258+ // Worker-scoped fixture - initialized once per worker
259259+ .extend('config', { scope: 'worker' }, () => {
260260+ return loadConfig()
261261+ })
262262+ // File-scoped fixture - initialized once per file
263263+ .extend('database', { scope: 'file' }, async ({ config }, { onCleanup }) => {
264264+ const db = await createDatabase(config)
265265+ onCleanup(() => db.close())
266266+ return db
267267+ })
268268+ // Injected fixture - can be overridden via config
269269+ .extend('baseUrl', { injected: true }, () => {
270270+ return 'http://localhost:3000'
271271+ })
272272+```
273273+274274+For test-scoped fixtures (the default), you can omit the options:
275275+276276+```ts
277277+const test = baseTest
278278+ .extend('simple', () => 'value')
279279+```
280280+281281+Non-function values only support the `injected` option:
282282+283283+```ts
284284+const test = baseTest
285285+ .extend('baseUrl', { injected: true }, 'http://localhost:3000')
286286+ .extend('defaults', { port: 3000, host: 'localhost' })
287287+```
288288+289289+#### Accessing Other Fixtures
290290+291291+Each fixture can access previously defined fixtures via its first parameter. This works for both function and non-function fixtures:
292292+293293+```ts
294294+const test = baseTest
295295+ .extend('config', { apiUrl: 'https://api.example.com', port: 3000 })
296296+ .extend('client', ({ config }) => {
297297+ // TypeScript knows config is { apiUrl: string; port: number }
298298+ return new ApiClient(config.apiUrl)
299299+ })
300300+ .extend('user', async ({ client }) => {
301301+ // TypeScript knows client is ApiClient
302302+ return await client.getCurrentUser()
303303+ })
304304+```
305305+306306+#### Object Syntax (Playwright-Compatible)
307307+308308+Vitest also supports a Playwright-compatible object syntax. This is useful if you're migrating from Playwright or prefer defining all fixtures at once:
309309+310310+```ts [my-test.ts]
311311+import { test as baseTest } from 'vitest'
312312+313313+export const test = baseTest.extend({
314314+ page: async ({}, use) => {
315315+ // setup the fixture before each test function
316316+ const page = await browser.newPage()
317317+318318+ // use the fixture value
319319+ await use(page)
320320+321321+ // cleanup the fixture after each test function
322322+ await page.close()
323323+ },
324324+ baseUrl: 'http://localhost:3000'
191325})
192326```
193327194194-#### Fixture initialization
328328+The key difference from the builder pattern is the `use()` callback pattern for cleanup:
329329+330330+```ts
331331+// Object syntax: cleanup code goes AFTER use()
332332+const test = baseTest.extend({
333333+ database: async ({}, use) => {
334334+ const db = await createDatabase()
335335+ await db.connect()
336336+337337+ await use(db) // Test runs here
338338+339339+ // Cleanup after the test
340340+ await db.disconnect()
341341+ }
342342+})
343343+344344+// Builder pattern: cleanup is registered with onCleanup()
345345+const test = baseTest
346346+ .extend('database', async ({}, { onCleanup }) => {
347347+ const db = await createDatabase()
348348+ await db.connect()
349349+350350+ onCleanup(() => db.disconnect())
351351+352352+ return db // Test runs after this returns
353353+ })
354354+```
355355+356356+::: info
357357+With the object syntax, you need to provide types manually as a generic parameter since TypeScript cannot infer them from the `use()` callback:
358358+359359+```ts
360360+const test = baseTest.extend<{
361361+ page: Page
362362+ baseUrl: string
363363+}>({
364364+ page: async ({}, use) => {
365365+ const page = await browser.newPage()
366366+ await use(page)
367367+ await page.close()
368368+ },
369369+ baseUrl: 'http://localhost:3000'
370370+})
371371+```
372372+:::
373373+374374+#### Tuple Syntax for Options
375375+376376+With the object syntax, use a tuple to specify fixture options:
377377+378378+```ts
379379+const test = baseTest.extend({
380380+ // Auto fixture
381381+ fixture: [
382382+ async ({}, use) => {
383383+ setup()
384384+ await use()
385385+ teardown()
386386+ },
387387+ { auto: true }
388388+ ],
389389+ // Scoped fixture
390390+ database: [
391391+ async ({}, use) => {
392392+ const db = await createDatabase()
393393+ await use(db)
394394+ await db.close()
395395+ },
396396+ { scope: 'file' }
397397+ ],
398398+ // Injected fixture
399399+ url: [
400400+ '/default',
401401+ { injected: true }
402402+ ],
403403+})
404404+```
405405+406406+### Fixture Initialization
195407196408Vitest runner will smartly initialize your fixtures and inject them into the test context based on usage.
197409198410```ts
199411import { test as baseTest } from 'vitest'
200412201201-const test = baseTest.extend<{
202202- todos: number[]
203203- archive: number[]
204204-}>({
205205- todos: async ({ task }, use) => {
206206- await use([1, 2, 3])
207207- },
208208- archive: []
209209-})
413413+const test = baseTest
414414+ .extend('database', async () => {
415415+ console.log('database initializing')
416416+ return createDatabase()
417417+ })
418418+ .extend('cache', async () => {
419419+ return createCache()
420420+ })
210421211211-// todos will not run
212212-test('skip', () => {})
213213-test('skip', ({ archive }) => {})
422422+// database will not run
423423+test('no fixtures needed', () => {})
424424+test('only cache', ({ cache }) => {})
214425215215-// todos will run
216216-test('run', ({ todos }) => {})
426426+// database will run
427427+test('needs database', ({ database }) => {})
217428```
218429219430::: warning
220220-When using `test.extend()` with fixtures, you should always use the object destructuring pattern `{ todos }` to access context both in fixture function and test function.
431431+When using `test.extend()` with fixtures, you should always use the object destructuring pattern `{ database }` to access context both in fixture function and test function.
221432222433```ts
223434test('context must be destructured', (context) => { // [!code --]
224224- expect(context.todos.length).toBe(2)
435435+ expect(context.database).toBeDefined()
225436})
226437227227-test('context must be destructured', ({ todos }) => { // [!code ++]
228228- expect(todos.length).toBe(2)
438438+test('context must be destructured', ({ database }) => { // [!code ++]
439439+ expect(database).toBeDefined()
229440})
230441```
231231-232442:::
233443234234-#### Automatic fixture
444444+### Extending Extended Tests
235445236236-Vitest also supports the tuple syntax for fixtures, allowing you to pass options for each fixture. For example, you can use it to explicitly initialize a fixture, even if it's not being used in tests.
446446+You can extend an already extended test to add more fixtures:
237447238448```ts
239239-import { test as base } from 'vitest'
449449+import { test as dbTest } from './my-test.js'
240450241241-const test = base.extend({
242242- fixture: [
243243- async ({}, use) => {
244244- // this function will run
245245- setup()
246246- await use()
247247- teardown()
248248- },
249249- { auto: true } // Mark as an automatic fixture
250250- ],
251251-})
252252-253253-test('works correctly')
451451+export const test = dbTest
452452+ .extend('user', ({ database }) => {
453453+ return database.createUser()
454454+ })
254455```
255456256256-#### Default fixture
457457+With the object syntax:
257458258258-Since Vitest 3, you can provide different values in different [projects](/guide/projects). To enable this feature, pass down `{ injected: true }` to the options. If the key is not specified in the [project configuration](/config/#provide), then the default value will be used.
459459+```ts
460460+import { test as dbTest } from './my-test.js'
461461+462462+export const test = dbTest.extend({
463463+ admin: async ({ database }, use) => {
464464+ const admin = await database.createAdmin()
465465+ await use(admin)
466466+ await database.deleteUser(admin.id)
467467+ }
468468+})
469469+```
470470+471471+### Mixing Both Syntaxes
472472+473473+You can combine both approaches. The builder pattern can be chained after object-based extensions:
474474+475475+```ts
476476+const test = baseTest
477477+ // Object syntax for simple fixtures
478478+ .extend<{ apiKey: string }>({
479479+ apiKey: 'test-key-123',
480480+ })
481481+ // Builder pattern for complex fixtures with inference
482482+ .extend('client', ({ apiKey }) => {
483483+ // TypeScript knows apiKey is string
484484+ return new ApiClient(apiKey)
485485+ })
486486+```
487487+488488+### Fixture Scopes <Version>3.2.0</Version> {#fixture-scopes}
489489+490490+By default, fixtures are initialized for each test. You can change this with the `scope` option to share fixtures across tests.
491491+492492+::: warning
493493+By default any fixture without a scope is treated as a `test` fixture. This means that you cannot use it inside `worker` and `file` scopes. If you wish to access it there, consider specifying a scope manually:
494494+495495+```ts
496496+test
497497+ .extend('port', { scope: 'worker' }, 5000)
498498+ .extend('db', { scope: 'worker' }, async ({ port }) => {
499499+ return createDb(port)
500500+ })
501501+```
502502+503503+Note that you cannot override non-test fixtures inside `describe` blocks:
504504+505505+```ts
506506+test.describe('a nested suite', () => {
507507+ test.override('port', 3000) // throws an error
508508+})
509509+```
510510+511511+Consider overriding it on the top level of the module, or by using [`injected`](#default-fixture-injected) option and providing the value in the project config.
512512+513513+Also note that in [non-isolate](/config/isolate) mode overriding a `worker` fixture will affect the fixture value in all test files running after it was overriden.
514514+515515+<!-- TODO(v5) should this be addressed? force a new worker if worker fixture is overriden? -->
516516+:::
517517+518518+#### Test Scope (Default)
519519+520520+Test-scoped fixtures are created fresh for each test:
521521+522522+```ts
523523+const test = baseTest
524524+ .extend('counter', () => {
525525+ return { value: 0 }
526526+ })
527527+528528+test('first test', ({ counter }) => {
529529+ counter.value++
530530+ expect(counter.value).toBe(1)
531531+})
532532+533533+test('second test', ({ counter }) => {
534534+ // Fresh instance, value is 0 again
535535+ expect(counter.value).toBe(0)
536536+})
537537+```
538538+539539+Test-scoped fixtures have access to the [built-in test context](#built-in-test-context) (`task`, `expect`, `skip`, etc.):
540540+541541+```ts
542542+const test = baseTest
543543+ .extend('testInfo', ({ task }) => {
544544+ return { name: task.name }
545545+ })
546546+```
547547+548548+#### File Scope
549549+550550+File-scoped fixtures are initialized once per test file:
551551+552552+```ts
553553+const test = baseTest
554554+ .extend('database', { scope: 'file' }, async ({}, { onCleanup }) => {
555555+ const db = await createDatabase()
556556+ onCleanup(() => db.close())
557557+ return db
558558+ })
559559+560560+test('first test', ({ database }) => {
561561+ // Uses the same database instance
562562+})
563563+564564+test('second test', ({ database }) => {
565565+ // Same database instance as first test
566566+})
567567+```
568568+569569+#### Worker Scope
570570+571571+Worker-scoped fixtures are initialized once per worker process:
572572+573573+```ts
574574+const test = baseTest
575575+ .extend('config', { scope: 'worker' }, () => {
576576+ return await loadExpensiveConfig()
577577+ })
578578+```
579579+580580+::: info
581581+By default, every file runs in a separate worker, so `file` and `worker` scopes work the same way. However, if you disable [isolation](/config/#isolate), then the number of workers is limited by [`maxWorkers`](/config/#maxworkers), and worker-scoped fixtures will be shared across files running in the same worker.
582582+583583+When running tests in `vmThreads` or `vmForks`, `scope: 'worker'` works the same way as `scope: 'file'` because each file has its own VM context.
584584+:::
585585+586586+#### Scope Hierarchy
587587+588588+Fixtures can only access other fixtures from the same or higher (longer-lived) scopes:
589589+590590+| Fixture Scope | Can Access |
591591+|---------------|------------|
592592+| `worker` | Only other worker fixtures |
593593+| `file` | Worker + file fixtures |
594594+| `test` | Worker + file + test fixtures + [test context](#built-in-test-context) |
595595+596596+```ts
597597+const test = baseTest
598598+ .extend('config', { scope: 'worker' }, () => {
599599+ return { apiUrl: 'https://api.example.com' }
600600+ })
601601+ .extend('database', { scope: 'file' }, async ({ config }, { onCleanup }) => {
602602+ // ✅ File fixture can access worker fixture
603603+ const db = await createDatabase(config.apiUrl)
604604+ onCleanup(() => db.close())
605605+ return db
606606+ })
607607+ .extend('user', async ({ database, task }) => {
608608+ // ✅ Test fixture can access file fixture AND test context
609609+ return await database.createUser(task.name)
610610+ })
611611+```
612612+613613+::: tip
614614+Only test-scoped fixtures have access to the [built-in test context](#built-in-test-context) (`task`, `expect`, `skip`, etc.). Worker and file fixtures run outside of any specific test, so test-specific properties are not available to them.
615615+616616+If you need the file path in a file-scoped fixture, use `expect.getState().testPath` instead.
617617+:::
618618+619619+#### Type-Safe Scope Access <Version>3.2.0</Version> {#type-safe-scope-access}
620620+621621+With the builder pattern, TypeScript automatically enforces scope-based access rules. If you try to access a test-scoped fixture from a file-scoped fixture, you'll get a compile-time error.
622622+623623+If you're using the object syntax and want the same type safety, you can use the `$worker`, `$file`, and `$test` keys to explicitly declare which fixtures belong to which scope:
624624+625625+```ts
626626+const test = baseTest.extend<{
627627+ $worker: { config: Config }
628628+ $file: { database: Database }
629629+ $test: { user: User }
630630+}>({
631631+ config: [async ({}, use) => {
632632+ await use(loadConfig())
633633+ }, { scope: 'worker' }],
634634+635635+ database: [async ({ config }, use) => {
636636+ const db = await createDatabase(config)
637637+ await use(db)
638638+ await db.close()
639639+ }, { scope: 'file' }],
640640+641641+ user: async ({ database }, use) => {
642642+ const user = await database.createUser()
643643+ await use(user)
644644+ await database.deleteUser(user.id)
645645+ },
646646+})
647647+```
648648+649649+This provides the same compile-time safety as the builder pattern, catching scope violations at build time rather than runtime.
650650+651651+### Default Fixture (Injected)
652652+653653+Since Vitest 3, you can provide different values in different [projects](/guide/projects). To enable this, pass `{ injected: true }` in the options. If the key is not specified in the [project configuration](/config/#provide), the default value will be used.
259654260655:::code-group
261656```ts [fixtures.test.ts]
262262-import { test as base } from 'vitest'
657657+import { test as baseTest } from 'vitest'
263658264264-const test = base.extend({
265265- url: [
266266- // default value if "url" is not defined in the config
267267- '/default',
268268- // mark the fixture as "injected" to allow the override
269269- { injected: true },
270270- ],
271271-})
659659+const test = baseTest
660660+ .extend('url', { injected: true }, '/default')
272661273662test('works correctly', ({ url }) => {
274663 // url is "/default" in "project-new"
···309698```
310699:::
311700312312-#### Scoping Values to Suite <Version>3.1.0</Version> {#scoping-values-to-suite}
701701+### Overriding Fixture Values <Version>4.1.0</Version> {#overriding-fixture-values}
313702314314-Since Vitest 3.1, you can override context values per suite and its children by using the `test.scoped` API:
703703+You can override fixture values for a specific suite and its children using `test.override`. This is useful when you need different fixture values for different test scenarios.
704704+705705+::: tip
706706+Vitest will automatically inherit the options, if they are not provided when overriding. Note that you cannot override fixture's `scope` or `auto` options.
707707+:::
708708+709709+#### Builder Pattern (Recommended)
315710316711```ts
317712import { test as baseTest, describe, expect } from 'vitest'
318713319319-const test = baseTest.extend({
320320- dependency: 'default',
321321- dependant: ({ dependency }, use) => use({ dependency })
714714+const test = baseTest
715715+ .extend('config', { port: 3000, host: 'localhost' })
716716+ .extend('server', ({ config }) => `http://${config.host}:${config.port}`)
717717+718718+describe('production environment', () => {
719719+ // Override with a new static value (chainable)
720720+ test
721721+ .override('config', { port: 8080, host: 'api.example.com' })
722722+723723+ test('uses production config', ({ server }) => {
724724+ expect(server).toBe('http://api.example.com:8080')
725725+ })
322726})
323727324324-describe('use scoped values', () => {
325325- test.scoped({ dependency: 'new' })
326326-327327- test('uses scoped value', ({ dependant }) => {
328328- // `dependant` uses the new overridden value that is scoped
329329- // to all tests in this suite
330330- expect(dependant).toEqual({ dependency: 'new' })
728728+describe('with custom server', () => {
729729+ // Override with a function that can access other fixtures
730730+ test.override('server', ({ config }) => {
731731+ return `https://${config.host}:${config.port}/v2`
331732 })
332733333333- describe('keeps using scoped value', () => {
334334- test('uses scoped value', ({ dependant }) => {
335335- // nested suite inherited the value
336336- expect(dependant).toEqual({ dependency: 'new' })
734734+ test('uses custom server', ({ server }) => {
735735+ expect(server).toBe('https://localhost:3000/v2')
736736+ })
737737+})
738738+739739+test('uses default values', ({ server }) => {
740740+ expect(server).toBe('http://localhost:3000')
741741+})
742742+```
743743+744744+#### Chaining Multiple Overrides
745745+746746+`test.override` returns the test API, so you can chain multiple calls:
747747+748748+```ts
749749+describe('production environment', () => {
750750+ test
751751+ .override('environment', 'production')
752752+ .override('port', 8080)
753753+ .override('debug', false)
754754+755755+ test('uses production settings', ({ environment, port, debug }) => {
756756+ expect(environment).toBe('production')
757757+ expect(port).toBe(8080)
758758+ expect(debug).toBe(false)
759759+ })
760760+})
761761+```
762762+763763+#### Object Syntax
764764+765765+You can also use object syntax to override multiple fixtures at once:
766766+767767+```ts
768768+describe('different configuration', () => {
769769+ test.override({
770770+ config: { port: 4000, host: 'test.local' },
771771+ })
772772+773773+ test('uses overwritten config', ({ config }) => {
774774+ expect(config.port).toBe(4000)
775775+ })
776776+})
777777+```
778778+779779+#### With Cleanup
780780+781781+When overwriting with a function, you can use `onCleanup` just like in `test.extend`:
782782+783783+```ts
784784+describe('with custom database', () => {
785785+ test.override('database', async ({ config }, { onCleanup }) => {
786786+ const db = await createTestDatabase(config)
787787+ onCleanup(() => db.drop())
788788+ return db
789789+ })
790790+791791+ test('uses custom database', ({ database }) => {
792792+ // Uses the overwritten database
793793+ })
794794+})
795795+```
796796+797797+#### Nested Scopes
798798+799799+Overrides are inherited by nested suites and can be overwritten again:
800800+801801+```ts
802802+describe('level 1', () => {
803803+ test.override('value', 'one')
804804+805805+ test('uses level 1 value', ({ value }) => {
806806+ expect(value).toBe('one')
807807+ })
808808+809809+ describe('level 2', () => {
810810+ test.override('value', 'two')
811811+812812+ test('uses level 2 value', ({ value }) => {
813813+ expect(value).toBe('two')
337814 })
338815 })
339339-})
340816341341-test('keep using the default values', ({ dependant }) => {
342342- // the `dependency` is using the default
343343- // value outside of the suite with .scoped
344344- expect(dependant).toEqual({ dependency: 'default' })
345345-})
346346-```
347347-348348-This API is particularly useful if you have a context value that relies on a dynamic variable like a database connection:
349349-350350-```ts
351351-const test = baseTest.extend<{
352352- db: Database
353353- schema: string
354354-}>({
355355- db: async ({ schema }, use) => {
356356- const db = await createDb({ schema })
357357- await use(db)
358358- await cleanup(db)
359359- },
360360- schema: '',
361361-})
362362-363363-describe('one type of schema', () => {
364364- test.scoped({ schema: 'schema-1' })
365365-366366- // ... tests
367367-})
368368-369369-describe('another type of schema', () => {
370370- test.scoped({ schema: 'schema-2' })
371371-372372- // ... tests
373373-})
374374-```
375375-376376-#### Per-Scope Context <Version>3.2.0</Version>
377377-378378-You can define context that will be initiated once per file or a worker. It is initiated the same way as a regular fixture with an objects parameter:
379379-380380-```ts
381381-import { test as baseTest } from 'vitest'
382382-383383-export const test = baseTest.extend({
384384- perFile: [
385385- ({}, use) => use([]),
386386- { scope: 'file' },
387387- ],
388388- perWorker: [
389389- ({}, use) => use([]),
390390- { scope: 'worker' },
391391- ],
392392-})
393393-```
394394-395395-The value is initialised the first time any test has accessed it, unless the fixture options have `auto: true` - in this case the value is initialised before any test has run.
396396-397397-```ts
398398-const test = baseTest.extend({
399399- perFile: [
400400- ({}, use) => use([]),
401401- {
402402- scope: 'file',
403403- // always run this hook before any test
404404- auto: true
405405- },
406406- ],
817817+ test('still uses level 1 value', ({ value }) => {
818818+ expect(value).toBe('one')
819819+ })
407820})
408821```
409822410823::: warning
411411-The built-in [`task`](#task) test context is **not available** in file-scoped or worker-scoped fixtures. These fixtures receive a different context object (file or worker context) that does not include test-specific properties like `task`.
412412-413413-If you need access to file-level metadata like the file path, use `expect.getState().testPath` instead.
824824+Note that you cannot introduce new fixtures inside `test.override`. Extend the test context with `test.extend` instead.
414825:::
415826416416-The `worker` scope will run the fixture once per worker. The number of running workers depends on various factors. By default, every file runs in a separate worker, so `file` and `worker` scopes work the same way.
417417-418418-However, if you disable [isolation](/config/#isolate), then the number of workers is limited by the [`maxWorkers`](/config/#maxworkers) configuration.
419419-420420-Note that specifying `scope: 'worker'` when running tests in `vmThreads` or `vmForks` will work the same way as `scope: 'file'`. This limitation exists because every test file has its own VM context, so if Vitest were to initiate it once, one context could leak to another and create many reference inconsistencies (instances of the same class would reference different constructors, for example).
421421-422422-#### TypeScript
423423-424424-To provide fixture types for all your custom contexts, you can pass the fixtures type as a generic.
425425-426426-```ts
427427-interface MyFixtures {
428428- todos: number[]
429429- archive: number[]
430430-}
431431-432432-const test = baseTest.extend<MyFixtures>({
433433- todos: [],
434434- archive: []
435435-})
436436-437437-test('types are defined correctly', ({ todos, archive }) => {
438438- expectTypeOf(todos).toEqualTypeOf<number[]>()
439439- expectTypeOf(archive).toEqualTypeOf<number[]>()
440440-})
441441-```
442442-443443-::: info Type Inferring
444444-Note that Vitest doesn't support infering the types when the `use` function is called. It is always preferable to pass down the whole context type as the generic type when `test.extend` is called:
445445-446446-```ts
447447-import { test as baseTest } from 'vitest'
448448-449449-const test = baseTest.extend<{
450450- todos: number[]
451451- schema: string
452452-}>({
453453- todos: ({ schema }, use) => use([]),
454454- schema: 'test'
455455-})
456456-457457-test('types are correct', ({
458458- todos, // number[]
459459- schema, // string
460460-}) => {
461461- // ...
462462-})
463463-```
464464-827827+::: info
828828+`test.scoped` is deprecated in favor of `test.override`. The `test.scoped` API still works but will be removed in a future version.
465829:::
830830+831831+### Type-Safe Hooks
466832467833When using `test.extend`, the extended `test` object provides type-safe `beforeEach` and `afterEach` hooks that are aware of the new context:
468834469835```ts
470470-const test = baseTest.extend<{
471471- todos: number[]
472472-}>({
473473- todos: async ({}, use) => {
474474- await use([])
475475- },
476476-})
836836+const test = baseTest
837837+ .extend('counter', { value: 0, increment() { this.value++ } })
477838478839// Unlike global hooks, these hooks are aware of the extended context
479479-test.beforeEach(({ todos }) => {
480480- todos.push(1)
840840+test.beforeEach(({ counter }) => {
841841+ counter.increment()
481842})
482843483483-test.afterEach(({ todos }) => {
484484- console.log(todos)
844844+test.afterEach(({ counter }) => {
845845+ console.log('Final count:', counter.value)
485846})
486847```
+1-1
packages/expect/src/utils.ts
···4949 const stack = processor(error.stack)
5050 console.warn([
5151 `Promise returned by \`${assertion}\` was not awaited. `,
5252- 'Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. ',
5252+ 'Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. ',
5353 'Please remember to await the assertion.\n',
5454 stack,
5555 ].join(''))
···11import type { Awaitable } from '@vitest/utils'
22import type { VitestRunner } from './types/runner'
33import type {
44- File,
54 RuntimeContext,
65 SuiteCollector,
76 Test,
···230229 error.stack = stackTraceError.stack.replace(error.message, stackTraceError.message)
231230 }
232231 return error
233233-}
234234-235235-const fileContexts = new WeakMap<File, Record<string, unknown>>()
236236-237237-export function getFileContext(file: File): Record<string, unknown> {
238238- const context = fileContexts.get(file)
239239- if (!context) {
240240- throw new Error(`Cannot find file context for ${file.name}`)
241241- }
242242- return context
243243-}
244244-245245-export function setFileContext(file: File, context: Record<string, unknown>): void {
246246- fileContexts.set(file, context)
247232}
+4
packages/runner/src/errors.ts
···2020 }
2121}
22222323+export class FixtureDependencyError extends Error {
2424+ public name = 'FixtureDependencyError'
2525+}
2626+2327export class AroundHookSetupError extends Error {
2428 public name = 'AroundHookSetupError'
2529}
+337-190
packages/runner/src/fixture.ts
···11-import type { VitestRunner } from './types'
22-import type { FixtureOptions, TestContext } from './types/tasks'
11+import type { FixtureFn, Suite, VitestRunner } from './types'
22+import type { File, FixtureOptions, TestContext } from './types/tasks'
33import { createDefer, filterOutComments, isObject } from '@vitest/utils/helpers'
44-import { getFileContext } from './context'
55-import { getTestFixture } from './map'
44+import { FixtureDependencyError } from './errors'
55+import { getTestFixtures } from './map'
66+import { getCurrentSuite } from './suite'
6777-export interface FixtureItem extends FixtureOptions {
88- prop: string
99- value: any
88+export interface TestFixtureItem extends FixtureOptions {
99+ name: string
1010+ value: unknown
1011 scope: 'test' | 'file' | 'worker'
1111- /**
1212- * Indicates whether the fixture is a function
1313- */
1414- isFn: boolean
1515- /**
1616- * The dependencies(fixtures) of current fixture function.
1717- */
1818- deps?: FixtureItem[]
1212+ deps: Set<string>
1313+ // so it's possible to call base fixture inside ({ a: ({ a }, use) => {} })
1414+ parent?: TestFixtureItem
1915}
20162121-export function mergeScopedFixtures(
2222- testFixtures: FixtureItem[],
2323- scopedFixtures: FixtureItem[],
2424-): FixtureItem[] {
2525- const scopedFixturesMap = scopedFixtures.reduce<Record<string, FixtureItem>>((map, fixture) => {
2626- map[fixture.prop] = fixture
2727- return map
2828- }, {})
2929- const newFixtures: Record<string, FixtureItem> = {}
3030- testFixtures.forEach((fixture) => {
3131- const useFixture = scopedFixturesMap[fixture.prop] || {
3232- // we need to clone the fixture because we override its values
3333- ...fixture,
3434- }
3535- newFixtures[useFixture.prop] = useFixture
3636- })
3737- for (const fixtureKep in newFixtures) {
3838- const fixture = newFixtures[fixtureKep]
3939- // if the fixture was define before the scope, then its dep
4040- // will reference the original fixture instead of the scope
4141- fixture.deps = fixture.deps?.map(dep => newFixtures[dep.prop])
1717+export type UserFixtures = Record<string, unknown>
1818+export type FixtureRegistrations = Map<string, TestFixtureItem>
1919+2020+export class TestFixtures {
2121+ private _suiteContexts: WeakMap<Suite | symbol, /* context object */ Record<string, unknown>>
2222+ private _overrides = new WeakMap<Suite, FixtureRegistrations>()
2323+ private _registrations: FixtureRegistrations
2424+2525+ private static _definitions: TestFixtures[] = []
2626+ private static _builtinFixtures: string[] = [
2727+ 'task',
2828+ 'signal',
2929+ 'onTestFailed',
3030+ 'onTestFinished',
3131+ 'skip',
3232+ 'annotate',
3333+ ] satisfies (keyof TestContext)[]
3434+3535+ private static _fixtureOptionKeys: string[] = ['auto', 'injected', 'scope']
3636+ private static _fixtureScopes: string[] = ['test', 'file', 'worker']
3737+ private static _workerContextSymbol = Symbol('workerContext')
3838+3939+ static clearDefinitions(): void {
4040+ TestFixtures._definitions.length = 0
4241 }
4343- return Object.values(newFixtures)
4444-}
45424646-export function mergeContextFixtures<T extends { fixtures?: FixtureItem[] }>(
4747- fixtures: Record<string, any>,
4848- context: T,
4949- runner: VitestRunner,
5050-): T {
5151- const fixtureOptionKeys = ['auto', 'injected', 'scope']
5252- const fixtureArray: FixtureItem[] = Object.entries(fixtures).map(
5353- ([prop, value]) => {
5454- const fixtureItem = { value } as FixtureItem
4343+ static getWorkerContexts(): Record<string, any>[] {
4444+ return TestFixtures._definitions.map(f => f.getWorkerContext())
4545+ }
4646+4747+ static getFileContexts(file: File): Record<string, any>[] {
4848+ return TestFixtures._definitions.map(f => f.getFileContext(file))
4949+ }
5050+5151+ constructor(registrations?: FixtureRegistrations) {
5252+ this._registrations = registrations ?? new Map()
5353+ this._suiteContexts = new WeakMap()
5454+ TestFixtures._definitions.push(this)
5555+ }
5656+5757+ extend(runner: VitestRunner, userFixtures: UserFixtures): TestFixtures {
5858+ const { suite } = getCurrentSuite()
5959+ const isTopLevel = !suite || suite.file === suite
6060+ const registrations = this.parseUserFixtures(runner, userFixtures, isTopLevel)
6161+ return new TestFixtures(registrations)
6262+ }
6363+6464+ get(suite: Suite): FixtureRegistrations {
6565+ let currentSuite: Suite | undefined = suite
6666+ while (currentSuite) {
6767+ const overrides = this._overrides.get(currentSuite)
6868+ // return the closest override
6969+ if (overrides) {
7070+ return overrides
7171+ }
7272+ if (currentSuite === currentSuite.file) {
7373+ break
7474+ }
7575+ currentSuite = currentSuite.suite || currentSuite.file
7676+ }
7777+ return this._registrations
7878+ }
7979+8080+ override(runner: VitestRunner, userFixtures: UserFixtures): void {
8181+ const { suite: currentSuite, file } = getCurrentSuite()
8282+ const suite = currentSuite || file
8383+ const isTopLevel = !currentSuite || currentSuite.file === currentSuite
8484+ // Create a copy of the closest parent's registrations to avoid modifying them
8585+ // For chained calls, this.get(suite) returns this suite's overrides; for first call, returns parent's
8686+ const suiteRegistrations = new Map(this.get(suite))
8787+ const registrations = this.parseUserFixtures(runner, userFixtures, isTopLevel, suiteRegistrations)
8888+ // If defined in top-level, just override all registrations
8989+ // We don't support overriding suite-level fixtures anyway (it will throw an error)
9090+ if (isTopLevel) {
9191+ this._registrations = registrations
9292+ }
9393+ else {
9494+ this._overrides.set(suite, registrations)
9595+ }
9696+ }
9797+9898+ getFileContext(file: File): Record<string, any> {
9999+ if (!this._suiteContexts.has(file)) {
100100+ this._suiteContexts.set(file, Object.create(null))
101101+ }
102102+ return this._suiteContexts.get(file)!
103103+ }
104104+105105+ getWorkerContext(): Record<string, any> {
106106+ if (!this._suiteContexts.has(TestFixtures._workerContextSymbol)) {
107107+ this._suiteContexts.set(TestFixtures._workerContextSymbol, Object.create(null))
108108+ }
109109+ return this._suiteContexts.get(TestFixtures._workerContextSymbol)!
110110+ }
111111+112112+ private parseUserFixtures(
113113+ runner: VitestRunner,
114114+ userFixtures: UserFixtures,
115115+ supportNonTest: boolean,
116116+ registrations = new Map<string, TestFixtureItem>(this._registrations),
117117+ ) {
118118+ const errors: Error[] = []
119119+120120+ Object.entries(userFixtures).forEach(([name, fn]) => {
121121+ let options: FixtureOptions | undefined
122122+ let value: unknown | undefined
123123+ let _options: FixtureOptions | undefined
5512456125 if (
5757- Array.isArray(value)
5858- && value.length >= 2
5959- && isObject(value[1])
6060- && Object.keys(value[1]).some(key => fixtureOptionKeys.includes(key))
126126+ Array.isArray(fn)
127127+ && fn.length >= 2
128128+ && isObject(fn[1])
129129+ && Object.keys(fn[1]).some(key => TestFixtures._fixtureOptionKeys.includes(key))
61130 ) {
6262- // fixture with options
6363- Object.assign(fixtureItem, value[1])
6464- const userValue = value[0]
6565- fixtureItem.value = fixtureItem.injected
6666- ? (runner.injectValue?.(prop) ?? userValue)
6767- : userValue
131131+ _options = fn[1] as FixtureOptions
132132+ options = {
133133+ auto: _options.auto ?? false,
134134+ scope: _options.scope ?? 'test',
135135+ injected: _options.injected ?? false,
136136+ }
137137+ value = options.injected
138138+ ? (runner.injectValue?.(name) ?? fn[0])
139139+ : fn[0]
140140+ }
141141+ else {
142142+ value = fn
68143 }
691447070- fixtureItem.scope = fixtureItem.scope || 'test'
7171- if (fixtureItem.scope === 'worker' && !runner.getWorkerContext) {
7272- fixtureItem.scope = 'file'
145145+ const parent = registrations.get(name)
146146+ if (parent && options) {
147147+ if (parent.scope !== options.scope) {
148148+ errors.push(new FixtureDependencyError(`The "${name}" fixture was already registered with a "${options.scope}" scope.`))
149149+ }
150150+ if (parent.auto !== options.auto) {
151151+ errors.push(new FixtureDependencyError(`The "${name}" fixture was already registered as { auto: ${options.auto} }.`))
152152+ }
73153 }
7474- fixtureItem.prop = prop
7575- fixtureItem.isFn = typeof fixtureItem.value === 'function'
7676- return fixtureItem
7777- },
7878- )
7979-8080- if (Array.isArray(context.fixtures)) {
8181- context.fixtures = context.fixtures.concat(fixtureArray)
8282- }
8383- else {
8484- context.fixtures = fixtureArray
8585- }
8686-8787- // Update dependencies of fixture functions
8888- fixtureArray.forEach((fixture) => {
8989- if (fixture.isFn) {
9090- const usedProps = getUsedProps(fixture.value)
9191- if (usedProps.length) {
9292- fixture.deps = context.fixtures!.filter(
9393- ({ prop }) => prop !== fixture.prop && usedProps.includes(prop),
9494- )
154154+ else if (parent) {
155155+ options = {
156156+ auto: parent.auto,
157157+ scope: parent.scope,
158158+ injected: parent.injected,
159159+ }
95160 }
9696- // test can access anything, so we ignore it
9797- if (fixture.scope !== 'test') {
9898- fixture.deps?.forEach((dep) => {
9999- if (!dep.isFn) {
100100- // non fn fixtures are always resolved and available to anyone
101101- return
102102- }
103103- // worker scope can only import from worker scope
104104- if (fixture.scope === 'worker' && dep.scope === 'worker') {
105105- return
106106- }
107107- // file scope an import from file and worker scopes
108108- if (fixture.scope === 'file' && dep.scope !== 'test') {
109109- return
110110- }
161161+ else if (!options) {
162162+ options = {
163163+ auto: false,
164164+ injected: false,
165165+ scope: 'test',
166166+ }
167167+ }
111168112112- throw new SyntaxError(`cannot use the ${dep.scope} fixture "${dep.prop}" inside the ${fixture.scope} fixture "${fixture.prop}"`)
113113- })
169169+ if (options.scope && !TestFixtures._fixtureScopes.includes(options.scope)) {
170170+ errors.push(new FixtureDependencyError(`The "${name}" fixture has unknown scope "${options.scope}".`))
171171+ }
172172+173173+ if (!supportNonTest && options.scope !== 'test') {
174174+ errors.push(new FixtureDependencyError(`The "${name}" fixture cannot be defined with a ${options.scope} scope${!_options?.scope && parent?.scope ? ' (inherited from the base fixture)' : ''} inside the describe block. Define it at the top level of the file instead.`))
175175+ }
176176+177177+ const deps = isFixtureFunction(value)
178178+ ? getUsedProps(value)
179179+ : new Set<string>()
180180+ const item: TestFixtureItem = {
181181+ name,
182182+ value,
183183+ auto: options.auto ?? false,
184184+ injected: options.injected ?? false,
185185+ scope: options.scope ?? 'test',
186186+ deps,
187187+ parent,
188188+ }
189189+190190+ registrations.set(name, item)
191191+192192+ if (item.scope === 'worker' && (runner.pool === 'vmThreads' || runner.pool === 'vmForks')) {
193193+ item.scope = 'file'
194194+ }
195195+ })
196196+197197+ // validate fixture dependency scopes
198198+ for (const fixture of registrations.values()) {
199199+ for (const depName of fixture.deps) {
200200+ if (TestFixtures._builtinFixtures.includes(depName)) {
201201+ continue
202202+ }
203203+204204+ const dep = registrations.get(depName)
205205+ if (!dep) {
206206+ errors.push(new FixtureDependencyError(`The "${fixture.name}" fixture depends on unknown fixture "${depName}".`))
207207+ continue
208208+ }
209209+ if (depName === fixture.name && !fixture.parent) {
210210+ errors.push(new FixtureDependencyError(`The "${fixture.name}" fixture depends on itself, but does not have a base implementation.`))
211211+ continue
212212+ }
213213+214214+ if (TestFixtures._fixtureScopes.indexOf(fixture.scope) > TestFixtures._fixtureScopes.indexOf(dep.scope)) {
215215+ errors.push(new FixtureDependencyError(`The ${fixture.scope} "${fixture.name}" fixture cannot depend on a ${dep.scope} fixture "${dep.name}".`))
216216+ continue
217217+ }
114218 }
115219 }
116116- })
117220118118- return context
221221+ if (errors.length === 1) {
222222+ throw errors[0]
223223+ }
224224+ else if (errors.length > 1) {
225225+ throw new AggregateError(errors, 'Cannot resolve user fixtures. See errors for more information.')
226226+ }
227227+ return registrations
228228+ }
119229}
120230121121-const fixtureValueMaps = new Map<TestContext, Map<FixtureItem, any>>()
122122-const cleanupFnArrayMap = new Map<
231231+const cleanupFnArrayMap = new WeakMap<
123232 object,
124233 Array<() => void | Promise<void>>
125234>()
···160269 cleanupFnArray.length = fromIndex
161270}
162271163163-export function withFixtures(runner: VitestRunner, fn: Function, testContext?: TestContext) {
164164- return (hookContext?: TestContext): any => {
165165- const context: (TestContext & { [key: string]: any }) | undefined
166166- = hookContext || testContext
272272+const contextHasFixturesCache = new WeakMap<TestContext, WeakSet<TestFixtureItem>>()
273273+274274+export function withFixtures(fn: Function, testContext?: TestContext) {
275275+ const collector = getCurrentSuite()
276276+ const suite = collector.suite || collector.file
277277+ return async (hookContext?: TestContext): Promise<any> => {
278278+ const context: (TestContext & { [key: string]: any }) | undefined = hookContext || testContext
167279168280 if (!context) {
169281 return fn({})
170282 }
171283172172- const fixtures = getTestFixture(context)
173173- if (!fixtures?.length) {
284284+ const fixtures = getTestFixtures(context)
285285+ if (!fixtures) {
174286 return fn(context)
175287 }
176288289289+ const registrations = fixtures.get(suite)
290290+ if (!registrations.size) {
291291+ return fn(context)
292292+ }
293293+294294+ const usedFixtures: TestFixtureItem[] = []
177295 const usedProps = getUsedProps(fn)
178178- const hasAutoFixture = fixtures.some(({ auto }) => auto)
179179- if (!usedProps.length && !hasAutoFixture) {
180180- return fn(context)
296296+297297+ for (const fixture of registrations.values()) {
298298+ if (fixture.auto || usedProps.has(fixture.name)) {
299299+ usedFixtures.push(fixture)
300300+ }
181301 }
182302183183- if (!fixtureValueMaps.get(context)) {
184184- fixtureValueMaps.set(context, new Map<FixtureItem, any>())
303303+ if (!usedFixtures.length) {
304304+ return fn(context)
185305 }
186186- const fixtureValueMap: Map<FixtureItem, any>
187187- = fixtureValueMaps.get(context)!
188306189307 if (!cleanupFnArrayMap.has(context)) {
190308 cleanupFnArrayMap.set(context, [])
191309 }
192310 const cleanupFnArray = cleanupFnArrayMap.get(context)!
193311194194- const usedFixtures = fixtures.filter(
195195- ({ prop, auto }) => auto || usedProps.includes(prop),
196196- )
197197- const pendingFixtures = resolveDeps(usedFixtures)
312312+ const pendingFixtures = resolveDeps(usedFixtures, registrations)
198313199314 if (!pendingFixtures.length) {
200315 return fn(context)
201316 }
202317203203- async function resolveFixtures() {
204204- for (const fixture of pendingFixtures) {
318318+ if (!contextHasFixturesCache.has(context)) {
319319+ contextHasFixturesCache.set(context, new WeakSet())
320320+ }
321321+ const cachedFixtures = contextHasFixturesCache.get(context)!
322322+323323+ for (const fixture of pendingFixtures) {
324324+ if (fixture.scope === 'test') {
205325 // fixture could be already initialized during "before" hook
206206- if (fixtureValueMap.has(fixture)) {
326326+ // we can't check "fixture.name" in context because context may
327327+ // access the parent fixture ({ a: ({ a }) => {} })
328328+ if (cachedFixtures.has(fixture)) {
207329 continue
208330 }
331331+ cachedFixtures.add(fixture)
209332210210- const resolvedValue = await resolveFixtureValue(
211211- runner,
333333+ const resolvedValue = await resolveTestFixtureValue(
212334 fixture,
213213- context!,
335335+ context,
214336 cleanupFnArray,
215337 )
216216- context![fixture.prop] = resolvedValue
217217- fixtureValueMap.set(fixture, resolvedValue)
338338+ context[fixture.name] = resolvedValue
218339219219- if (fixture.scope === 'test') {
220220- cleanupFnArray.unshift(() => {
221221- fixtureValueMap.delete(fixture)
222222- })
223223- }
340340+ cleanupFnArray.push(() => {
341341+ cachedFixtures.delete(fixture)
342342+ })
343343+ }
344344+ else {
345345+ const resolvedValue = await resolveScopeFixtureValue(
346346+ fixtures,
347347+ suite,
348348+ fixture,
349349+ )
350350+ context[fixture.name] = resolvedValue
224351 }
225352 }
226353227227- return resolveFixtures().then(() => fn(context))
354354+ return fn(context)
228355 }
229356}
230357231231-const globalFixturePromise = new WeakMap<FixtureItem, Promise<unknown>>()
358358+function isFixtureFunction(value: unknown): value is FixtureFn<any, any, any> {
359359+ return typeof value === 'function'
360360+}
232361233233-function resolveFixtureValue(
234234- runner: VitestRunner,
235235- fixture: FixtureItem,
362362+function resolveTestFixtureValue(
363363+ fixture: TestFixtureItem,
236364 context: TestContext & { [key: string]: any },
237365 cleanupFnArray: (() => void | Promise<void>)[],
238366) {
239239- const fileContext = getFileContext(context.task.file)
240240- const workerContext = runner.getWorkerContext?.()
241241-242242- if (!fixture.isFn) {
243243- fileContext[fixture.prop] ??= fixture.value
244244- if (workerContext) {
245245- workerContext[fixture.prop] ??= fixture.value
246246- }
367367+ if (!isFixtureFunction(fixture.value)) {
247368 return fixture.value
248369 }
249370250250- if (fixture.scope === 'test') {
251251- return resolveFixtureFunction(
252252- fixture.value,
253253- context,
254254- cleanupFnArray,
255255- )
371371+ return resolveFixtureFunction(
372372+ fixture.value,
373373+ context,
374374+ cleanupFnArray,
375375+ )
376376+}
377377+378378+const scopedFixturePromiseCache = new WeakMap<TestFixtureItem, Promise<unknown>>()
379379+380380+async function resolveScopeFixtureValue(
381381+ fixtures: TestFixtures,
382382+ suite: Suite,
383383+ fixture: TestFixtureItem,
384384+) {
385385+ const workerContext = fixtures.getWorkerContext()
386386+ const fileContext = fixtures.getFileContext(suite.file)
387387+ const fixtureContext = fixture.scope === 'worker' ? workerContext : fileContext
388388+389389+ if (!isFixtureFunction(fixture.value)) {
390390+ fixtureContext[fixture.name] = fixture.value
391391+ return fixture.value
256392 }
257393258258- // in case the test runs in parallel
259259- if (globalFixturePromise.has(fixture)) {
260260- return globalFixturePromise.get(fixture)!
394394+ if (fixture.name in fixtureContext) {
395395+ return fixtureContext[fixture.name]
261396 }
262397263263- let fixtureContext: Record<string, unknown>
264264-265265- if (fixture.scope === 'worker') {
266266- if (!workerContext) {
267267- throw new TypeError('[@vitest/runner] The worker context is not available in the current test runner. Please, provide the `getWorkerContext` method when initiating the runner.')
268268- }
269269- fixtureContext = workerContext
270270- }
271271- else {
272272- fixtureContext = fileContext
273273- }
274274-275275- if (fixture.prop in fixtureContext) {
276276- return fixtureContext[fixture.prop]
398398+ if (scopedFixturePromiseCache.has(fixture)) {
399399+ return scopedFixturePromiseCache.get(fixture)!
277400 }
278401279402 if (!cleanupFnArrayMap.has(fixtureContext)) {
···283406284407 const promise = resolveFixtureFunction(
285408 fixture.value,
286286- fixtureContext,
409409+ fixture.scope === 'file' ? { ...workerContext, ...fileContext } : fixtureContext,
287410 cleanupFnFileArray,
288411 ).then((value) => {
289289- fixtureContext[fixture.prop] = value
290290- globalFixturePromise.delete(fixture)
412412+ fixtureContext[fixture.name] = value
413413+ scopedFixturePromiseCache.delete(fixture)
291414 return value
292415 })
293293-294294- globalFixturePromise.set(fixture, promise)
416416+ scopedFixturePromiseCache.set(fixture, promise)
295417 return promise
296418}
297419···335457}
336458337459function resolveDeps(
338338- fixtures: FixtureItem[],
339339- depSet = new Set<FixtureItem>(),
340340- pendingFixtures: FixtureItem[] = [],
460460+ usedFixtures: TestFixtureItem[],
461461+ registrations: FixtureRegistrations,
462462+ depSet = new Set<TestFixtureItem>(),
463463+ pendingFixtures: TestFixtureItem[] = [],
341464) {
342342- fixtures.forEach((fixture) => {
465465+ usedFixtures.forEach((fixture) => {
343466 if (pendingFixtures.includes(fixture)) {
344467 return
345468 }
346346- if (!fixture.isFn || !fixture.deps) {
469469+ if (!isFixtureFunction(fixture.value) || !fixture.deps) {
347470 pendingFixtures.push(fixture)
348471 return
349472 }
350473 if (depSet.has(fixture)) {
351351- throw new Error(
352352- `Circular fixture dependency detected: ${fixture.prop} <- ${[...depSet]
353353- .reverse()
354354- .map(d => d.prop)
355355- .join(' <- ')}`,
356356- )
474474+ if (fixture.parent) {
475475+ fixture = fixture.parent
476476+ }
477477+ else {
478478+ throw new Error(
479479+ `Circular fixture dependency detected: ${fixture.name} <- ${[...depSet]
480480+ .reverse()
481481+ .map(d => d.name)
482482+ .join(' <- ')}`,
483483+ )
484484+ }
357485 }
358486359487 depSet.add(fixture)
360360- resolveDeps(fixture.deps, depSet, pendingFixtures)
488488+ resolveDeps(
489489+ [...fixture.deps].map(n => n === fixture.name ? fixture.parent : registrations.get(n)).filter(n => !!n),
490490+ registrations,
491491+ depSet,
492492+ pendingFixtures,
493493+ )
361494 pendingFixtures.push(fixture)
362495 depSet.clear()
363496 })
···365498 return pendingFixtures
366499}
367500368368-function getUsedProps(fn: Function) {
501501+const propsSymbol = Symbol('$vitest:fixture-props')
502502+503503+export function migrateProps(from: Function, to: Function): void {
504504+ const props = getUsedProps(from)
505505+ Object.defineProperty(to, propsSymbol, {
506506+ enumerable: false,
507507+ writable: true,
508508+ value: props,
509509+ })
510510+}
511511+512512+function getUsedProps(fn: Function): Set<string> {
513513+ if (propsSymbol in fn) {
514514+ return fn[propsSymbol] as Set<string>
515515+ }
369516 let fnString = filterOutComments(fn.toString())
370517 // match lowered async function and strip it off
371518 // example code on esbuild-try https://esbuild.github.io/try/#YgAwLjI0LjAALS1zdXBwb3J0ZWQ6YXN5bmMtYXdhaXQ9ZmFsc2UAZQBlbnRyeS50cwBjb25zdCBvID0gewogIGYxOiBhc3luYyAoKSA9PiB7fSwKICBmMjogYXN5bmMgKGEpID0+IHt9LAogIGYzOiBhc3luYyAoYSwgYikgPT4ge30sCiAgZjQ6IGFzeW5jIGZ1bmN0aW9uKGEpIHt9LAogIGY1OiBhc3luYyBmdW5jdGlvbiBmZihhKSB7fSwKICBhc3luYyBmNihhKSB7fSwKCiAgZzE6IGFzeW5jICgpID0+IHt9LAogIGcyOiBhc3luYyAoeyBhIH0pID0+IHt9LAogIGczOiBhc3luYyAoeyBhIH0sIGIpID0+IHt9LAogIGc0OiBhc3luYyBmdW5jdGlvbiAoeyBhIH0pIHt9LAogIGc1OiBhc3luYyBmdW5jdGlvbiBnZyh7IGEgfSkge30sCiAgYXN5bmMgZzYoeyBhIH0pIHt9LAoKICBoMTogYXN5bmMgKCkgPT4ge30sCiAgLy8gY29tbWVudCBiZXR3ZWVuCiAgaDI6IGFzeW5jIChhKSA9PiB7fSwKfQ
···377524 }
378525 const match = fnString.match(/[^(]*\(([^)]*)/)
379526 if (!match) {
380380- return []
527527+ return new Set()
381528 }
382529383530 const args = splitByComma(match[1])
384531 if (!args.length) {
385385- return []
532532+ return new Set()
386533 }
387534388535 let first = args[0]
389536 if ('__VITEST_FIXTURE_INDEX__' in fn) {
390537 first = args[(fn as any).__VITEST_FIXTURE_INDEX__]
391538 if (!first) {
392392- return []
539539+ return new Set()
393540 }
394541 }
395542···411558 )
412559 }
413560414414- return props
561561+ return new Set(props)
415562}
416563417564function splitByComma(s: string) {
+4-9
packages/runner/src/hooks.ts
···11-import type { VitestRunner } from './types'
21import type {
32 AfterAllListener,
43 AfterEachListener,
···144143): void {
145144 assertTypes(fn, '"beforeEach" callback', ['function'])
146145 const stackTraceError = new Error('STACK_TRACE_ERROR')
147147- const runner = getRunner()
148146 return getCurrentSuite<ExtraContext>().on(
149147 'beforeEach',
150148 Object.assign(
151149 withTimeout(
152152- withFixtures(runner, fn),
150150+ withFixtures(fn),
153151 timeout ?? getDefaultHookTimeout(),
154152 true,
155153 stackTraceError,
···185183 timeout?: number,
186184): void {
187185 assertTypes(fn, '"afterEach" callback', ['function'])
188188- const runner = getRunner()
189186 return getCurrentSuite<ExtraContext>().on(
190187 'afterEach',
191188 withTimeout(
192192- withFixtures(runner, fn),
189189+ withFixtures(fn),
193190 timeout ?? getDefaultHookTimeout(),
194191 true,
195192 new Error('STACK_TRACE_ERROR'),
···351348 assertTypes(fn, '"aroundEach" callback', ['function'])
352349 const stackTraceError = new Error('STACK_TRACE_ERROR')
353350 const resolvedTimeout = timeout ?? getDefaultHookTimeout()
354354- const runner = getRunner()
355351356352 // Create a wrapper function that supports fixtures in the second argument (context)
357353 // withFixtures resolves fixtures into context, then we call fn with all 3 args
358358- const wrappedFn: AroundEachListener<ExtraContext> = withAroundEachFixtures(runner, fn)
354354+ const wrappedFn: AroundEachListener<ExtraContext> = withAroundEachFixtures(fn)
359355360356 // Store timeout and stack trace on the function for use in callAroundEachHooks
361357 // Setup and teardown phases will each have their own timeout
···376372 * - Third arg is suite
377373 */
378374function withAroundEachFixtures<ExtraContext>(
379379- runner: VitestRunner,
380375 fn: AroundEachListener<ExtraContext>,
381376): AroundEachListener<ExtraContext> {
382377 // Create the wrapper that will be returned
···390385 ;(innerFn as any).toString = () => fn.toString()
391386392387 // Use withFixtures to resolve fixtures, passing context as the hook context
393393- const fixtureResolver = withFixtures(runner, innerFn)
388388+ const fixtureResolver = withFixtures(innerFn)
394389 return fixtureResolver(context)
395390 }
396391
+3-3
packages/runner/src/map.ts
···11import type { Awaitable } from '@vitest/utils'
22-import type { FixtureItem } from './fixture'
22+import type { TestFixtures } from './fixture'
33import type { Suite, SuiteHooks, Test, TestContext } from './types/tasks'
4455// use WeakMap here to make the Test and Suite object serializable
···17171818export function setTestFixture(
1919 key: TestContext,
2020- fixture: FixtureItem[] | undefined,
2020+ fixture: TestFixtures,
2121): void {
2222 testFixtureMap.set(key, fixture)
2323}
24242525-export function getTestFixture<Context = TestContext>(key: Context): FixtureItem[] {
2525+export function getTestFixtures<Context = TestContext>(key: Context): TestFixtures {
2626 return testFixtureMap.get(key as any)
2727}
2828
···11561156 >> user fixture setup
11571157 >> test running, db available: true
11581158 >> user fixture teardown
11591159- >> db fixture teardown
11601160- >> aroundEach teardown"
11591159+ >> aroundEach teardown
11601160+ >> db fixture teardown"
11611161 `)
11621162 expect(errorTree()).toMatchInlineSnapshot(`
11631163 {
+5-5
test/cli/test/fails.test.ts
···9393 })
9494 expect(warnings).toMatchInlineSnapshot(`
9595 [
9696- "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion.
9696+ "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. Please remember to await the assertion.
9797 at <rootDir>/base.test.js:5:33",
9898- "Promise returned by \`expect(actual).rejects.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion.
9898+ "Promise returned by \`expect(actual).rejects.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. Please remember to await the assertion.
9999 at <rootDir>/base.test.js:10:32",
100100- "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion.
100100+ "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. Please remember to await the assertion.
101101 at <rootDir>/base.test.js:9:33",
102102- "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion.
102102+ "Promise returned by \`expect(actual).resolves.toBe(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. Please remember to await the assertion.
103103 at <rootDir>/base.test.js:14:33",
104104- "Promise returned by \`expect(actual).toMatchFileSnapshot(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. Please remember to await the assertion.
104104+ "Promise returned by \`expect(actual).toMatchFileSnapshot(expected)\` was not awaited. Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in the next Vitest major. Please remember to await the assertion.
105105 at <rootDir>/base.test.js:19:17",
106106 ]
107107 `)