[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat: support type inference with a new `test.extend` syntax (#9550)

authored by

Vladimir and committed by
GitHub
(Feb 2, 2026, 4:27 PM +0100) e53854fc 7f031039

+3444 -688
+4
AGENTS.md
··· 35 35 - **Core directory test**: `CI=true pnpm test <test-file>` (for `test/core`) 36 36 - **Browser tests**: `CI=true pnpm test:browser:playwright` or `CI=true pnpm test:browser:webdriverio` 37 37 38 + When writing tests, AVOID using `toContain` for validation. Prefer using `toMatchSnapshot` to include the test error and its stack. 39 + 40 + If you need to typecheck tests, run `pnpm typecheck` from the root of the workspace. 41 + 38 42 ### Testing Utilities 39 43 - **`runInlineTests`** from `test/test-utils/index.ts` - You must use this for complex file system setups (>1 file) 40 44 - **`runVitest`** from `test/test-utils/index.ts` - You can use this to run Vitest programmatically
+38 -29
docs/api/test.md
··· 261 261 262 262 - **Alias:** `it.extend` 263 263 264 - 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. 264 + 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. 265 265 266 266 ```ts 267 267 import { test as baseTest, expect } from 'vitest' 268 268 269 - const todos = [] 270 - const archive = [] 269 + export const test = baseTest 270 + // Simple value - type is inferred as { port: number; host: string } 271 + .extend('config', { port: 3000, host: 'localhost' }) 272 + // Function fixture - type is inferred from return value 273 + .extend('server', async ({ config }) => { 274 + // TypeScript knows config is { port: number; host: string } 275 + return `http://${config.host}:${config.port}` 276 + }) 271 277 272 - const test = baseTest.extend({ 273 - todos: async ({ task }, use) => { 274 - todos.push(1, 2, 3) 275 - await use(todos) 276 - todos.length = 0 277 - }, 278 - archive, 279 - }) 280 - 281 - test('add item', ({ todos }) => { 282 - expect(todos.length).toBe(3) 283 - 284 - todos.push(4) 285 - expect(todos.length).toBe(4) 278 + test('server uses correct port', ({ config, server }) => { 279 + // TypeScript knows the types: 280 + // - config is { port: number; host: string } 281 + // - server is string 282 + expect(server).toBe('http://localhost:3000') 283 + expect(config.port).toBe(3000) 286 284 }) 287 285 ``` 288 286 289 - ## test.scoped <Version>3.1.0</Version> {#test-scoped} 287 + ## test.override <Version>4.1.0</Version> {#test-override} 290 288 291 - - **Alias:** `it.scoped` 292 - 293 - 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. 289 + 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. 294 290 295 291 ```ts 296 292 import { test as baseTest, describe, expect } from 'vitest' 297 293 298 - const test = baseTest.extend({ 299 - dependency: 'default', 300 - dependant: ({ dependency }, use) => use({ dependency }), 301 - }) 294 + const test = baseTest 295 + .extend('dependency', 'default') 296 + .extend('dependant', ({ dependency }) => dependency) 302 297 303 298 describe('use scoped values', () => { 304 - test.scoped({ dependency: 'new' }) 299 + test.override({ dependency: 'new' }) 305 300 306 301 test('uses scoped value', ({ dependant }) => { 307 302 // `dependant` uses the new overridden value that is scoped ··· 310 305 }) 311 306 }) 312 307 ``` 308 + 309 + ## test.scoped <Version>3.1.0</Version> <Deprecated /> {#test-scoped} 310 + 311 + - **Alias:** `it.scoped` 312 + 313 + ::: danger DEPRECATED 314 + `test.scoped` is deprecated in favor of [`test.override`](#test-override) and will be removed in a future major version. 315 + ::: 316 + 317 + Alias of [`test.override`](#test-override) 313 318 314 319 ## test.skip 315 320 ··· 661 666 662 667 Scoped `describe`. See [describe](/api/describe) for more information. 663 668 669 + ## test.suite <Version>4.1.0</Version> {#test-suite} 670 + 671 + Alias for `suite`. See [describe](/api/describe) for more information. 672 + 664 673 ## test.beforeEach 665 674 666 675 Scoped `beforeEach` hook that inherits types from [`test.extend`](#test-extend). See [beforeEach](/api/hooks#beforeeach) for more information. ··· 671 680 672 681 ## test.beforeAll 673 682 674 - Scoped `beforeAll` hook. See [beforeAll](/api/hooks#beforeall) for more information. 683 + Scoped `beforeAll` hook that inherits types from [`test.extend`](#test-extend). See [beforeAll](/api/hooks#beforeall) for more information. 675 684 676 685 ## test.afterAll 677 686 678 - Scoped `afterAll` hook. See [afterAll](/api/hooks#afterall) for more information. 687 + Scoped `afterAll` hook that inherits types from [`test.extend`](#test-extend). See [afterAll](/api/hooks#afterall) for more information. 679 688 680 689 ## test.aroundEach <Version>4.1.0</Version> {#test-aroundeach} 681 690 ··· 683 692 684 693 ## test.aroundAll <Version>4.1.0</Version> {#test-aroundall} 685 694 686 - Scoped `aroundAll` hook. See [aroundAll](/api/hooks#aroundall) for more information. 695 + Scoped `aroundAll` hook that inherits types from [`test.extend`](#test-extend). See [aroundAll](/api/hooks#aroundall) for more information. 687 696 688 697 ## bench <Experimental /> {#bench} 689 698
+595 -234
docs/guide/test-context.md
··· 127 127 128 128 ## Extend Test Context 129 129 130 - Vitest provides two different ways to help you extend the test context. 130 + Vitest allows you to extend the test context with custom fixtures using `test.extend`. 131 131 132 - ### `test.extend` 132 + 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). 133 133 134 - 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. 134 + ### Builder Pattern <Version>4.1.0</Version> {#builder-pattern} 135 135 136 - For example, we first create the `test` collector with two fixtures: `todos` and `archive`. 136 + 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. 137 137 138 138 ```ts [my-test.ts] 139 139 import { test as baseTest } from 'vitest' 140 140 141 - const todos = [] 142 - const archive = [] 143 - 144 - export const test = baseTest.extend({ 145 - todos: async ({}, use) => { 146 - // setup the fixture before each test function 147 - todos.push(1, 2, 3) 148 - 149 - // use the fixture value 150 - await use(todos) 151 - 152 - // cleanup the fixture after each test function 153 - todos.length = 0 154 - }, 155 - archive 156 - }) 141 + export const test = baseTest 142 + // Simple value - type is inferred as { port: number; host: string } 143 + .extend('config', { port: 3000, host: 'localhost' }) 144 + // Function fixture - type is inferred from return value 145 + .extend('server', async ({ config }) => { 146 + // TypeScript knows config is { port: number; host: string } 147 + return `http://${config.host}:${config.port}` 148 + }) 157 149 ``` 158 150 159 - Then we can import and use it. 151 + Then use it in your tests: 160 152 161 153 ```ts [my-test.test.ts] 162 154 import { expect } from 'vitest' 163 155 import { test } from './my-test.js' 164 156 165 - test('add items to todos', ({ todos }) => { 166 - expect(todos.length).toBe(3) 167 - 168 - todos.push(4) 169 - expect(todos.length).toBe(4) 170 - }) 171 - 172 - test('move items from todos to archive', ({ todos, archive }) => { 173 - expect(todos.length).toBe(3) 174 - expect(archive.length).toBe(0) 175 - 176 - archive.push(todos.pop()) 177 - expect(todos.length).toBe(2) 178 - expect(archive.length).toBe(1) 157 + test('server uses correct port', ({ config, server }) => { 158 + // TypeScript knows the types: 159 + // - config is { port: number; host: string } 160 + // - server is string 161 + expect(server).toBe('http://localhost:3000') 162 + expect(config.port).toBe(3000) 179 163 }) 180 164 ``` 181 165 182 - We can also add more fixtures or override existing fixtures by extending our `test`. 166 + #### Setup and Cleanup with `onCleanup` 167 + 168 + 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: 183 169 184 170 ```ts 185 - import { test as todosTest } from './my-test.js' 171 + import { test as baseTest } from 'vitest' 186 172 187 - export const test = todosTest.extend({ 188 - settings: { 189 - // ... 190 - } 173 + export const test = baseTest 174 + .extend('tempFile', async ({}, { onCleanup }) => { 175 + const filePath = `/tmp/test-${Date.now()}.txt` 176 + await fs.writeFile(filePath, 'test data') 177 + 178 + // Register cleanup - runs after test completes 179 + onCleanup(async () => { 180 + await fs.unlink(filePath) 181 + }) 182 + 183 + return filePath 184 + }) 185 + ``` 186 + 187 + For more complex examples: 188 + 189 + ```ts 190 + const test = baseTest 191 + .extend('database', { scope: 'file' }, async ({}, { onCleanup }) => { 192 + const db = await createDatabase() 193 + await db.connect() 194 + 195 + onCleanup(async () => { 196 + await db.disconnect() 197 + }) 198 + 199 + return db 200 + }) 201 + .extend('user', async ({ database }, { onCleanup }) => { 202 + const user = await database.createTestUser() 203 + 204 + onCleanup(async () => { 205 + await database.deleteUser(user.id) 206 + }) 207 + 208 + return user 209 + }) 210 + ``` 211 + 212 + ::: warning 213 + 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: 214 + 215 + ```ts 216 + // ❌ This will throw an error 217 + const test = baseTest 218 + .extend('resources', async ({}, { onCleanup }) => { 219 + const a = await acquireA() 220 + onCleanup(() => releaseA(a)) 221 + 222 + const b = await acquireB() 223 + onCleanup(() => releaseB(b)) // Error: onCleanup can only be called once 224 + 225 + return { a, b } 226 + }) 227 + 228 + // ✅ Split into separate fixtures (recommended) 229 + const test = baseTest 230 + .extend('resourceA', async ({}, { onCleanup }) => { 231 + const a = await acquireA() 232 + onCleanup(() => releaseA(a)) 233 + return a 234 + }) 235 + .extend('resourceB', async ({}, { onCleanup }) => { 236 + const b = await acquireB() 237 + onCleanup(() => releaseB(b)) 238 + return b 239 + }) 240 + ``` 241 + 242 + Splitting into separate fixtures is the recommended approach as it provides better isolation and makes dependencies explicit. 243 + ::: 244 + 245 + #### Fixture Options 246 + 247 + The second argument to `.extend()` accepts options: 248 + 249 + ```ts 250 + const test = baseTest 251 + // Automatic fixture - runs for every test even if not used 252 + .extend('metrics', { auto: true }, ({}, { onCleanup }) => { 253 + const metrics = new MetricsCollector() 254 + metrics.start() 255 + onCleanup(() => metrics.stop()) 256 + return metrics 257 + }) 258 + // Worker-scoped fixture - initialized once per worker 259 + .extend('config', { scope: 'worker' }, () => { 260 + return loadConfig() 261 + }) 262 + // File-scoped fixture - initialized once per file 263 + .extend('database', { scope: 'file' }, async ({ config }, { onCleanup }) => { 264 + const db = await createDatabase(config) 265 + onCleanup(() => db.close()) 266 + return db 267 + }) 268 + // Injected fixture - can be overridden via config 269 + .extend('baseUrl', { injected: true }, () => { 270 + return 'http://localhost:3000' 271 + }) 272 + ``` 273 + 274 + For test-scoped fixtures (the default), you can omit the options: 275 + 276 + ```ts 277 + const test = baseTest 278 + .extend('simple', () => 'value') 279 + ``` 280 + 281 + Non-function values only support the `injected` option: 282 + 283 + ```ts 284 + const test = baseTest 285 + .extend('baseUrl', { injected: true }, 'http://localhost:3000') 286 + .extend('defaults', { port: 3000, host: 'localhost' }) 287 + ``` 288 + 289 + #### Accessing Other Fixtures 290 + 291 + Each fixture can access previously defined fixtures via its first parameter. This works for both function and non-function fixtures: 292 + 293 + ```ts 294 + const test = baseTest 295 + .extend('config', { apiUrl: 'https://api.example.com', port: 3000 }) 296 + .extend('client', ({ config }) => { 297 + // TypeScript knows config is { apiUrl: string; port: number } 298 + return new ApiClient(config.apiUrl) 299 + }) 300 + .extend('user', async ({ client }) => { 301 + // TypeScript knows client is ApiClient 302 + return await client.getCurrentUser() 303 + }) 304 + ``` 305 + 306 + #### Object Syntax (Playwright-Compatible) 307 + 308 + Vitest also supports a Playwright-compatible object syntax. This is useful if you're migrating from Playwright or prefer defining all fixtures at once: 309 + 310 + ```ts [my-test.ts] 311 + import { test as baseTest } from 'vitest' 312 + 313 + export const test = baseTest.extend({ 314 + page: async ({}, use) => { 315 + // setup the fixture before each test function 316 + const page = await browser.newPage() 317 + 318 + // use the fixture value 319 + await use(page) 320 + 321 + // cleanup the fixture after each test function 322 + await page.close() 323 + }, 324 + baseUrl: 'http://localhost:3000' 191 325 }) 192 326 ``` 193 327 194 - #### Fixture initialization 328 + The key difference from the builder pattern is the `use()` callback pattern for cleanup: 329 + 330 + ```ts 331 + // Object syntax: cleanup code goes AFTER use() 332 + const test = baseTest.extend({ 333 + database: async ({}, use) => { 334 + const db = await createDatabase() 335 + await db.connect() 336 + 337 + await use(db) // Test runs here 338 + 339 + // Cleanup after the test 340 + await db.disconnect() 341 + } 342 + }) 343 + 344 + // Builder pattern: cleanup is registered with onCleanup() 345 + const test = baseTest 346 + .extend('database', async ({}, { onCleanup }) => { 347 + const db = await createDatabase() 348 + await db.connect() 349 + 350 + onCleanup(() => db.disconnect()) 351 + 352 + return db // Test runs after this returns 353 + }) 354 + ``` 355 + 356 + ::: info 357 + With the object syntax, you need to provide types manually as a generic parameter since TypeScript cannot infer them from the `use()` callback: 358 + 359 + ```ts 360 + const test = baseTest.extend<{ 361 + page: Page 362 + baseUrl: string 363 + }>({ 364 + page: async ({}, use) => { 365 + const page = await browser.newPage() 366 + await use(page) 367 + await page.close() 368 + }, 369 + baseUrl: 'http://localhost:3000' 370 + }) 371 + ``` 372 + ::: 373 + 374 + #### Tuple Syntax for Options 375 + 376 + With the object syntax, use a tuple to specify fixture options: 377 + 378 + ```ts 379 + const test = baseTest.extend({ 380 + // Auto fixture 381 + fixture: [ 382 + async ({}, use) => { 383 + setup() 384 + await use() 385 + teardown() 386 + }, 387 + { auto: true } 388 + ], 389 + // Scoped fixture 390 + database: [ 391 + async ({}, use) => { 392 + const db = await createDatabase() 393 + await use(db) 394 + await db.close() 395 + }, 396 + { scope: 'file' } 397 + ], 398 + // Injected fixture 399 + url: [ 400 + '/default', 401 + { injected: true } 402 + ], 403 + }) 404 + ``` 405 + 406 + ### Fixture Initialization 195 407 196 408 Vitest runner will smartly initialize your fixtures and inject them into the test context based on usage. 197 409 198 410 ```ts 199 411 import { test as baseTest } from 'vitest' 200 412 201 - const test = baseTest.extend<{ 202 - todos: number[] 203 - archive: number[] 204 - }>({ 205 - todos: async ({ task }, use) => { 206 - await use([1, 2, 3]) 207 - }, 208 - archive: [] 209 - }) 413 + const test = baseTest 414 + .extend('database', async () => { 415 + console.log('database initializing') 416 + return createDatabase() 417 + }) 418 + .extend('cache', async () => { 419 + return createCache() 420 + }) 210 421 211 - // todos will not run 212 - test('skip', () => {}) 213 - test('skip', ({ archive }) => {}) 422 + // database will not run 423 + test('no fixtures needed', () => {}) 424 + test('only cache', ({ cache }) => {}) 214 425 215 - // todos will run 216 - test('run', ({ todos }) => {}) 426 + // database will run 427 + test('needs database', ({ database }) => {}) 217 428 ``` 218 429 219 430 ::: warning 220 - 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. 431 + 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. 221 432 222 433 ```ts 223 434 test('context must be destructured', (context) => { // [!code --] 224 - expect(context.todos.length).toBe(2) 435 + expect(context.database).toBeDefined() 225 436 }) 226 437 227 - test('context must be destructured', ({ todos }) => { // [!code ++] 228 - expect(todos.length).toBe(2) 438 + test('context must be destructured', ({ database }) => { // [!code ++] 439 + expect(database).toBeDefined() 229 440 }) 230 441 ``` 231 - 232 442 ::: 233 443 234 - #### Automatic fixture 444 + ### Extending Extended Tests 235 445 236 - 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. 446 + You can extend an already extended test to add more fixtures: 237 447 238 448 ```ts 239 - import { test as base } from 'vitest' 449 + import { test as dbTest } from './my-test.js' 240 450 241 - const test = base.extend({ 242 - fixture: [ 243 - async ({}, use) => { 244 - // this function will run 245 - setup() 246 - await use() 247 - teardown() 248 - }, 249 - { auto: true } // Mark as an automatic fixture 250 - ], 251 - }) 252 - 253 - test('works correctly') 451 + export const test = dbTest 452 + .extend('user', ({ database }) => { 453 + return database.createUser() 454 + }) 254 455 ``` 255 456 256 - #### Default fixture 457 + With the object syntax: 257 458 258 - 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. 459 + ```ts 460 + import { test as dbTest } from './my-test.js' 461 + 462 + export const test = dbTest.extend({ 463 + admin: async ({ database }, use) => { 464 + const admin = await database.createAdmin() 465 + await use(admin) 466 + await database.deleteUser(admin.id) 467 + } 468 + }) 469 + ``` 470 + 471 + ### Mixing Both Syntaxes 472 + 473 + You can combine both approaches. The builder pattern can be chained after object-based extensions: 474 + 475 + ```ts 476 + const test = baseTest 477 + // Object syntax for simple fixtures 478 + .extend<{ apiKey: string }>({ 479 + apiKey: 'test-key-123', 480 + }) 481 + // Builder pattern for complex fixtures with inference 482 + .extend('client', ({ apiKey }) => { 483 + // TypeScript knows apiKey is string 484 + return new ApiClient(apiKey) 485 + }) 486 + ``` 487 + 488 + ### Fixture Scopes <Version>3.2.0</Version> {#fixture-scopes} 489 + 490 + By default, fixtures are initialized for each test. You can change this with the `scope` option to share fixtures across tests. 491 + 492 + ::: warning 493 + 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: 494 + 495 + ```ts 496 + test 497 + .extend('port', { scope: 'worker' }, 5000) 498 + .extend('db', { scope: 'worker' }, async ({ port }) => { 499 + return createDb(port) 500 + }) 501 + ``` 502 + 503 + Note that you cannot override non-test fixtures inside `describe` blocks: 504 + 505 + ```ts 506 + test.describe('a nested suite', () => { 507 + test.override('port', 3000) // throws an error 508 + }) 509 + ``` 510 + 511 + 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. 512 + 513 + 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. 514 + 515 + <!-- TODO(v5) should this be addressed? force a new worker if worker fixture is overriden? --> 516 + ::: 517 + 518 + #### Test Scope (Default) 519 + 520 + Test-scoped fixtures are created fresh for each test: 521 + 522 + ```ts 523 + const test = baseTest 524 + .extend('counter', () => { 525 + return { value: 0 } 526 + }) 527 + 528 + test('first test', ({ counter }) => { 529 + counter.value++ 530 + expect(counter.value).toBe(1) 531 + }) 532 + 533 + test('second test', ({ counter }) => { 534 + // Fresh instance, value is 0 again 535 + expect(counter.value).toBe(0) 536 + }) 537 + ``` 538 + 539 + Test-scoped fixtures have access to the [built-in test context](#built-in-test-context) (`task`, `expect`, `skip`, etc.): 540 + 541 + ```ts 542 + const test = baseTest 543 + .extend('testInfo', ({ task }) => { 544 + return { name: task.name } 545 + }) 546 + ``` 547 + 548 + #### File Scope 549 + 550 + File-scoped fixtures are initialized once per test file: 551 + 552 + ```ts 553 + const test = baseTest 554 + .extend('database', { scope: 'file' }, async ({}, { onCleanup }) => { 555 + const db = await createDatabase() 556 + onCleanup(() => db.close()) 557 + return db 558 + }) 559 + 560 + test('first test', ({ database }) => { 561 + // Uses the same database instance 562 + }) 563 + 564 + test('second test', ({ database }) => { 565 + // Same database instance as first test 566 + }) 567 + ``` 568 + 569 + #### Worker Scope 570 + 571 + Worker-scoped fixtures are initialized once per worker process: 572 + 573 + ```ts 574 + const test = baseTest 575 + .extend('config', { scope: 'worker' }, () => { 576 + return await loadExpensiveConfig() 577 + }) 578 + ``` 579 + 580 + ::: info 581 + 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. 582 + 583 + When running tests in `vmThreads` or `vmForks`, `scope: 'worker'` works the same way as `scope: 'file'` because each file has its own VM context. 584 + ::: 585 + 586 + #### Scope Hierarchy 587 + 588 + Fixtures can only access other fixtures from the same or higher (longer-lived) scopes: 589 + 590 + | Fixture Scope | Can Access | 591 + |---------------|------------| 592 + | `worker` | Only other worker fixtures | 593 + | `file` | Worker + file fixtures | 594 + | `test` | Worker + file + test fixtures + [test context](#built-in-test-context) | 595 + 596 + ```ts 597 + const test = baseTest 598 + .extend('config', { scope: 'worker' }, () => { 599 + return { apiUrl: 'https://api.example.com' } 600 + }) 601 + .extend('database', { scope: 'file' }, async ({ config }, { onCleanup }) => { 602 + // ✅ File fixture can access worker fixture 603 + const db = await createDatabase(config.apiUrl) 604 + onCleanup(() => db.close()) 605 + return db 606 + }) 607 + .extend('user', async ({ database, task }) => { 608 + // ✅ Test fixture can access file fixture AND test context 609 + return await database.createUser(task.name) 610 + }) 611 + ``` 612 + 613 + ::: tip 614 + 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. 615 + 616 + If you need the file path in a file-scoped fixture, use `expect.getState().testPath` instead. 617 + ::: 618 + 619 + #### Type-Safe Scope Access <Version>3.2.0</Version> {#type-safe-scope-access} 620 + 621 + 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. 622 + 623 + 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: 624 + 625 + ```ts 626 + const test = baseTest.extend<{ 627 + $worker: { config: Config } 628 + $file: { database: Database } 629 + $test: { user: User } 630 + }>({ 631 + config: [async ({}, use) => { 632 + await use(loadConfig()) 633 + }, { scope: 'worker' }], 634 + 635 + database: [async ({ config }, use) => { 636 + const db = await createDatabase(config) 637 + await use(db) 638 + await db.close() 639 + }, { scope: 'file' }], 640 + 641 + user: async ({ database }, use) => { 642 + const user = await database.createUser() 643 + await use(user) 644 + await database.deleteUser(user.id) 645 + }, 646 + }) 647 + ``` 648 + 649 + This provides the same compile-time safety as the builder pattern, catching scope violations at build time rather than runtime. 650 + 651 + ### Default Fixture (Injected) 652 + 653 + 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. 259 654 260 655 :::code-group 261 656 ```ts [fixtures.test.ts] 262 - import { test as base } from 'vitest' 657 + import { test as baseTest } from 'vitest' 263 658 264 - const test = base.extend({ 265 - url: [ 266 - // default value if "url" is not defined in the config 267 - '/default', 268 - // mark the fixture as "injected" to allow the override 269 - { injected: true }, 270 - ], 271 - }) 659 + const test = baseTest 660 + .extend('url', { injected: true }, '/default') 272 661 273 662 test('works correctly', ({ url }) => { 274 663 // url is "/default" in "project-new" ··· 309 698 ``` 310 699 ::: 311 700 312 - #### Scoping Values to Suite <Version>3.1.0</Version> {#scoping-values-to-suite} 701 + ### Overriding Fixture Values <Version>4.1.0</Version> {#overriding-fixture-values} 313 702 314 - Since Vitest 3.1, you can override context values per suite and its children by using the `test.scoped` API: 703 + 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. 704 + 705 + ::: tip 706 + Vitest will automatically inherit the options, if they are not provided when overriding. Note that you cannot override fixture's `scope` or `auto` options. 707 + ::: 708 + 709 + #### Builder Pattern (Recommended) 315 710 316 711 ```ts 317 712 import { test as baseTest, describe, expect } from 'vitest' 318 713 319 - const test = baseTest.extend({ 320 - dependency: 'default', 321 - dependant: ({ dependency }, use) => use({ dependency }) 714 + const test = baseTest 715 + .extend('config', { port: 3000, host: 'localhost' }) 716 + .extend('server', ({ config }) => `http://${config.host}:${config.port}`) 717 + 718 + describe('production environment', () => { 719 + // Override with a new static value (chainable) 720 + test 721 + .override('config', { port: 8080, host: 'api.example.com' }) 722 + 723 + test('uses production config', ({ server }) => { 724 + expect(server).toBe('http://api.example.com:8080') 725 + }) 322 726 }) 323 727 324 - describe('use scoped values', () => { 325 - test.scoped({ dependency: 'new' }) 326 - 327 - test('uses scoped value', ({ dependant }) => { 328 - // `dependant` uses the new overridden value that is scoped 329 - // to all tests in this suite 330 - expect(dependant).toEqual({ dependency: 'new' }) 728 + describe('with custom server', () => { 729 + // Override with a function that can access other fixtures 730 + test.override('server', ({ config }) => { 731 + return `https://${config.host}:${config.port}/v2` 331 732 }) 332 733 333 - describe('keeps using scoped value', () => { 334 - test('uses scoped value', ({ dependant }) => { 335 - // nested suite inherited the value 336 - expect(dependant).toEqual({ dependency: 'new' }) 734 + test('uses custom server', ({ server }) => { 735 + expect(server).toBe('https://localhost:3000/v2') 736 + }) 737 + }) 738 + 739 + test('uses default values', ({ server }) => { 740 + expect(server).toBe('http://localhost:3000') 741 + }) 742 + ``` 743 + 744 + #### Chaining Multiple Overrides 745 + 746 + `test.override` returns the test API, so you can chain multiple calls: 747 + 748 + ```ts 749 + describe('production environment', () => { 750 + test 751 + .override('environment', 'production') 752 + .override('port', 8080) 753 + .override('debug', false) 754 + 755 + test('uses production settings', ({ environment, port, debug }) => { 756 + expect(environment).toBe('production') 757 + expect(port).toBe(8080) 758 + expect(debug).toBe(false) 759 + }) 760 + }) 761 + ``` 762 + 763 + #### Object Syntax 764 + 765 + You can also use object syntax to override multiple fixtures at once: 766 + 767 + ```ts 768 + describe('different configuration', () => { 769 + test.override({ 770 + config: { port: 4000, host: 'test.local' }, 771 + }) 772 + 773 + test('uses overwritten config', ({ config }) => { 774 + expect(config.port).toBe(4000) 775 + }) 776 + }) 777 + ``` 778 + 779 + #### With Cleanup 780 + 781 + When overwriting with a function, you can use `onCleanup` just like in `test.extend`: 782 + 783 + ```ts 784 + describe('with custom database', () => { 785 + test.override('database', async ({ config }, { onCleanup }) => { 786 + const db = await createTestDatabase(config) 787 + onCleanup(() => db.drop()) 788 + return db 789 + }) 790 + 791 + test('uses custom database', ({ database }) => { 792 + // Uses the overwritten database 793 + }) 794 + }) 795 + ``` 796 + 797 + #### Nested Scopes 798 + 799 + Overrides are inherited by nested suites and can be overwritten again: 800 + 801 + ```ts 802 + describe('level 1', () => { 803 + test.override('value', 'one') 804 + 805 + test('uses level 1 value', ({ value }) => { 806 + expect(value).toBe('one') 807 + }) 808 + 809 + describe('level 2', () => { 810 + test.override('value', 'two') 811 + 812 + test('uses level 2 value', ({ value }) => { 813 + expect(value).toBe('two') 337 814 }) 338 815 }) 339 - }) 340 816 341 - test('keep using the default values', ({ dependant }) => { 342 - // the `dependency` is using the default 343 - // value outside of the suite with .scoped 344 - expect(dependant).toEqual({ dependency: 'default' }) 345 - }) 346 - ``` 347 - 348 - This API is particularly useful if you have a context value that relies on a dynamic variable like a database connection: 349 - 350 - ```ts 351 - const test = baseTest.extend<{ 352 - db: Database 353 - schema: string 354 - }>({ 355 - db: async ({ schema }, use) => { 356 - const db = await createDb({ schema }) 357 - await use(db) 358 - await cleanup(db) 359 - }, 360 - schema: '', 361 - }) 362 - 363 - describe('one type of schema', () => { 364 - test.scoped({ schema: 'schema-1' }) 365 - 366 - // ... tests 367 - }) 368 - 369 - describe('another type of schema', () => { 370 - test.scoped({ schema: 'schema-2' }) 371 - 372 - // ... tests 373 - }) 374 - ``` 375 - 376 - #### Per-Scope Context <Version>3.2.0</Version> 377 - 378 - 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: 379 - 380 - ```ts 381 - import { test as baseTest } from 'vitest' 382 - 383 - export const test = baseTest.extend({ 384 - perFile: [ 385 - ({}, use) => use([]), 386 - { scope: 'file' }, 387 - ], 388 - perWorker: [ 389 - ({}, use) => use([]), 390 - { scope: 'worker' }, 391 - ], 392 - }) 393 - ``` 394 - 395 - 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. 396 - 397 - ```ts 398 - const test = baseTest.extend({ 399 - perFile: [ 400 - ({}, use) => use([]), 401 - { 402 - scope: 'file', 403 - // always run this hook before any test 404 - auto: true 405 - }, 406 - ], 817 + test('still uses level 1 value', ({ value }) => { 818 + expect(value).toBe('one') 819 + }) 407 820 }) 408 821 ``` 409 822 410 823 ::: warning 411 - 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`. 412 - 413 - If you need access to file-level metadata like the file path, use `expect.getState().testPath` instead. 824 + Note that you cannot introduce new fixtures inside `test.override`. Extend the test context with `test.extend` instead. 414 825 ::: 415 826 416 - 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. 417 - 418 - However, if you disable [isolation](/config/#isolate), then the number of workers is limited by the [`maxWorkers`](/config/#maxworkers) configuration. 419 - 420 - 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). 421 - 422 - #### TypeScript 423 - 424 - To provide fixture types for all your custom contexts, you can pass the fixtures type as a generic. 425 - 426 - ```ts 427 - interface MyFixtures { 428 - todos: number[] 429 - archive: number[] 430 - } 431 - 432 - const test = baseTest.extend<MyFixtures>({ 433 - todos: [], 434 - archive: [] 435 - }) 436 - 437 - test('types are defined correctly', ({ todos, archive }) => { 438 - expectTypeOf(todos).toEqualTypeOf<number[]>() 439 - expectTypeOf(archive).toEqualTypeOf<number[]>() 440 - }) 441 - ``` 442 - 443 - ::: info Type Inferring 444 - 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: 445 - 446 - ```ts 447 - import { test as baseTest } from 'vitest' 448 - 449 - const test = baseTest.extend<{ 450 - todos: number[] 451 - schema: string 452 - }>({ 453 - todos: ({ schema }, use) => use([]), 454 - schema: 'test' 455 - }) 456 - 457 - test('types are correct', ({ 458 - todos, // number[] 459 - schema, // string 460 - }) => { 461 - // ... 462 - }) 463 - ``` 464 - 827 + ::: info 828 + `test.scoped` is deprecated in favor of `test.override`. The `test.scoped` API still works but will be removed in a future version. 465 829 ::: 830 + 831 + ### Type-Safe Hooks 466 832 467 833 When using `test.extend`, the extended `test` object provides type-safe `beforeEach` and `afterEach` hooks that are aware of the new context: 468 834 469 835 ```ts 470 - const test = baseTest.extend<{ 471 - todos: number[] 472 - }>({ 473 - todos: async ({}, use) => { 474 - await use([]) 475 - }, 476 - }) 836 + const test = baseTest 837 + .extend('counter', { value: 0, increment() { this.value++ } }) 477 838 478 839 // Unlike global hooks, these hooks are aware of the extended context 479 - test.beforeEach(({ todos }) => { 480 - todos.push(1) 840 + test.beforeEach(({ counter }) => { 841 + counter.increment() 481 842 }) 482 843 483 - test.afterEach(({ todos }) => { 484 - console.log(todos) 844 + test.afterEach(({ counter }) => { 845 + console.log('Final count:', counter.value) 485 846 }) 486 847 ```
+1 -1
packages/expect/src/utils.ts
··· 49 49 const stack = processor(error.stack) 50 50 console.warn([ 51 51 `Promise returned by \`${assertion}\` was not awaited. `, 52 - 'Vitest currently auto-awaits hanging assertions at the end of the test, but this will cause the test to fail in Vitest 3. ', 52 + '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. ', 53 53 'Please remember to await the assertion.\n', 54 54 stack, 55 55 ].join(''))
+5 -4
packages/runner/src/collect.ts
··· 2 2 import type { File, SuiteHooks } from './types/tasks' 3 3 import { processError } from '@vitest/utils/error' // TODO: load dynamically 4 4 import { toArray } from '@vitest/utils/helpers' 5 - import { collectorContext, setFileContext } from './context' 5 + import { collectorContext } from './context' 6 6 import { getHooks, setHooks } from './map' 7 7 import { runSetupFiles } from './setup' 8 8 import { ··· 46 46 const fileTags: string[] = typeof spec === 'string' ? [] : (spec.fileTags || []) 47 47 48 48 const file = createFileTask(filepath, config.root, config.name, runner.pool, runner.viteEnvironment) 49 - setFileContext(file, Object.create(null)) 50 49 file.tags = fileTags 51 50 file.shuffle = config.sequence.shuffle 52 51 ··· 103 102 file.collectDuration = now() - collectStart 104 103 } 105 104 catch (e) { 106 - const error = processError(e) 105 + const errors = e instanceof AggregateError 106 + ? e.errors.map(e => processError(e, runner.config.diffOptions)) 107 + : [processError(e, runner.config.diffOptions)] 107 108 file.result = { 108 109 state: 'fail', 109 - errors: [error], 110 + errors, 110 111 } 111 112 112 113 const durations = runner.getImportDurations?.()
-15
packages/runner/src/context.ts
··· 1 1 import type { Awaitable } from '@vitest/utils' 2 2 import type { VitestRunner } from './types/runner' 3 3 import type { 4 - File, 5 4 RuntimeContext, 6 5 SuiteCollector, 7 6 Test, ··· 230 229 error.stack = stackTraceError.stack.replace(error.message, stackTraceError.message) 231 230 } 232 231 return error 233 - } 234 - 235 - const fileContexts = new WeakMap<File, Record<string, unknown>>() 236 - 237 - export function getFileContext(file: File): Record<string, unknown> { 238 - const context = fileContexts.get(file) 239 - if (!context) { 240 - throw new Error(`Cannot find file context for ${file.name}`) 241 - } 242 - return context 243 - } 244 - 245 - export function setFileContext(file: File, context: Record<string, unknown>): void { 246 - fileContexts.set(file, context) 247 232 }
+4
packages/runner/src/errors.ts
··· 20 20 } 21 21 } 22 22 23 + export class FixtureDependencyError extends Error { 24 + public name = 'FixtureDependencyError' 25 + } 26 + 23 27 export class AroundHookSetupError extends Error { 24 28 public name = 'AroundHookSetupError' 25 29 }
+337 -190
packages/runner/src/fixture.ts
··· 1 - import type { VitestRunner } from './types' 2 - import type { FixtureOptions, TestContext } from './types/tasks' 1 + import type { FixtureFn, Suite, VitestRunner } from './types' 2 + import type { File, FixtureOptions, TestContext } from './types/tasks' 3 3 import { createDefer, filterOutComments, isObject } from '@vitest/utils/helpers' 4 - import { getFileContext } from './context' 5 - import { getTestFixture } from './map' 4 + import { FixtureDependencyError } from './errors' 5 + import { getTestFixtures } from './map' 6 + import { getCurrentSuite } from './suite' 6 7 7 - export interface FixtureItem extends FixtureOptions { 8 - prop: string 9 - value: any 8 + export interface TestFixtureItem extends FixtureOptions { 9 + name: string 10 + value: unknown 10 11 scope: 'test' | 'file' | 'worker' 11 - /** 12 - * Indicates whether the fixture is a function 13 - */ 14 - isFn: boolean 15 - /** 16 - * The dependencies(fixtures) of current fixture function. 17 - */ 18 - deps?: FixtureItem[] 12 + deps: Set<string> 13 + // so it's possible to call base fixture inside ({ a: ({ a }, use) => {} }) 14 + parent?: TestFixtureItem 19 15 } 20 16 21 - export function mergeScopedFixtures( 22 - testFixtures: FixtureItem[], 23 - scopedFixtures: FixtureItem[], 24 - ): FixtureItem[] { 25 - const scopedFixturesMap = scopedFixtures.reduce<Record<string, FixtureItem>>((map, fixture) => { 26 - map[fixture.prop] = fixture 27 - return map 28 - }, {}) 29 - const newFixtures: Record<string, FixtureItem> = {} 30 - testFixtures.forEach((fixture) => { 31 - const useFixture = scopedFixturesMap[fixture.prop] || { 32 - // we need to clone the fixture because we override its values 33 - ...fixture, 34 - } 35 - newFixtures[useFixture.prop] = useFixture 36 - }) 37 - for (const fixtureKep in newFixtures) { 38 - const fixture = newFixtures[fixtureKep] 39 - // if the fixture was define before the scope, then its dep 40 - // will reference the original fixture instead of the scope 41 - fixture.deps = fixture.deps?.map(dep => newFixtures[dep.prop]) 17 + export type UserFixtures = Record<string, unknown> 18 + export type FixtureRegistrations = Map<string, TestFixtureItem> 19 + 20 + export class TestFixtures { 21 + private _suiteContexts: WeakMap<Suite | symbol, /* context object */ Record<string, unknown>> 22 + private _overrides = new WeakMap<Suite, FixtureRegistrations>() 23 + private _registrations: FixtureRegistrations 24 + 25 + private static _definitions: TestFixtures[] = [] 26 + private static _builtinFixtures: string[] = [ 27 + 'task', 28 + 'signal', 29 + 'onTestFailed', 30 + 'onTestFinished', 31 + 'skip', 32 + 'annotate', 33 + ] satisfies (keyof TestContext)[] 34 + 35 + private static _fixtureOptionKeys: string[] = ['auto', 'injected', 'scope'] 36 + private static _fixtureScopes: string[] = ['test', 'file', 'worker'] 37 + private static _workerContextSymbol = Symbol('workerContext') 38 + 39 + static clearDefinitions(): void { 40 + TestFixtures._definitions.length = 0 42 41 } 43 - return Object.values(newFixtures) 44 - } 45 42 46 - export function mergeContextFixtures<T extends { fixtures?: FixtureItem[] }>( 47 - fixtures: Record<string, any>, 48 - context: T, 49 - runner: VitestRunner, 50 - ): T { 51 - const fixtureOptionKeys = ['auto', 'injected', 'scope'] 52 - const fixtureArray: FixtureItem[] = Object.entries(fixtures).map( 53 - ([prop, value]) => { 54 - const fixtureItem = { value } as FixtureItem 43 + static getWorkerContexts(): Record<string, any>[] { 44 + return TestFixtures._definitions.map(f => f.getWorkerContext()) 45 + } 46 + 47 + static getFileContexts(file: File): Record<string, any>[] { 48 + return TestFixtures._definitions.map(f => f.getFileContext(file)) 49 + } 50 + 51 + constructor(registrations?: FixtureRegistrations) { 52 + this._registrations = registrations ?? new Map() 53 + this._suiteContexts = new WeakMap() 54 + TestFixtures._definitions.push(this) 55 + } 56 + 57 + extend(runner: VitestRunner, userFixtures: UserFixtures): TestFixtures { 58 + const { suite } = getCurrentSuite() 59 + const isTopLevel = !suite || suite.file === suite 60 + const registrations = this.parseUserFixtures(runner, userFixtures, isTopLevel) 61 + return new TestFixtures(registrations) 62 + } 63 + 64 + get(suite: Suite): FixtureRegistrations { 65 + let currentSuite: Suite | undefined = suite 66 + while (currentSuite) { 67 + const overrides = this._overrides.get(currentSuite) 68 + // return the closest override 69 + if (overrides) { 70 + return overrides 71 + } 72 + if (currentSuite === currentSuite.file) { 73 + break 74 + } 75 + currentSuite = currentSuite.suite || currentSuite.file 76 + } 77 + return this._registrations 78 + } 79 + 80 + override(runner: VitestRunner, userFixtures: UserFixtures): void { 81 + const { suite: currentSuite, file } = getCurrentSuite() 82 + const suite = currentSuite || file 83 + const isTopLevel = !currentSuite || currentSuite.file === currentSuite 84 + // Create a copy of the closest parent's registrations to avoid modifying them 85 + // For chained calls, this.get(suite) returns this suite's overrides; for first call, returns parent's 86 + const suiteRegistrations = new Map(this.get(suite)) 87 + const registrations = this.parseUserFixtures(runner, userFixtures, isTopLevel, suiteRegistrations) 88 + // If defined in top-level, just override all registrations 89 + // We don't support overriding suite-level fixtures anyway (it will throw an error) 90 + if (isTopLevel) { 91 + this._registrations = registrations 92 + } 93 + else { 94 + this._overrides.set(suite, registrations) 95 + } 96 + } 97 + 98 + getFileContext(file: File): Record<string, any> { 99 + if (!this._suiteContexts.has(file)) { 100 + this._suiteContexts.set(file, Object.create(null)) 101 + } 102 + return this._suiteContexts.get(file)! 103 + } 104 + 105 + getWorkerContext(): Record<string, any> { 106 + if (!this._suiteContexts.has(TestFixtures._workerContextSymbol)) { 107 + this._suiteContexts.set(TestFixtures._workerContextSymbol, Object.create(null)) 108 + } 109 + return this._suiteContexts.get(TestFixtures._workerContextSymbol)! 110 + } 111 + 112 + private parseUserFixtures( 113 + runner: VitestRunner, 114 + userFixtures: UserFixtures, 115 + supportNonTest: boolean, 116 + registrations = new Map<string, TestFixtureItem>(this._registrations), 117 + ) { 118 + const errors: Error[] = [] 119 + 120 + Object.entries(userFixtures).forEach(([name, fn]) => { 121 + let options: FixtureOptions | undefined 122 + let value: unknown | undefined 123 + let _options: FixtureOptions | undefined 55 124 56 125 if ( 57 - Array.isArray(value) 58 - && value.length >= 2 59 - && isObject(value[1]) 60 - && Object.keys(value[1]).some(key => fixtureOptionKeys.includes(key)) 126 + Array.isArray(fn) 127 + && fn.length >= 2 128 + && isObject(fn[1]) 129 + && Object.keys(fn[1]).some(key => TestFixtures._fixtureOptionKeys.includes(key)) 61 130 ) { 62 - // fixture with options 63 - Object.assign(fixtureItem, value[1]) 64 - const userValue = value[0] 65 - fixtureItem.value = fixtureItem.injected 66 - ? (runner.injectValue?.(prop) ?? userValue) 67 - : userValue 131 + _options = fn[1] as FixtureOptions 132 + options = { 133 + auto: _options.auto ?? false, 134 + scope: _options.scope ?? 'test', 135 + injected: _options.injected ?? false, 136 + } 137 + value = options.injected 138 + ? (runner.injectValue?.(name) ?? fn[0]) 139 + : fn[0] 140 + } 141 + else { 142 + value = fn 68 143 } 69 144 70 - fixtureItem.scope = fixtureItem.scope || 'test' 71 - if (fixtureItem.scope === 'worker' && !runner.getWorkerContext) { 72 - fixtureItem.scope = 'file' 145 + const parent = registrations.get(name) 146 + if (parent && options) { 147 + if (parent.scope !== options.scope) { 148 + errors.push(new FixtureDependencyError(`The "${name}" fixture was already registered with a "${options.scope}" scope.`)) 149 + } 150 + if (parent.auto !== options.auto) { 151 + errors.push(new FixtureDependencyError(`The "${name}" fixture was already registered as { auto: ${options.auto} }.`)) 152 + } 73 153 } 74 - fixtureItem.prop = prop 75 - fixtureItem.isFn = typeof fixtureItem.value === 'function' 76 - return fixtureItem 77 - }, 78 - ) 79 - 80 - if (Array.isArray(context.fixtures)) { 81 - context.fixtures = context.fixtures.concat(fixtureArray) 82 - } 83 - else { 84 - context.fixtures = fixtureArray 85 - } 86 - 87 - // Update dependencies of fixture functions 88 - fixtureArray.forEach((fixture) => { 89 - if (fixture.isFn) { 90 - const usedProps = getUsedProps(fixture.value) 91 - if (usedProps.length) { 92 - fixture.deps = context.fixtures!.filter( 93 - ({ prop }) => prop !== fixture.prop && usedProps.includes(prop), 94 - ) 154 + else if (parent) { 155 + options = { 156 + auto: parent.auto, 157 + scope: parent.scope, 158 + injected: parent.injected, 159 + } 95 160 } 96 - // test can access anything, so we ignore it 97 - if (fixture.scope !== 'test') { 98 - fixture.deps?.forEach((dep) => { 99 - if (!dep.isFn) { 100 - // non fn fixtures are always resolved and available to anyone 101 - return 102 - } 103 - // worker scope can only import from worker scope 104 - if (fixture.scope === 'worker' && dep.scope === 'worker') { 105 - return 106 - } 107 - // file scope an import from file and worker scopes 108 - if (fixture.scope === 'file' && dep.scope !== 'test') { 109 - return 110 - } 161 + else if (!options) { 162 + options = { 163 + auto: false, 164 + injected: false, 165 + scope: 'test', 166 + } 167 + } 111 168 112 - throw new SyntaxError(`cannot use the ${dep.scope} fixture "${dep.prop}" inside the ${fixture.scope} fixture "${fixture.prop}"`) 113 - }) 169 + if (options.scope && !TestFixtures._fixtureScopes.includes(options.scope)) { 170 + errors.push(new FixtureDependencyError(`The "${name}" fixture has unknown scope "${options.scope}".`)) 171 + } 172 + 173 + if (!supportNonTest && options.scope !== 'test') { 174 + 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.`)) 175 + } 176 + 177 + const deps = isFixtureFunction(value) 178 + ? getUsedProps(value) 179 + : new Set<string>() 180 + const item: TestFixtureItem = { 181 + name, 182 + value, 183 + auto: options.auto ?? false, 184 + injected: options.injected ?? false, 185 + scope: options.scope ?? 'test', 186 + deps, 187 + parent, 188 + } 189 + 190 + registrations.set(name, item) 191 + 192 + if (item.scope === 'worker' && (runner.pool === 'vmThreads' || runner.pool === 'vmForks')) { 193 + item.scope = 'file' 194 + } 195 + }) 196 + 197 + // validate fixture dependency scopes 198 + for (const fixture of registrations.values()) { 199 + for (const depName of fixture.deps) { 200 + if (TestFixtures._builtinFixtures.includes(depName)) { 201 + continue 202 + } 203 + 204 + const dep = registrations.get(depName) 205 + if (!dep) { 206 + errors.push(new FixtureDependencyError(`The "${fixture.name}" fixture depends on unknown fixture "${depName}".`)) 207 + continue 208 + } 209 + if (depName === fixture.name && !fixture.parent) { 210 + errors.push(new FixtureDependencyError(`The "${fixture.name}" fixture depends on itself, but does not have a base implementation.`)) 211 + continue 212 + } 213 + 214 + if (TestFixtures._fixtureScopes.indexOf(fixture.scope) > TestFixtures._fixtureScopes.indexOf(dep.scope)) { 215 + errors.push(new FixtureDependencyError(`The ${fixture.scope} "${fixture.name}" fixture cannot depend on a ${dep.scope} fixture "${dep.name}".`)) 216 + continue 217 + } 114 218 } 115 219 } 116 - }) 117 220 118 - return context 221 + if (errors.length === 1) { 222 + throw errors[0] 223 + } 224 + else if (errors.length > 1) { 225 + throw new AggregateError(errors, 'Cannot resolve user fixtures. See errors for more information.') 226 + } 227 + return registrations 228 + } 119 229 } 120 230 121 - const fixtureValueMaps = new Map<TestContext, Map<FixtureItem, any>>() 122 - const cleanupFnArrayMap = new Map< 231 + const cleanupFnArrayMap = new WeakMap< 123 232 object, 124 233 Array<() => void | Promise<void>> 125 234 >() ··· 160 269 cleanupFnArray.length = fromIndex 161 270 } 162 271 163 - export function withFixtures(runner: VitestRunner, fn: Function, testContext?: TestContext) { 164 - return (hookContext?: TestContext): any => { 165 - const context: (TestContext & { [key: string]: any }) | undefined 166 - = hookContext || testContext 272 + const contextHasFixturesCache = new WeakMap<TestContext, WeakSet<TestFixtureItem>>() 273 + 274 + export function withFixtures(fn: Function, testContext?: TestContext) { 275 + const collector = getCurrentSuite() 276 + const suite = collector.suite || collector.file 277 + return async (hookContext?: TestContext): Promise<any> => { 278 + const context: (TestContext & { [key: string]: any }) | undefined = hookContext || testContext 167 279 168 280 if (!context) { 169 281 return fn({}) 170 282 } 171 283 172 - const fixtures = getTestFixture(context) 173 - if (!fixtures?.length) { 284 + const fixtures = getTestFixtures(context) 285 + if (!fixtures) { 174 286 return fn(context) 175 287 } 176 288 289 + const registrations = fixtures.get(suite) 290 + if (!registrations.size) { 291 + return fn(context) 292 + } 293 + 294 + const usedFixtures: TestFixtureItem[] = [] 177 295 const usedProps = getUsedProps(fn) 178 - const hasAutoFixture = fixtures.some(({ auto }) => auto) 179 - if (!usedProps.length && !hasAutoFixture) { 180 - return fn(context) 296 + 297 + for (const fixture of registrations.values()) { 298 + if (fixture.auto || usedProps.has(fixture.name)) { 299 + usedFixtures.push(fixture) 300 + } 181 301 } 182 302 183 - if (!fixtureValueMaps.get(context)) { 184 - fixtureValueMaps.set(context, new Map<FixtureItem, any>()) 303 + if (!usedFixtures.length) { 304 + return fn(context) 185 305 } 186 - const fixtureValueMap: Map<FixtureItem, any> 187 - = fixtureValueMaps.get(context)! 188 306 189 307 if (!cleanupFnArrayMap.has(context)) { 190 308 cleanupFnArrayMap.set(context, []) 191 309 } 192 310 const cleanupFnArray = cleanupFnArrayMap.get(context)! 193 311 194 - const usedFixtures = fixtures.filter( 195 - ({ prop, auto }) => auto || usedProps.includes(prop), 196 - ) 197 - const pendingFixtures = resolveDeps(usedFixtures) 312 + const pendingFixtures = resolveDeps(usedFixtures, registrations) 198 313 199 314 if (!pendingFixtures.length) { 200 315 return fn(context) 201 316 } 202 317 203 - async function resolveFixtures() { 204 - for (const fixture of pendingFixtures) { 318 + if (!contextHasFixturesCache.has(context)) { 319 + contextHasFixturesCache.set(context, new WeakSet()) 320 + } 321 + const cachedFixtures = contextHasFixturesCache.get(context)! 322 + 323 + for (const fixture of pendingFixtures) { 324 + if (fixture.scope === 'test') { 205 325 // fixture could be already initialized during "before" hook 206 - if (fixtureValueMap.has(fixture)) { 326 + // we can't check "fixture.name" in context because context may 327 + // access the parent fixture ({ a: ({ a }) => {} }) 328 + if (cachedFixtures.has(fixture)) { 207 329 continue 208 330 } 331 + cachedFixtures.add(fixture) 209 332 210 - const resolvedValue = await resolveFixtureValue( 211 - runner, 333 + const resolvedValue = await resolveTestFixtureValue( 212 334 fixture, 213 - context!, 335 + context, 214 336 cleanupFnArray, 215 337 ) 216 - context![fixture.prop] = resolvedValue 217 - fixtureValueMap.set(fixture, resolvedValue) 338 + context[fixture.name] = resolvedValue 218 339 219 - if (fixture.scope === 'test') { 220 - cleanupFnArray.unshift(() => { 221 - fixtureValueMap.delete(fixture) 222 - }) 223 - } 340 + cleanupFnArray.push(() => { 341 + cachedFixtures.delete(fixture) 342 + }) 343 + } 344 + else { 345 + const resolvedValue = await resolveScopeFixtureValue( 346 + fixtures, 347 + suite, 348 + fixture, 349 + ) 350 + context[fixture.name] = resolvedValue 224 351 } 225 352 } 226 353 227 - return resolveFixtures().then(() => fn(context)) 354 + return fn(context) 228 355 } 229 356 } 230 357 231 - const globalFixturePromise = new WeakMap<FixtureItem, Promise<unknown>>() 358 + function isFixtureFunction(value: unknown): value is FixtureFn<any, any, any> { 359 + return typeof value === 'function' 360 + } 232 361 233 - function resolveFixtureValue( 234 - runner: VitestRunner, 235 - fixture: FixtureItem, 362 + function resolveTestFixtureValue( 363 + fixture: TestFixtureItem, 236 364 context: TestContext & { [key: string]: any }, 237 365 cleanupFnArray: (() => void | Promise<void>)[], 238 366 ) { 239 - const fileContext = getFileContext(context.task.file) 240 - const workerContext = runner.getWorkerContext?.() 241 - 242 - if (!fixture.isFn) { 243 - fileContext[fixture.prop] ??= fixture.value 244 - if (workerContext) { 245 - workerContext[fixture.prop] ??= fixture.value 246 - } 367 + if (!isFixtureFunction(fixture.value)) { 247 368 return fixture.value 248 369 } 249 370 250 - if (fixture.scope === 'test') { 251 - return resolveFixtureFunction( 252 - fixture.value, 253 - context, 254 - cleanupFnArray, 255 - ) 371 + return resolveFixtureFunction( 372 + fixture.value, 373 + context, 374 + cleanupFnArray, 375 + ) 376 + } 377 + 378 + const scopedFixturePromiseCache = new WeakMap<TestFixtureItem, Promise<unknown>>() 379 + 380 + async function resolveScopeFixtureValue( 381 + fixtures: TestFixtures, 382 + suite: Suite, 383 + fixture: TestFixtureItem, 384 + ) { 385 + const workerContext = fixtures.getWorkerContext() 386 + const fileContext = fixtures.getFileContext(suite.file) 387 + const fixtureContext = fixture.scope === 'worker' ? workerContext : fileContext 388 + 389 + if (!isFixtureFunction(fixture.value)) { 390 + fixtureContext[fixture.name] = fixture.value 391 + return fixture.value 256 392 } 257 393 258 - // in case the test runs in parallel 259 - if (globalFixturePromise.has(fixture)) { 260 - return globalFixturePromise.get(fixture)! 394 + if (fixture.name in fixtureContext) { 395 + return fixtureContext[fixture.name] 261 396 } 262 397 263 - let fixtureContext: Record<string, unknown> 264 - 265 - if (fixture.scope === 'worker') { 266 - if (!workerContext) { 267 - 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.') 268 - } 269 - fixtureContext = workerContext 270 - } 271 - else { 272 - fixtureContext = fileContext 273 - } 274 - 275 - if (fixture.prop in fixtureContext) { 276 - return fixtureContext[fixture.prop] 398 + if (scopedFixturePromiseCache.has(fixture)) { 399 + return scopedFixturePromiseCache.get(fixture)! 277 400 } 278 401 279 402 if (!cleanupFnArrayMap.has(fixtureContext)) { ··· 283 406 284 407 const promise = resolveFixtureFunction( 285 408 fixture.value, 286 - fixtureContext, 409 + fixture.scope === 'file' ? { ...workerContext, ...fileContext } : fixtureContext, 287 410 cleanupFnFileArray, 288 411 ).then((value) => { 289 - fixtureContext[fixture.prop] = value 290 - globalFixturePromise.delete(fixture) 412 + fixtureContext[fixture.name] = value 413 + scopedFixturePromiseCache.delete(fixture) 291 414 return value 292 415 }) 293 - 294 - globalFixturePromise.set(fixture, promise) 416 + scopedFixturePromiseCache.set(fixture, promise) 295 417 return promise 296 418 } 297 419 ··· 335 457 } 336 458 337 459 function resolveDeps( 338 - fixtures: FixtureItem[], 339 - depSet = new Set<FixtureItem>(), 340 - pendingFixtures: FixtureItem[] = [], 460 + usedFixtures: TestFixtureItem[], 461 + registrations: FixtureRegistrations, 462 + depSet = new Set<TestFixtureItem>(), 463 + pendingFixtures: TestFixtureItem[] = [], 341 464 ) { 342 - fixtures.forEach((fixture) => { 465 + usedFixtures.forEach((fixture) => { 343 466 if (pendingFixtures.includes(fixture)) { 344 467 return 345 468 } 346 - if (!fixture.isFn || !fixture.deps) { 469 + if (!isFixtureFunction(fixture.value) || !fixture.deps) { 347 470 pendingFixtures.push(fixture) 348 471 return 349 472 } 350 473 if (depSet.has(fixture)) { 351 - throw new Error( 352 - `Circular fixture dependency detected: ${fixture.prop} <- ${[...depSet] 353 - .reverse() 354 - .map(d => d.prop) 355 - .join(' <- ')}`, 356 - ) 474 + if (fixture.parent) { 475 + fixture = fixture.parent 476 + } 477 + else { 478 + throw new Error( 479 + `Circular fixture dependency detected: ${fixture.name} <- ${[...depSet] 480 + .reverse() 481 + .map(d => d.name) 482 + .join(' <- ')}`, 483 + ) 484 + } 357 485 } 358 486 359 487 depSet.add(fixture) 360 - resolveDeps(fixture.deps, depSet, pendingFixtures) 488 + resolveDeps( 489 + [...fixture.deps].map(n => n === fixture.name ? fixture.parent : registrations.get(n)).filter(n => !!n), 490 + registrations, 491 + depSet, 492 + pendingFixtures, 493 + ) 361 494 pendingFixtures.push(fixture) 362 495 depSet.clear() 363 496 }) ··· 365 498 return pendingFixtures 366 499 } 367 500 368 - function getUsedProps(fn: Function) { 501 + const propsSymbol = Symbol('$vitest:fixture-props') 502 + 503 + export function migrateProps(from: Function, to: Function): void { 504 + const props = getUsedProps(from) 505 + Object.defineProperty(to, propsSymbol, { 506 + enumerable: false, 507 + writable: true, 508 + value: props, 509 + }) 510 + } 511 + 512 + function getUsedProps(fn: Function): Set<string> { 513 + if (propsSymbol in fn) { 514 + return fn[propsSymbol] as Set<string> 515 + } 369 516 let fnString = filterOutComments(fn.toString()) 370 517 // match lowered async function and strip it off 371 518 // example code on esbuild-try https://esbuild.github.io/try/#YgAwLjI0LjAALS1zdXBwb3J0ZWQ6YXN5bmMtYXdhaXQ9ZmFsc2UAZQBlbnRyeS50cwBjb25zdCBvID0gewogIGYxOiBhc3luYyAoKSA9PiB7fSwKICBmMjogYXN5bmMgKGEpID0+IHt9LAogIGYzOiBhc3luYyAoYSwgYikgPT4ge30sCiAgZjQ6IGFzeW5jIGZ1bmN0aW9uKGEpIHt9LAogIGY1OiBhc3luYyBmdW5jdGlvbiBmZihhKSB7fSwKICBhc3luYyBmNihhKSB7fSwKCiAgZzE6IGFzeW5jICgpID0+IHt9LAogIGcyOiBhc3luYyAoeyBhIH0pID0+IHt9LAogIGczOiBhc3luYyAoeyBhIH0sIGIpID0+IHt9LAogIGc0OiBhc3luYyBmdW5jdGlvbiAoeyBhIH0pIHt9LAogIGc1OiBhc3luYyBmdW5jdGlvbiBnZyh7IGEgfSkge30sCiAgYXN5bmMgZzYoeyBhIH0pIHt9LAoKICBoMTogYXN5bmMgKCkgPT4ge30sCiAgLy8gY29tbWVudCBiZXR3ZWVuCiAgaDI6IGFzeW5jIChhKSA9PiB7fSwKfQ ··· 377 524 } 378 525 const match = fnString.match(/[^(]*\(([^)]*)/) 379 526 if (!match) { 380 - return [] 527 + return new Set() 381 528 } 382 529 383 530 const args = splitByComma(match[1]) 384 531 if (!args.length) { 385 - return [] 532 + return new Set() 386 533 } 387 534 388 535 let first = args[0] 389 536 if ('__VITEST_FIXTURE_INDEX__' in fn) { 390 537 first = args[(fn as any).__VITEST_FIXTURE_INDEX__] 391 538 if (!first) { 392 - return [] 539 + return new Set() 393 540 } 394 541 } 395 542 ··· 411 558 ) 412 559 } 413 560 414 - return props 561 + return new Set(props) 415 562 } 416 563 417 564 function splitByComma(s: string) {
+4 -9
packages/runner/src/hooks.ts
··· 1 - import type { VitestRunner } from './types' 2 1 import type { 3 2 AfterAllListener, 4 3 AfterEachListener, ··· 144 143 ): void { 145 144 assertTypes(fn, '"beforeEach" callback', ['function']) 146 145 const stackTraceError = new Error('STACK_TRACE_ERROR') 147 - const runner = getRunner() 148 146 return getCurrentSuite<ExtraContext>().on( 149 147 'beforeEach', 150 148 Object.assign( 151 149 withTimeout( 152 - withFixtures(runner, fn), 150 + withFixtures(fn), 153 151 timeout ?? getDefaultHookTimeout(), 154 152 true, 155 153 stackTraceError, ··· 185 183 timeout?: number, 186 184 ): void { 187 185 assertTypes(fn, '"afterEach" callback', ['function']) 188 - const runner = getRunner() 189 186 return getCurrentSuite<ExtraContext>().on( 190 187 'afterEach', 191 188 withTimeout( 192 - withFixtures(runner, fn), 189 + withFixtures(fn), 193 190 timeout ?? getDefaultHookTimeout(), 194 191 true, 195 192 new Error('STACK_TRACE_ERROR'), ··· 351 348 assertTypes(fn, '"aroundEach" callback', ['function']) 352 349 const stackTraceError = new Error('STACK_TRACE_ERROR') 353 350 const resolvedTimeout = timeout ?? getDefaultHookTimeout() 354 - const runner = getRunner() 355 351 356 352 // Create a wrapper function that supports fixtures in the second argument (context) 357 353 // withFixtures resolves fixtures into context, then we call fn with all 3 args 358 - const wrappedFn: AroundEachListener<ExtraContext> = withAroundEachFixtures(runner, fn) 354 + const wrappedFn: AroundEachListener<ExtraContext> = withAroundEachFixtures(fn) 359 355 360 356 // Store timeout and stack trace on the function for use in callAroundEachHooks 361 357 // Setup and teardown phases will each have their own timeout ··· 376 372 * - Third arg is suite 377 373 */ 378 374 function withAroundEachFixtures<ExtraContext>( 379 - runner: VitestRunner, 380 375 fn: AroundEachListener<ExtraContext>, 381 376 ): AroundEachListener<ExtraContext> { 382 377 // Create the wrapper that will be returned ··· 390 385 ;(innerFn as any).toString = () => fn.toString() 391 386 392 387 // Use withFixtures to resolve fixtures, passing context as the hook context 393 - const fixtureResolver = withFixtures(runner, innerFn) 388 + const fixtureResolver = withFixtures(innerFn) 394 389 return fixtureResolver(context) 395 390 } 396 391
+3 -3
packages/runner/src/map.ts
··· 1 1 import type { Awaitable } from '@vitest/utils' 2 - import type { FixtureItem } from './fixture' 2 + import type { TestFixtures } from './fixture' 3 3 import type { Suite, SuiteHooks, Test, TestContext } from './types/tasks' 4 4 5 5 // use WeakMap here to make the Test and Suite object serializable ··· 17 17 18 18 export function setTestFixture( 19 19 key: TestContext, 20 - fixture: FixtureItem[] | undefined, 20 + fixture: TestFixtures, 21 21 ): void { 22 22 testFixtureMap.set(key, fixture) 23 23 } 24 24 25 - export function getTestFixture<Context = TestContext>(key: Context): FixtureItem[] { 25 + export function getTestFixtures<Context = TestContext>(key: Context): TestFixtures { 26 26 return testFixtureMap.get(key as any) 27 27 } 28 28
+13 -10
packages/runner/src/run.ts
··· 22 22 import { shuffle } from '@vitest/utils/helpers' 23 23 import { getSafeTimers } from '@vitest/utils/timers' 24 24 import { collectTests } from './collect' 25 - import { abortContextSignal, getFileContext } from './context' 25 + import { abortContextSignal } from './context' 26 26 import { AroundHookMultipleCallsError, AroundHookSetupError, AroundHookTeardownError, PendingError, TestRunAbortError } from './errors' 27 - import { callFixtureCleanup, callFixtureCleanupFrom, getFixtureCleanupCount } from './fixture' 27 + import { callFixtureCleanup, callFixtureCleanupFrom, getFixtureCleanupCount, TestFixtures } from './fixture' 28 28 import { getAroundHookStackTrace, getAroundHookTimeout, getBeforeHookCleanupCallback } from './hooks' 29 29 import { getFn, getHooks } from './map' 30 30 import { addRunningTest, getRunningTests, setCurrentTest } from './test-state' ··· 767 767 result.state = 'fail' 768 768 const errors = Array.isArray(err) ? err : [err] 769 769 for (const e of errors) { 770 - const error = processError(e, diffOptions) 770 + const errors = e instanceof AggregateError 771 + ? e.errors.map(e => processError(e, diffOptions)) 772 + : [processError(e, diffOptions)] 771 773 result.errors ??= [] 772 - result.errors.push(error) 774 + result.errors.push(...errors) 773 775 } 774 776 } 775 777 ··· 878 880 await $('suite.cleanup', () => callCleanupHooks(runner, beforeAllCleanups)) 879 881 } 880 882 if (suite.file === suite) { 881 - const context = getFileContext(suite as File) 882 - await callFixtureCleanup(context) 883 + const contexts = TestFixtures.getFileContexts(suite.file) 884 + await Promise.all(contexts.map(context => callFixtureCleanup(context))) 883 885 } 884 886 } 885 887 catch (e) { ··· 1008 1010 1009 1011 if (!workerRunners.has(runner)) { 1010 1012 runner.onCleanupWorkerContext?.(async () => { 1011 - const context = runner.getWorkerContext?.() 1012 - if (context) { 1013 - await callFixtureCleanup(context) 1014 - } 1013 + await Promise.all( 1014 + [...TestFixtures.getWorkerContexts()].map(context => callFixtureCleanup(context)), 1015 + ).finally(() => { 1016 + TestFixtures.clearDefinitions() 1017 + }) 1015 1018 }) 1016 1019 workerRunners.add(runner) 1017 1020 }
+134 -75
packages/runner/src/suite.ts
··· 1 - import type { FixtureItem } from './fixture' 1 + import type { UserFixtures } from './fixture' 2 2 import type { VitestRunner } from './types/runner' 3 3 import type { 4 4 File, 5 - Fixtures, 5 + InternalTestContext, 6 6 RunMode, 7 7 Suite, 8 8 SuiteAPI, ··· 34 34 runWithSuite, 35 35 withTimeout, 36 36 } from './context' 37 - import { mergeContextFixtures, mergeScopedFixtures, withFixtures } from './fixture' 37 + import { migrateProps, TestFixtures, withFixtures } from './fixture' 38 38 import { afterAll, afterEach, aroundAll, aroundEach, beforeAll, beforeEach } from './hooks' 39 39 import { getHooks, setFn, setHooks, setTestFixture } from './map' 40 40 import { getCurrentTest } from './test-state' 41 41 import { findTestFileStackTrace } from './utils' 42 - import { createChainable } from './utils/chain' 42 + import { createChainable, getChainableContext } from './utils/chain' 43 43 import { createNoTagsError, validateTags } from './utils/tags' 44 44 import { createTaskName } from './utils/tasks' 45 45 ··· 243 243 244 244 export function getCurrentSuite<ExtraContext = object>(): SuiteCollector<ExtraContext> { 245 245 const currentSuite = (collectorContext.currentSuite 246 - || defaultSuite) as SuiteCollector<ExtraContext> 246 + || defaultSuite) as unknown as SuiteCollector<ExtraContext> 247 247 assert(currentSuite, 'the current suite') 248 248 return currentSuite 249 249 } ··· 306 306 mode: RunMode, 307 307 each?: boolean, 308 308 suiteOptions?: SuiteOptions, 309 - parentCollectorFixtures?: FixtureItem[], 310 309 ) { 311 310 const tasks: (Test | Suite | SuiteCollector)[] = [] 312 311 313 312 let suite!: Suite 314 - 315 313 initSuite(true) 316 314 317 315 const task = function (name = '', options: TaskCustomOptions = {}) { ··· 403 401 value: context, 404 402 enumerable: false, 405 403 }) 406 - setTestFixture(context, options.fixtures) 404 + setTestFixture(context, options.fixtures ?? new TestFixtures()) 407 405 408 406 // custom can be called from any place, let's assume the limit is 15 stacks 409 407 const limit = Error.stackTraceLimit ··· 415 413 setFn( 416 414 task, 417 415 withTimeout( 418 - withAwaitAsyncAssertions(withFixtures(runner, handler, context), task), 416 + withAwaitAsyncAssertions(withFixtures(handler, context), task), 419 417 timeout, 420 418 false, 421 419 stackTraceError, ··· 471 469 test.type = 'test' 472 470 }) 473 471 474 - let collectorFixtures = parentCollectorFixtures 475 - 476 472 const collector: SuiteCollector = { 477 473 type: 'collector', 478 474 name, ··· 480 476 suite, 481 477 options: suiteOptions, 482 478 test, 479 + file: suite.file, 483 480 tasks, 484 481 collect, 485 482 task, 486 483 clear, 487 484 on: addHook, 488 - fixtures() { 489 - return collectorFixtures 490 - }, 491 - scoped(fixtures) { 492 - const parsed = mergeContextFixtures( 493 - fixtures, 494 - { fixtures: collectorFixtures }, 495 - runner, 496 - ) 497 - if (parsed.fixtures) { 498 - collectorFixtures = parsed.fixtures 499 - } 500 - }, 501 485 } 502 486 503 487 function addHook<T extends keyof SuiteHooks>(name: T, ...fn: SuiteHooks[T]) { ··· 665 649 mode, 666 650 this.each, 667 651 options, 668 - currentSuite?.fixtures(), 669 652 ) 670 653 } 671 654 672 655 suiteFn.each = function <T>( 673 - this: { 674 - withContext: () => SuiteAPI 675 - setContext: (key: string, value: boolean | undefined) => SuiteAPI 676 - }, 656 + this: SuiteAPI, 677 657 cases: ReadonlyArray<T>, 678 658 ...args: any[] 679 659 ) { 680 - const suite = this.withContext() 681 - this.setContext('each', true) 660 + const context = getChainableContext(this) 661 + const suite = context.withContext() 662 + context.setContext('each', true) 682 663 683 664 if (Array.isArray(cases) && args.length) { 684 665 cases = formatTemplateString(cases, args) ··· 720 701 } 721 702 }) 722 703 723 - this.setContext('each', undefined) 704 + context.setContext('each', undefined) 724 705 } 725 706 } 726 707 ··· 762 743 763 744 export function createTaskCollector( 764 745 fn: (...args: any[]) => any, 765 - context?: Record<string, unknown>, 766 746 ): TestAPI { 767 747 const taskFn = fn as any 768 748 769 749 taskFn.each = function <T>( 770 - this: { 771 - withContext: () => SuiteAPI 772 - setContext: (key: string, value: boolean | undefined) => SuiteAPI 773 - }, 750 + this: TestAPI, 774 751 cases: ReadonlyArray<T>, 775 752 ...args: any[] 776 753 ) { 777 - const test = this.withContext() 778 - this.setContext('each', true) 754 + const context = getChainableContext(this) 755 + const test = context.withContext() 756 + context.setContext('each', true) 779 757 780 758 if (Array.isArray(cases) && args.length) { 781 759 cases = formatTemplateString(cases, args) ··· 818 796 } 819 797 }) 820 798 821 - this.setContext('each', undefined) 799 + context.setContext('each', undefined) 822 800 } 823 801 } 824 802 825 803 taskFn.for = function <T>( 826 - this: { 827 - withContext: () => SuiteAPI 828 - setContext: (key: string, value: boolean | undefined) => SuiteAPI 829 - }, 804 + this: TestAPI, 830 805 cases: ReadonlyArray<T>, 831 806 ...args: any[] 832 807 ) { 833 - const test = this.withContext() 808 + const context = getChainableContext(this) 809 + const test = context.withContext() 834 810 835 811 if (Array.isArray(cases) && args.length) { 836 812 cases = formatTemplateString(cases, args) ··· 862 838 return condition ? this : this.skip 863 839 } 864 840 865 - taskFn.scoped = function (fixtures: Fixtures<Record<string, any>>) { 866 - const collector = getCurrentSuite() 867 - collector.scoped(fixtures) 841 + /** 842 + * Parse builder pattern arguments into a fixtures object. 843 + * Handles both builder pattern (name, options?, value) and object syntax. 844 + */ 845 + function parseBuilderFixtures( 846 + fixturesOrName: UserFixtures | string, 847 + optionsOrFn?: object | ((...args: any[]) => any), 848 + maybeFn?: (...args: any[]) => any, 849 + ): UserFixtures { 850 + // Object syntax: just return as-is 851 + if (typeof fixturesOrName !== 'string') { 852 + return fixturesOrName 853 + } 854 + 855 + const fixtureName = fixturesOrName 856 + let fixtureOptions: object | undefined 857 + let fixtureValue: any 858 + 859 + if (maybeFn !== undefined) { 860 + // (name, options, value) or (name, options, fn) 861 + fixtureOptions = optionsOrFn as object 862 + fixtureValue = maybeFn 863 + } 864 + else { 865 + // (name, value) or (name, fn) 866 + // Check if optionsOrFn looks like fixture options (has scope or auto) 867 + if ( 868 + optionsOrFn !== null 869 + && typeof optionsOrFn === 'object' 870 + && !Array.isArray(optionsOrFn) 871 + && ('scope' in optionsOrFn || 'auto' in optionsOrFn) 872 + ) { 873 + // (name, options) with no value - treat as empty object fixture 874 + fixtureOptions = optionsOrFn as object 875 + fixtureValue = {} 876 + } 877 + else { 878 + // (name, value) or (name, fn) 879 + fixtureOptions = undefined 880 + fixtureValue = optionsOrFn 881 + } 882 + } 883 + 884 + // Function value: wrap with onCleanup pattern 885 + if (typeof fixtureValue === 'function') { 886 + const builderFn = fixtureValue as (...args: any[]) => any 887 + 888 + // Wrap builder pattern function (returns value) to use() pattern 889 + const fixture = async (ctx: any, use: (value: any) => Promise<void>) => { 890 + let cleanup: (() => any) | undefined 891 + const onCleanup = (fn: () => any) => { 892 + if (cleanup !== undefined) { 893 + throw new Error( 894 + `onCleanup can only be called once per fixture. ` 895 + + `Define separate fixtures if you need multiple cleanup functions.`, 896 + ) 897 + } 898 + cleanup = fn 899 + } 900 + const value = await builderFn(ctx, { onCleanup }) 901 + await use(value) 902 + if (cleanup) { 903 + await cleanup() 904 + } 905 + } 906 + migrateProps(builderFn, fixture) 907 + 908 + if (fixtureOptions) { 909 + return { [fixtureName]: [fixture, fixtureOptions] } as any 910 + } 911 + return { [fixtureName]: fixture } as any 912 + } 913 + 914 + // Non-function value: use directly 915 + if (fixtureOptions) { 916 + return { [fixtureName]: [fixtureValue, fixtureOptions] } as any 917 + } 918 + return { [fixtureName]: fixtureValue } as any 868 919 } 869 920 870 - taskFn.extend = function (fixtures: Fixtures<Record<string, any>>) { 871 - const _context = mergeContextFixtures( 872 - fixtures, 873 - context || {}, 921 + taskFn.override = function ( 922 + this: TestAPI, 923 + fixturesOrName: UserFixtures | string, 924 + optionsOrFn?: object | ((...args: any[]) => any), 925 + maybeFn?: (...args: any[]) => any, 926 + ) { 927 + const userFixtures = parseBuilderFixtures(fixturesOrName, optionsOrFn, maybeFn) 928 + getChainableContext(this).getFixtures().override(runner, userFixtures) 929 + return this 930 + } 931 + 932 + taskFn.scoped = function (fixtures: UserFixtures) { 933 + console.warn(`test.scoped() is deprecated and will be removed in future versions. Please use test.override() instead.`) 934 + return this.override(fixtures) 935 + } 936 + 937 + taskFn.extend = function ( 938 + this: TestAPI, 939 + fixturesOrName: UserFixtures | string, 940 + optionsOrFn?: object | ((...args: any[]) => any), 941 + maybeFn?: (...args: any[]) => any, 942 + ) { 943 + const userFixtures = parseBuilderFixtures(fixturesOrName, optionsOrFn, maybeFn) 944 + const fixtures = getChainableContext(this).getFixtures().extend( 874 945 runner, 946 + userFixtures, 875 947 ) 876 948 877 - const originalWrapper = fn 878 - return createTest(function ( 949 + const _test = createTest(function ( 879 950 name: string | Function, 880 951 optionsOrFn?: TestOptions | TestFunction, 881 952 optionsOrTest?: number | TestFunction, 882 953 ) { 883 - const collector = getCurrentSuite() 884 - const scopedFixtures = collector.fixtures() 885 - const context = { ...this } 886 - if (scopedFixtures) { 887 - context.fixtures = mergeScopedFixtures( 888 - context.fixtures || [], 889 - scopedFixtures, 890 - ) 891 - } 892 - originalWrapper.call(context, formatName(name), optionsOrFn, optionsOrTest) 893 - }, _context) 954 + fn.call(this, formatName(name), optionsOrFn, optionsOrTest) 955 + }) 956 + getChainableContext(_test).mergeContext({ fixtures }) 957 + 958 + return _test 894 959 } 895 960 896 961 taskFn.describe = suite 962 + taskFn.suite = suite 897 963 taskFn.beforeEach = beforeEach 898 964 taskFn.afterEach = afterEach 899 965 taskFn.beforeAll = beforeAll ··· 904 970 const _test = createChainable( 905 971 ['concurrent', 'sequential', 'skip', 'only', 'todo', 'fails'], 906 972 taskFn, 973 + { fixtures: new TestFixtures() }, 907 974 ) as TestAPI 908 - 909 - if (context) { 910 - (_test as any).mergeContext(context) 911 - } 912 975 913 976 return _test 914 977 } 915 978 916 979 function createTest( 917 980 fn: ( 918 - this: Record< 919 - 'concurrent' | 'sequential' | 'skip' | 'only' | 'todo' | 'fails' | 'each', 920 - boolean | undefined 921 - > & { fixtures?: FixtureItem[] }, 981 + this: InternalTestContext, 922 982 title: string, 923 983 optionsOrFn?: TestOptions | TestFunction, 924 984 optionsOrTest?: number | TestFunction, 925 985 ) => void, 926 - context?: Record<string, any>, 927 986 ) { 928 - return createTaskCollector(fn, context) as TestAPI 987 + return createTaskCollector(fn) as TestAPI 929 988 } 930 989 931 990 function formatName(name: string | Function) {
+1 -1
packages/utils/src/error.ts
··· 10 10 _err: any, 11 11 diffOptions?: DiffOptions, 12 12 seen: WeakSet<WeakKey> = new WeakSet(), 13 - ): any { 13 + ): TestError { 14 14 if (!_err || typeof _err !== 'object') { 15 15 return { message: String(_err) } 16 16 }
+2 -2
test/cli/test/around-each.test.ts
··· 1156 1156 >> user fixture setup 1157 1157 >> test running, db available: true 1158 1158 >> user fixture teardown 1159 - >> db fixture teardown 1160 - >> aroundEach teardown" 1159 + >> aroundEach teardown 1160 + >> db fixture teardown" 1161 1161 `) 1162 1162 expect(errorTree()).toMatchInlineSnapshot(` 1163 1163 {
+5 -5
test/cli/test/fails.test.ts
··· 93 93 }) 94 94 expect(warnings).toMatchInlineSnapshot(` 95 95 [ 96 - "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. 96 + "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. 97 97 at <rootDir>/base.test.js:5:33", 98 - "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. 98 + "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. 99 99 at <rootDir>/base.test.js:10:32", 100 - "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. 100 + "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. 101 101 at <rootDir>/base.test.js:9:33", 102 - "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. 102 + "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. 103 103 at <rootDir>/base.test.js:14:33", 104 - "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. 104 + "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. 105 105 at <rootDir>/base.test.js:19:17", 106 106 ] 107 107 `)
+1719 -19
test/cli/test/scoped-fixtures.test.ts
··· 1 - import type { ExpectStatic, SuiteAPI, TestAPI } from 'vitest' 1 + import type { ExpectStatic, expectTypeOf as ExpectTypeOfFn, SuiteAPI, TestAPI } from 'vitest' 2 2 import type { ViteUserConfig } from 'vitest/config' 3 3 import type { TestSpecification, TestUserConfig } from 'vitest/node' 4 4 import type { TestFsStructure } from '../../test-utils' ··· 26 26 extendedTest('not working', ({ file: _file }) => {}) 27 27 }, 28 28 }, { globals: true }) 29 - expect(stderr).toContain('cannot use the test fixture "local" inside the file fixture "file"') 29 + expect(stderr).toMatchInlineSnapshot(` 30 + " 31 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 32 + 33 + FAIL basic.test.ts [ basic.test.ts ] 34 + FixtureDependencyError: The file "file" fixture cannot depend on a test fixture "local". 35 + ❯ basic.test.ts:2:31 36 + 1| await (() => { 37 + 2| const extendedTest = it.extend({ 38 + | ^ 39 + 3| local: ({}, use) => use("local"), 40 + 4| file: [ 41 + ❯ basic.test.ts:11:5 42 + 43 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 44 + 45 + " 46 + `) 30 47 }) 31 48 32 49 test('can import file fixture inside the local fixture', async () => { ··· 116 133 extendedTest('not working', ({ worker: _worker }) => {}) 117 134 }, 118 135 }, { globals: true }) 119 - expect(stderr).toContain('cannot use the test fixture "local" inside the worker fixture "worker"') 136 + expect(stderr).toMatchInlineSnapshot(` 137 + " 138 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 139 + 140 + FAIL basic.test.ts [ basic.test.ts ] 141 + FixtureDependencyError: The worker "worker" fixture cannot depend on a test fixture "local". 142 + ❯ basic.test.ts:2:31 143 + 1| await (() => { 144 + 2| const extendedTest = it.extend({ 145 + | ^ 146 + 3| local: ({}, use) => use("local"), 147 + 4| worker: [ 148 + ❯ basic.test.ts:11:5 149 + 150 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 151 + 152 + " 153 + `) 120 154 }) 121 155 122 156 test('auto worker fixture is initialised always before the first test', async () => { ··· 152 186 }) 153 187 154 188 test('worker fixture can import a static value from test fixture', async () => { 155 - const { stderr, stdout } = await runFixtureTests(() => it.extend<{ 189 + const { stderr } = await runFixtureTests(() => it.extend<{ 156 190 worker: string 157 191 local: string 158 192 }>({ ··· 169 203 }) 170 204 }, 171 205 }, { globals: true }) 172 - expect(stdout).toContain('basic.test.ts') 173 - expect(stderr).toBe('') 206 + expect(stderr).toMatchInlineSnapshot(` 207 + " 208 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 209 + 210 + FAIL basic.test.ts [ basic.test.ts ] 211 + FixtureDependencyError: The worker "worker" fixture cannot depend on a test fixture "local". 212 + ❯ test.js:4:39 213 + 2| export const expect = globalThis.expect 214 + 3| export const expectTypeOf = globalThis.expectTypeOf 215 + 4| export const extendedTest = (() => it.extend({ 216 + | ^ 217 + 5| local: "local", 218 + 6| worker: [ 219 + ❯ test.js:10:4 220 + 221 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 222 + 223 + " 224 + `) 174 225 }) 175 226 176 - test('file fixture can import a static value from test fixture', async () => { 177 - const { stderr, stdout } = await runFixtureTests(() => it.extend<{ 227 + test('file fixture cannot import a static value from test fixture', async () => { 228 + const { stderr } = await runFixtureTests(() => it.extend<{ 178 229 file: string 179 230 local: string 180 231 }>({ ··· 191 242 }) 192 243 }, 193 244 }, { globals: true }) 194 - expect(stdout).toContain('basic.test.ts') 195 - expect(stderr).toBe('') 245 + expect(stderr).toMatchInlineSnapshot(` 246 + " 247 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 248 + 249 + FAIL basic.test.ts [ basic.test.ts ] 250 + FixtureDependencyError: The file "file" fixture cannot depend on a test fixture "local". 251 + ❯ test.js:4:39 252 + 2| export const expect = globalThis.expect 253 + 3| export const expectTypeOf = globalThis.expectTypeOf 254 + 4| export const extendedTest = (() => it.extend({ 255 + | ^ 256 + 5| local: "local", 257 + 6| file: [ 258 + ❯ test.js:10:4 259 + 260 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 261 + 262 + " 263 + `) 196 264 }) 197 265 198 266 test('worker fixture works in vmThreads and runs for every file', async () => { ··· 242 310 extendedTest('test1', ({ worker: _worker }) => {}) 243 311 }, 244 312 '2-basic.test.ts': ({ extendedTest }) => { 245 - extendedTest('test1', ({ worker: _worker }) => {}) 313 + extendedTest('test2', ({ worker: _worker }) => {}) 246 314 }, 247 315 }, { 248 316 globals: true, ··· 254 322 expect(fixtures).toMatchInlineSnapshot(` 255 323 ">> fixture | init worker | test1 256 324 >> fixture | teardown worker | test1 257 - >> fixture | init worker | test1 258 - >> fixture | teardown worker | test1" 325 + >> fixture | init worker | test2 326 + >> fixture | teardown worker | test2" 259 327 `) 260 328 expect(tests).toMatchInlineSnapshot(` 261 329 " ✓ 1-basic.test.ts > test1 <time> 262 - ✓ 2-basic.test.ts > test1 <time>" 330 + ✓ 2-basic.test.ts > test2 <time>" 263 331 `) 264 332 }) 265 333 ··· 690 758 }) 691 759 }) 692 760 761 + test('file fixture cannot access test fixture at runtime', async () => { 762 + // This test verifies that the runtime prevents file fixtures from accessing test fixtures 763 + const { stderr } = await runInlineTests({ 764 + 'basic.test.ts': () => { 765 + const extendedTest = it.extend< 766 + { 767 + testFixture: string 768 + fileFixture: number 769 + } 770 + >({ 771 + testFixture: ({}, use) => use('test'), 772 + fileFixture: [ 773 + ({ testFixture }, use) => use(testFixture.length), 774 + { scope: 'file' }, 775 + ], 776 + }) 777 + 778 + extendedTest('not working', ({ fileFixture: _file }) => {}) 779 + }, 780 + }, { globals: true }) 781 + expect(stderr).toMatchInlineSnapshot(` 782 + " 783 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 784 + 785 + FAIL basic.test.ts [ basic.test.ts ] 786 + FixtureDependencyError: The file "fileFixture" fixture cannot depend on a test fixture "testFixture". 787 + ❯ basic.test.ts:2:31 788 + 1| await (() => { 789 + 2| const extendedTest = it.extend({ 790 + | ^ 791 + 3| testFixture: ({}, use) => use("test"), 792 + 4| fileFixture: [ 793 + ❯ basic.test.ts:11:5 794 + 795 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 796 + 797 + " 798 + `) 799 + }) 800 + 801 + test('scoped fixtures with tuple syntax work', async () => { 802 + const { stderr, fixtures, tests } = await runFixtureTests(({ log, expectTypeOf }) => it.extend<{ 803 + $test: { testFixture: string } 804 + $file: { fileFixture: number } 805 + $worker: { workerFixture: boolean } 806 + }>({ 807 + workerFixture: [async ({}, use) => { 808 + log('workerFixture setup') 809 + await use(true) 810 + log('workerFixture teardown') 811 + }, { scope: 'worker' }], 812 + fileFixture: [async ({ workerFixture }, use) => { 813 + // Confirm workerFixture is typed as boolean (file can access worker fixtures) 814 + expectTypeOf(workerFixture).toEqualTypeOf<boolean>() 815 + log('fileFixture setup', workerFixture) 816 + await use(workerFixture ? 42 : 0) 817 + log('fileFixture teardown') 818 + }, { scope: 'file' }], 819 + testFixture: async ({ fileFixture, workerFixture }, use) => { 820 + // Confirm fileFixture is typed as number, workerFixture as boolean (test can access all) 821 + expectTypeOf(fileFixture).toEqualTypeOf<number>() 822 + expectTypeOf(workerFixture).toEqualTypeOf<boolean>() 823 + log('testFixture setup', fileFixture, workerFixture) 824 + await use(`test-${fileFixture}-${workerFixture}`) 825 + log('testFixture teardown') 826 + }, 827 + }), { 828 + 'basic.test.ts': ({ extendedTest, expect, expectTypeOf }) => { 829 + extendedTest('test 1', ({ testFixture, fileFixture, workerFixture }) => { 830 + expectTypeOf(workerFixture).toEqualTypeOf<boolean>() 831 + expectTypeOf(fileFixture).toEqualTypeOf<number>() 832 + expectTypeOf(testFixture).toEqualTypeOf<string>() 833 + expect(workerFixture).toBe(true) 834 + expect(fileFixture).toBe(42) 835 + expect(testFixture).toBe('test-42-true') 836 + }) 837 + }, 838 + }) 839 + expect(stderr).toBe('') 840 + expect(fixtures).toMatchInlineSnapshot(` 841 + ">> fixture | workerFixture setup | test 1 842 + >> fixture | fileFixture setup true | test 1 843 + >> fixture | testFixture setup 42 true | test 1 844 + >> fixture | testFixture teardown | test 1 845 + >> fixture | fileFixture teardown | test 1 846 + >> fixture | workerFixture teardown | test 1" 847 + `) 848 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > test 1 <time>"`) 849 + }) 850 + 851 + describe('scoped fixtures type safety', () => { 852 + test('types are correctly inferred for scoped fixtures', async () => { 853 + const { stderr, fixtures, tests } = await runFixtureTests(({ log, expectTypeOf }) => it.extend<{ 854 + $worker: { workerValue: boolean } 855 + $file: { fileValue: number } 856 + $test: { testValue: string } 857 + }>({ 858 + workerValue: [async ({}, use) => { 859 + log('workerValue setup') 860 + await use(true) 861 + log('workerValue teardown') 862 + }, { scope: 'worker' }], 863 + fileValue: [async ({ workerValue }, use) => { 864 + // Confirm file fixture can access worker fixtures (workerValue is typed as boolean) 865 + expectTypeOf(workerValue).toEqualTypeOf<boolean>() 866 + log('fileValue setup', workerValue) 867 + await use(workerValue ? 42 : 0) 868 + log('fileValue teardown') 869 + }, { scope: 'file' }], 870 + testValue: async ({ fileValue, workerValue }, use) => { 871 + // Confirm test fixture can access both file and worker fixtures 872 + expectTypeOf(fileValue).toEqualTypeOf<number>() 873 + expectTypeOf(workerValue).toEqualTypeOf<boolean>() 874 + log('testValue setup', fileValue, workerValue) 875 + await use(`${fileValue}-${workerValue}`) 876 + log('testValue teardown') 877 + }, 878 + }), { 879 + 'basic.test.ts': ({ extendedTest, expect, expectTypeOf }) => { 880 + extendedTest('has correct types', ({ workerValue, fileValue, testValue }) => { 881 + // Verify the values and types are correct 882 + expectTypeOf(workerValue).toEqualTypeOf<boolean>() 883 + expectTypeOf(fileValue).toEqualTypeOf<number>() 884 + expectTypeOf(testValue).toEqualTypeOf<string>() 885 + expect(workerValue).toBe(true) 886 + expect(fileValue).toBe(42) 887 + expect(testValue).toBe('42-true') 888 + }) 889 + }, 890 + }) 891 + expect(stderr).toBe('') 892 + expect(fixtures).toMatchInlineSnapshot(` 893 + ">> fixture | workerValue setup | has correct types 894 + >> fixture | fileValue setup true | has correct types 895 + >> fixture | testValue setup 42 true | has correct types 896 + >> fixture | testValue teardown | has correct types 897 + >> fixture | fileValue teardown | has correct types 898 + >> fixture | workerValue teardown | has correct types" 899 + `) 900 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > has correct types <time>"`) 901 + }) 902 + 903 + test('file fixture cannot access test-scoped fixtures (runtime error)', async () => { 904 + const { stderr } = await runFixtureTests(({ log, expectTypeOf: _expectTypeOf }) => it.extend<{ 905 + $worker: { workerValue: boolean } 906 + $file: { fileValue: number } 907 + $test: { testValue: string } 908 + }>({ 909 + workerValue: [async ({}, use) => { 910 + log('workerValue setup') 911 + await use(true) 912 + log('workerValue teardown') 913 + }, { scope: 'worker' }], 914 + // @ts-expect-error - file fixture cannot access test-scoped fixture 'testValue' 915 + fileValue: [async ({ testValue }, use) => { 916 + log('fileValue setup') 917 + await use(testValue.length) 918 + log('fileValue teardown') 919 + }, { scope: 'file' }], 920 + testValue: async ({}, use) => { 921 + log('testValue setup') 922 + await use('test') 923 + log('testValue teardown') 924 + }, 925 + }), { 926 + 'basic.test.ts': ({ extendedTest }) => { 927 + extendedTest('should fail', ({ fileValue: _fileValue }) => {}) 928 + }, 929 + }) 930 + expect(stderr).toMatchInlineSnapshot(` 931 + " 932 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 933 + 934 + FAIL basic.test.ts [ basic.test.ts ] 935 + FixtureDependencyError: The file "fileValue" fixture cannot depend on a test fixture "testValue". 936 + ❯ test.js:4:75 937 + 2| export const expect = globalThis.expect 938 + 3| export const expectTypeOf = globalThis.expectTypeOf 939 + 4| export const extendedTest = (({ log, expectTypeOf: _expectTypeOf }) =>… 940 + | ^ 941 + 5| workerValue: [async ({}, use) => { 942 + 6| log("workerValue setup"); 943 + ❯ test.js:21:4 944 + 945 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 946 + 947 + " 948 + `) 949 + }) 950 + 951 + test('worker fixture cannot access file-scoped fixtures (runtime error)', async () => { 952 + const { stderr } = await runFixtureTests(({ log, expectTypeOf: _expectTypeOf }) => it.extend<{ 953 + $worker: { workerValue: boolean } 954 + $file: { fileValue: number } 955 + $test: { testValue: string } 956 + }>({ 957 + // @ts-expect-error - worker fixture cannot access file-scoped fixture 'fileValue' 958 + workerValue: [async ({ fileValue }, use) => { 959 + log('workerValue setup') 960 + await use(fileValue > 0) 961 + log('workerValue teardown') 962 + }, { scope: 'worker' }], 963 + fileValue: [async ({}, use) => { 964 + log('fileValue setup') 965 + await use(42) 966 + log('fileValue teardown') 967 + }, { scope: 'file' }], 968 + testValue: async ({}, use) => { 969 + log('testValue setup') 970 + await use('test') 971 + log('testValue teardown') 972 + }, 973 + }), { 974 + 'basic.test.ts': ({ extendedTest }) => { 975 + extendedTest('should fail', ({ workerValue: _workerValue }) => {}) 976 + }, 977 + }) 978 + expect(stderr).toMatchInlineSnapshot(` 979 + " 980 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 981 + 982 + FAIL basic.test.ts [ basic.test.ts ] 983 + FixtureDependencyError: The worker "workerValue" fixture cannot depend on a file fixture "fileValue". 984 + ❯ test.js:4:75 985 + 2| export const expect = globalThis.expect 986 + 3| export const expectTypeOf = globalThis.expectTypeOf 987 + 4| export const extendedTest = (({ log, expectTypeOf: _expectTypeOf }) =>… 988 + | ^ 989 + 5| // @ts-expect-error - worker fixture cannot access file-scoped fixtu… 990 + 6| workerValue: [async ({ fileValue }, use) => { 991 + ❯ test.js:21:4 992 + 993 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 994 + 995 + " 996 + `) 997 + }) 998 + 999 + test('worker fixture cannot access test-scoped fixtures (runtime error)', async () => { 1000 + const { stderr } = await runFixtureTests(({ log, expectTypeOf: _expectTypeOf }) => it.extend<{ 1001 + $worker: { workerValue: boolean } 1002 + $file: { fileValue: number } 1003 + $test: { testValue: string } 1004 + }>({ 1005 + // @ts-expect-error - worker fixture cannot access test-scoped fixture 'testValue' 1006 + workerValue: [async ({ testValue }, use) => { 1007 + log('workerValue setup') 1008 + await use(testValue.length > 0) 1009 + log('workerValue teardown') 1010 + }, { scope: 'worker' }], 1011 + fileValue: [async ({}, use) => { 1012 + log('fileValue setup') 1013 + await use(42) 1014 + log('fileValue teardown') 1015 + }, { scope: 'file' }], 1016 + testValue: async ({}, use) => { 1017 + log('testValue setup') 1018 + await use('test') 1019 + log('testValue teardown') 1020 + }, 1021 + }), { 1022 + 'basic.test.ts': ({ extendedTest }) => { 1023 + extendedTest('should fail', ({ workerValue: _workerValue }) => {}) 1024 + }, 1025 + }) 1026 + expect(stderr).toMatchInlineSnapshot(` 1027 + " 1028 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 1029 + 1030 + FAIL basic.test.ts [ basic.test.ts ] 1031 + FixtureDependencyError: The worker "workerValue" fixture cannot depend on a test fixture "testValue". 1032 + ❯ test.js:4:75 1033 + 2| export const expect = globalThis.expect 1034 + 3| export const expectTypeOf = globalThis.expectTypeOf 1035 + 4| export const extendedTest = (({ log, expectTypeOf: _expectTypeOf }) =>… 1036 + | ^ 1037 + 5| // @ts-expect-error - worker fixture cannot access test-scoped fixtu… 1038 + 6| workerValue: [async ({ testValue }, use) => { 1039 + ❯ test.js:21:4 1040 + 1041 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 1042 + 1043 + " 1044 + `) 1045 + }) 1046 + 1047 + test('scoped fixtures accept additional options (auto)', async () => { 1048 + const { stderr, fixtures, tests } = await runFixtureTests(({ log, expectTypeOf: _expectTypeOf }) => it.extend<{ 1049 + $worker: { workerValue: boolean } 1050 + $file: { fileValue: number } 1051 + }>({ 1052 + workerValue: [async ({}, use) => { 1053 + log('workerValue setup') 1054 + await use(true) 1055 + log('workerValue teardown') 1056 + }, { scope: 'worker', auto: true }], 1057 + fileValue: [async ({}, use) => { 1058 + log('fileValue setup') 1059 + await use(42) 1060 + log('fileValue teardown') 1061 + }, { scope: 'file', auto: true }], 1062 + }), { 1063 + 'basic.test.ts': ({ extendedTest }) => { 1064 + // auto fixtures should initialize even without being explicitly requested 1065 + extendedTest('auto fixtures work', ({}) => {}) 1066 + }, 1067 + }) 1068 + expect(stderr).toBe('') 1069 + // Auto fixtures initialize even when not explicitly requested 1070 + expect(fixtures).toMatchInlineSnapshot(` 1071 + ">> fixture | workerValue setup | auto fixtures work 1072 + >> fixture | fileValue setup | auto fixtures work 1073 + >> fixture | fileValue teardown | auto fixtures work 1074 + >> fixture | workerValue teardown | auto fixtures work" 1075 + `) 1076 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > auto fixtures work <time>"`) 1077 + }) 1078 + 1079 + test('file fixture cannot access TestContext properties like task (type and runtime error)', async () => { 1080 + const { stderr } = await runFixtureTests(({ log, expectTypeOf: _expectTypeOf }) => it.extend<{ 1081 + $file: { fileValue: string } 1082 + $test: { testValue: number } 1083 + }>({ 1084 + // @ts-expect-error - file fixture cannot access 'task' from TestContext 1085 + fileValue: [async ({ task }, use) => { 1086 + log('fileValue setup') 1087 + await use(task.name) 1088 + log('fileValue teardown') 1089 + }, { scope: 'file' }], 1090 + testValue: async ({}, use) => { 1091 + log('testValue setup') 1092 + await use(42) 1093 + log('testValue teardown') 1094 + }, 1095 + }), { 1096 + 'basic.test.ts': ({ extendedTest }) => { 1097 + extendedTest('should fail', ({ fileValue: _fileValue }) => {}) 1098 + }, 1099 + }) 1100 + // Runtime error because 'task' is not available in file fixtures 1101 + expect(stderr).toMatchInlineSnapshot(` 1102 + " 1103 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 1104 + 1105 + FAIL basic.test.ts > should fail 1106 + TypeError: Cannot read properties of undefined (reading 'name') 1107 + ❯ it.extend.fileValue.scope test.js:8:20 1108 + 6| fileValue: [async ({ task }, use) => { 1109 + 7| log("fileValue setup"); 1110 + 8| await use(task.name); 1111 + | ^ 1112 + 9| log("fileValue teardown"); 1113 + 10| }, { scope: "file" }], 1114 + 1115 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 1116 + 1117 + " 1118 + `) 1119 + }) 1120 + 1121 + test('worker fixture cannot access TestContext properties like task (type and runtime error)', async () => { 1122 + const { stderr } = await runFixtureTests(({ log, expectTypeOf: _expectTypeOf }) => it.extend<{ 1123 + $worker: { workerValue: string } 1124 + $test: { testValue: number } 1125 + }>({ 1126 + // @ts-expect-error - worker fixture cannot access 'task' from TestContext 1127 + workerValue: [async ({ task }, use) => { 1128 + log('workerValue setup') 1129 + await use(task.name) 1130 + log('workerValue teardown') 1131 + }, { scope: 'worker' }], 1132 + testValue: async ({}, use) => { 1133 + log('testValue setup') 1134 + await use(42) 1135 + log('testValue teardown') 1136 + }, 1137 + }), { 1138 + 'basic.test.ts': ({ extendedTest }) => { 1139 + extendedTest('should fail', ({ workerValue: _workerValue }) => {}) 1140 + }, 1141 + }) 1142 + // Runtime error because 'task' is not available in worker fixtures 1143 + expect(stderr).toMatchInlineSnapshot(` 1144 + " 1145 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 1146 + 1147 + FAIL basic.test.ts > should fail 1148 + TypeError: Cannot read properties of undefined (reading 'name') 1149 + ❯ it.extend.workerValue.scope test.js:8:20 1150 + 6| workerValue: [async ({ task }, use) => { 1151 + 7| log("workerValue setup"); 1152 + 8| await use(task.name); 1153 + | ^ 1154 + 9| log("workerValue teardown"); 1155 + 10| }, { scope: "worker" }], 1156 + 1157 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 1158 + 1159 + " 1160 + `) 1161 + }) 1162 + }) 1163 + 1164 + describe('builder pattern API with automatic type inference', () => { 1165 + test('types are automatically inferred from return values', async () => { 1166 + const { stderr, fixtures, tests } = await runFixtureTests(({ log, expectTypeOf }) => { 1167 + return it 1168 + .extend('workerValue', { scope: 'worker' }, async () => { 1169 + log('workerValue setup') 1170 + return 123 1171 + }) 1172 + .extend('fileValue', { scope: 'file' }, async ({ workerValue }) => { 1173 + // TypeScript automatically knows workerValue is number 1174 + expectTypeOf(workerValue).toEqualTypeOf<number>() 1175 + log('fileValue setup', workerValue) 1176 + return workerValue > 100 1177 + }) 1178 + .extend('testValue', async ({ workerValue, fileValue }) => { 1179 + // TypeScript automatically knows both types 1180 + expectTypeOf(workerValue).toEqualTypeOf<number>() 1181 + expectTypeOf(fileValue).toEqualTypeOf<boolean>() 1182 + log('testValue setup', workerValue, fileValue) 1183 + return { num: workerValue, bool: fileValue } 1184 + }) 1185 + }, { 1186 + 'basic.test.ts': ({ extendedTest, expect, expectTypeOf }) => { 1187 + extendedTest('builder pattern provides correct types and values', ({ workerValue, fileValue, testValue }) => { 1188 + // Verify types are automatically inferred 1189 + expectTypeOf(workerValue).toEqualTypeOf<number>() 1190 + expectTypeOf(fileValue).toEqualTypeOf<boolean>() 1191 + expectTypeOf(testValue).toEqualTypeOf<{ num: number; bool: boolean }>() 1192 + 1193 + // Verify values 1194 + expect(workerValue).toBe(123) 1195 + expect(fileValue).toBe(true) 1196 + expect(testValue).toEqual({ num: 123, bool: true }) 1197 + }) 1198 + }, 1199 + }) 1200 + 1201 + expect(stderr).toBe('') 1202 + expect(fixtures).toMatchInlineSnapshot(` 1203 + ">> fixture | workerValue setup | builder pattern provides correct types and values 1204 + >> fixture | fileValue setup 123 | builder pattern provides correct types and values 1205 + >> fixture | testValue setup 123 true | builder pattern provides correct types and values" 1206 + `) 1207 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > builder pattern provides correct types and values <time>"`) 1208 + }) 1209 + 1210 + test('builder pattern without options defaults to test scope', async () => { 1211 + const { stderr, fixtures, tests } = await runFixtureTests(({ log, expectTypeOf }) => { 1212 + return it 1213 + // No need to pass {} when not using any dependencies 1214 + .extend('count', async () => { 1215 + log('count setup') 1216 + return 42 1217 + }) 1218 + .extend('doubled', async ({ count }) => { 1219 + expectTypeOf(count).toEqualTypeOf<number>() 1220 + log('doubled setup', count) 1221 + return count * 2 1222 + }) 1223 + }, { 1224 + 'basic.test.ts': ({ extendedTest, expect, expectTypeOf }) => { 1225 + extendedTest('test scope fixtures work', ({ count, doubled }) => { 1226 + expectTypeOf(count).toEqualTypeOf<number>() 1227 + expectTypeOf(doubled).toEqualTypeOf<number>() 1228 + 1229 + expect(count).toBe(42) 1230 + expect(doubled).toBe(84) 1231 + }) 1232 + }, 1233 + }) 1234 + 1235 + expect(stderr).toBe('') 1236 + expect(fixtures).toMatchInlineSnapshot(` 1237 + ">> fixture | count setup | test scope fixtures work 1238 + >> fixture | doubled setup 42 | test scope fixtures work" 1239 + `) 1240 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > test scope fixtures work <time>"`) 1241 + }) 1242 + 1243 + test('can mix builder pattern with object-based extend', async () => { 1244 + const { stderr, fixtures, tests } = await runFixtureTests(({ log, expectTypeOf }) => { 1245 + return it 1246 + .extend('first', async () => { 1247 + log('first setup') 1248 + return 'hello' 1249 + }) 1250 + .extend<{ second: number }>({ 1251 + second: 100, 1252 + }) 1253 + .extend('third', async ({ first, second }) => { 1254 + expectTypeOf(first).toEqualTypeOf<string>() 1255 + expectTypeOf(second).toEqualTypeOf<number>() 1256 + log('third setup', first, second) 1257 + return `${first}-${second}` 1258 + }) 1259 + }, { 1260 + 'basic.test.ts': ({ extendedTest, expect, expectTypeOf }) => { 1261 + extendedTest('mixed patterns work', ({ first, second, third }) => { 1262 + expectTypeOf(first).toEqualTypeOf<string>() 1263 + expectTypeOf(second).toEqualTypeOf<number>() 1264 + expectTypeOf(third).toEqualTypeOf<string>() 1265 + 1266 + expect(first).toBe('hello') 1267 + expect(second).toBe(100) 1268 + expect(third).toBe('hello-100') 1269 + }) 1270 + }, 1271 + }) 1272 + 1273 + expect(stderr).toBe('') 1274 + expect(fixtures).toMatchInlineSnapshot(` 1275 + ">> fixture | first setup | mixed patterns work 1276 + >> fixture | third setup hello 100 | mixed patterns work" 1277 + `) 1278 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > mixed patterns work <time>"`) 1279 + }) 1280 + 1281 + test('non-function values work in builder pattern', async () => { 1282 + const { stderr, tests } = await runFixtureTests(({ expectTypeOf }) => { 1283 + return it 1284 + .extend('stringValue', 'hello') 1285 + .extend('numberValue', 42) 1286 + .extend('arrayValue', [1, 2, 3]) 1287 + .extend('objectValue', { key: 'value' }) 1288 + .extend('combined', async ({ stringValue, numberValue }) => { 1289 + expectTypeOf(stringValue).toEqualTypeOf<string>() 1290 + expectTypeOf(numberValue).toEqualTypeOf<number>() 1291 + return `${stringValue}-${numberValue}` 1292 + }) 1293 + }, { 1294 + 'basic.test.ts': ({ extendedTest, expect, expectTypeOf }) => { 1295 + extendedTest('non-function values work', ({ stringValue, numberValue, arrayValue, objectValue, combined }) => { 1296 + expectTypeOf(stringValue).toEqualTypeOf<string>() 1297 + expectTypeOf(numberValue).toEqualTypeOf<number>() 1298 + expectTypeOf(arrayValue).toEqualTypeOf<number[]>() 1299 + expectTypeOf(objectValue).toEqualTypeOf<{ key: string }>() 1300 + expectTypeOf(combined).toEqualTypeOf<string>() 1301 + 1302 + expect(stringValue).toBe('hello') 1303 + expect(numberValue).toBe(42) 1304 + expect(arrayValue).toEqual([1, 2, 3]) 1305 + expect(objectValue).toEqual({ key: 'value' }) 1306 + expect(combined).toBe('hello-42') 1307 + }) 1308 + }, 1309 + }) 1310 + 1311 + expect(stderr).toBe('') 1312 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > non-function values work <time>"`) 1313 + }) 1314 + 1315 + test('object with injected is treated as an object', async () => { 1316 + const { stderr, tests } = await runFixtureTests(({}) => { 1317 + return it 1318 + .extend('object', { injected: true }) 1319 + }, { 1320 + 'basic.test.ts': ({ extendedTest, expect, expectTypeOf }) => { 1321 + extendedTest('object with injected is treated as an object', ({ object }) => { 1322 + expectTypeOf(object).toEqualTypeOf<{ injected: boolean }>() 1323 + expect(object).toEqual({ injected: true }) 1324 + }) 1325 + }, 1326 + }) 1327 + expect(stderr).toBe('') 1328 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > object with injected is treated as an object <time>"`) 1329 + }) 1330 + 1331 + test('non-function values with injected option work', async () => { 1332 + const { stderr, tests } = await runFixtureTests(({ expectTypeOf }) => { 1333 + return it 1334 + // Static values only support 'injected' option 1335 + .extend('apiUrl', { injected: true }, 'https://api.example.com') 1336 + .extend('config', { port: 3000, host: 'localhost' }) 1337 + .extend('computed', async ({ apiUrl, config }) => { 1338 + expectTypeOf(apiUrl).toEqualTypeOf<string>() 1339 + expectTypeOf(config).toEqualTypeOf<{ port: number; host: string }>() 1340 + return `${apiUrl}:${config.port}` 1341 + }) 1342 + }, { 1343 + 'basic.test.ts': ({ extendedTest, expect, expectTypeOf }) => { 1344 + extendedTest('non-function values with injected option work', ({ apiUrl, config, computed }) => { 1345 + expectTypeOf(apiUrl).toEqualTypeOf<string>() 1346 + expectTypeOf(config).toEqualTypeOf<{ port: number; host: string }>() 1347 + expectTypeOf(computed).toEqualTypeOf<string>() 1348 + 1349 + expect(apiUrl).toBe('https://injected.example.com') 1350 + expect(config).toEqual({ port: 3000, host: 'localhost' }) 1351 + expect(computed).toBe('https://injected.example.com:3000') 1352 + }) 1353 + }, 1354 + 'vitest.config.js': { 1355 + test: { 1356 + provide: { 1357 + apiUrl: 'https://injected.example.com', 1358 + } as any, // requires type pollution otherwise 1359 + }, 1360 + }, 1361 + }) 1362 + 1363 + expect(stderr).toBe('') 1364 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > non-function values with injected option work <time>"`) 1365 + }) 1366 + 1367 + test('extending the extended', async () => { 1368 + const { stderr, tests } = await runFixtureTests(() => { 1369 + return it 1370 + .extend('apiUrl', 'https://api.example.com') 1371 + .extend( 1372 + 'apiUrl', 1373 + // @ts-expect-error false should be string 1374 + false, 1375 + ) 1376 + }, { 1377 + 'basic.test.ts': ({ extendedTest, expect, expectTypeOf }) => { 1378 + extendedTest('onCleanup works', ({ apiUrl }) => { 1379 + expectTypeOf(apiUrl).toEqualTypeOf<string>() 1380 + // runtime overrides, but ts shows an error 1381 + // we don't enforce it because it's possible to provide objects 1382 + // which are hard to compare at runtime 1383 + expect(apiUrl).toBe(false) 1384 + }) 1385 + }, 1386 + }) 1387 + expect(stderr).toBe('') 1388 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > onCleanup works <time>"`) 1389 + }) 1390 + 1391 + test('dependencies in extended extend use the new extended value', async () => { 1392 + const { stderr, tests } = await runFixtureTests(() => { 1393 + return it 1394 + .extend('a', () => 1) 1395 + .extend('b', ({ a }) => a + 1) 1396 + .extend('a', () => 100) 1397 + }, { 1398 + 'basic.test.ts': ({ extendedTest, expect }) => { 1399 + extendedTest('direct access returns new value', ({ a }) => { 1400 + expect(a).toBe(100) 1401 + }) 1402 + 1403 + extendedTest('dependent access returns new value', ({ b }) => { 1404 + expect(b).toBe(101) 1405 + }) 1406 + }, 1407 + }) 1408 + expect(stderr).toBe('') 1409 + expect(tests).toMatchInlineSnapshot(` 1410 + " ✓ basic.test.ts > direct access returns new value <time> 1411 + ✓ basic.test.ts > dependent access returns new value <time>" 1412 + `) 1413 + }) 1414 + 1415 + test('onCleanup registers teardown function', async () => { 1416 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => { 1417 + return it.extend('resource', { scope: 'file' }, async ({}, { onCleanup }) => { 1418 + const resource = { id: 42, data: 'test' } 1419 + log('resource setup') 1420 + onCleanup(() => { 1421 + log('resource cleanup', resource.id) 1422 + }) 1423 + return resource 1424 + }) 1425 + }, { 1426 + 'basic.test.ts': ({ extendedTest, expect }) => { 1427 + extendedTest('onCleanup works', ({ resource }) => { 1428 + expect(resource).toEqual({ id: 42, data: 'test' }) 1429 + }) 1430 + }, 1431 + }) 1432 + 1433 + expect(stderr).toBe('') 1434 + expect(fixtures).toMatchInlineSnapshot(` 1435 + ">> fixture | resource setup | onCleanup works 1436 + >> fixture | resource cleanup 42 | onCleanup works" 1437 + `) 1438 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > onCleanup works <time>"`) 1439 + }) 1440 + 1441 + test('builder pattern with auto option', async () => { 1442 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => { 1443 + return it 1444 + .extend('autoValue', { auto: true }, async () => { 1445 + log('autoValue setup') 1446 + return 'auto-initialized' 1447 + }) 1448 + .extend('regularValue', async () => { 1449 + log('regularValue setup') 1450 + return 'regular' 1451 + }) 1452 + }, { 1453 + 'basic.test.ts': ({ extendedTest, expect }) => { 1454 + extendedTest('auto fixture is initialized even when not used', ({ regularValue }) => { 1455 + expect(regularValue).toBe('regular') 1456 + }) 1457 + }, 1458 + }) 1459 + 1460 + expect(stderr).toBe('') 1461 + expect(fixtures).toMatchInlineSnapshot(` 1462 + ">> fixture | autoValue setup | auto fixture is initialized even when not used 1463 + >> fixture | regularValue setup | auto fixture is initialized even when not used" 1464 + `) 1465 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > auto fixture is initialized even when not used <time>"`) 1466 + }) 1467 + 1468 + test('onCleanup can only be called once per fixture', async () => { 1469 + const { stderr } = await runFixtureTests(({ log }) => { 1470 + return it.extend('resource', async ({}, { onCleanup }) => { 1471 + log('resource setup') 1472 + onCleanup(() => log('cleanup 1')) 1473 + onCleanup(() => log('cleanup 2')) // This should throw 1474 + return 'value' 1475 + }) 1476 + }, { 1477 + 'basic.test.ts': ({ extendedTest, expect }) => { 1478 + extendedTest('should fail because onCleanup called twice', ({ resource }) => { 1479 + expect(resource).toBe('value') 1480 + }) 1481 + }, 1482 + }) 1483 + expect(stderr).toMatchInlineSnapshot(` 1484 + " 1485 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 1486 + 1487 + FAIL basic.test.ts > should fail because onCleanup called twice 1488 + Error: onCleanup can only be called once per fixture. Define separate fixtures if you need multiple cleanup functions. 1489 + ❯ test.js:8:5 1490 + 6| log("resource setup"); 1491 + 7| onCleanup(() => log("cleanup 1")); 1492 + 8| onCleanup(() => log("cleanup 2")); 1493 + | ^ 1494 + 9| return "value"; 1495 + 10| }); 1496 + 1497 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 1498 + 1499 + " 1500 + `) 1501 + }) 1502 + 1503 + test('nested fixtures cleanup in correct order (dependent cleaned up first)', async () => { 1504 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => { 1505 + return it 1506 + .extend('base', async ({}, { onCleanup }) => { 1507 + log('base setup') 1508 + onCleanup(() => log('base cleanup')) 1509 + return 1 1510 + }) 1511 + .extend('middle', async ({ base }, { onCleanup }) => { 1512 + log('middle setup', base) 1513 + onCleanup(() => log('middle cleanup')) 1514 + return base + 10 1515 + }) 1516 + .extend('top', async ({ middle }, { onCleanup }) => { 1517 + log('top setup', middle) 1518 + onCleanup(() => log('top cleanup')) 1519 + return middle + 100 1520 + }) 1521 + }, { 1522 + 'basic.test.ts': ({ extendedTest, expect }) => { 1523 + extendedTest('fixtures are cleaned up in reverse dependency order', ({ top }) => { 1524 + expect(top).toBe(111) 1525 + }) 1526 + }, 1527 + }) 1528 + 1529 + expect(stderr).toBe('') 1530 + expect(fixtures).toMatchInlineSnapshot(` 1531 + ">> fixture | base setup | fixtures are cleaned up in reverse dependency order 1532 + >> fixture | middle setup 1 | fixtures are cleaned up in reverse dependency order 1533 + >> fixture | top setup 11 | fixtures are cleaned up in reverse dependency order 1534 + >> fixture | top cleanup | fixtures are cleaned up in reverse dependency order 1535 + >> fixture | middle cleanup | fixtures are cleaned up in reverse dependency order 1536 + >> fixture | base cleanup | fixtures are cleaned up in reverse dependency order" 1537 + `) 1538 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > fixtures are cleaned up in reverse dependency order <time>"`) 1539 + }) 1540 + 1541 + test('cleanup order across different scopes', async () => { 1542 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => { 1543 + return it 1544 + .extend('workerFixture', { scope: 'worker' }, async ({}, { onCleanup }) => { 1545 + log('worker setup') 1546 + onCleanup(() => log('worker cleanup')) 1547 + return 'worker' 1548 + }) 1549 + .extend('fileFixture', { scope: 'file' }, async ({ workerFixture }, { onCleanup }) => { 1550 + log('file setup', workerFixture) 1551 + onCleanup(() => log('file cleanup')) 1552 + return 'file' 1553 + }) 1554 + .extend('testFixture', async ({ fileFixture }, { onCleanup }) => { 1555 + log('test setup', fileFixture) 1556 + onCleanup(() => log('test cleanup')) 1557 + return 'test' 1558 + }) 1559 + }, { 1560 + 'basic.test.ts': ({ extendedTest, expect }) => { 1561 + extendedTest('scoped fixtures cleanup in order', ({ testFixture }) => { 1562 + expect(testFixture).toBe('test') 1563 + }) 1564 + }, 1565 + }) 1566 + 1567 + expect(stderr).toBe('') 1568 + expect(fixtures).toMatchInlineSnapshot(` 1569 + ">> fixture | worker setup | scoped fixtures cleanup in order 1570 + >> fixture | file setup worker | scoped fixtures cleanup in order 1571 + >> fixture | test setup file | scoped fixtures cleanup in order 1572 + >> fixture | test cleanup | scoped fixtures cleanup in order 1573 + >> fixture | file cleanup | scoped fixtures cleanup in order 1574 + >> fixture | worker cleanup | scoped fixtures cleanup in order" 1575 + `) 1576 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > scoped fixtures cleanup in order <time>"`) 1577 + }) 1578 + 1579 + test('deep fixture chain with all scopes and cleanups', async () => { 1580 + const { stderr, fixtures, tests } = await runFixtureTests(({ log, expectTypeOf }) => { 1581 + return it 1582 + .extend('config', { scope: 'worker' }, async ({}, { onCleanup }) => { 1583 + log('config setup') 1584 + onCleanup(() => log('config cleanup')) 1585 + return { port: 3000, host: 'localhost' } 1586 + }) 1587 + .extend('connection', { scope: 'worker' }, async ({ config }, { onCleanup }) => { 1588 + expectTypeOf(config).toEqualTypeOf<{ port: number; host: string }>() 1589 + log('connection setup', config.port) 1590 + onCleanup(() => log('connection cleanup')) 1591 + return `${config.host}:${config.port}` 1592 + }) 1593 + .extend('database', { scope: 'file' }, async ({ connection }, { onCleanup }) => { 1594 + expectTypeOf(connection).toEqualTypeOf<string>() 1595 + log('database setup', connection) 1596 + onCleanup(() => log('database cleanup')) 1597 + return { url: connection, connected: true } 1598 + }) 1599 + .extend('transaction', async ({ database }, { onCleanup }) => { 1600 + expectTypeOf(database).toEqualTypeOf<{ url: string; connected: boolean }>() 1601 + log('transaction setup', database.connected) 1602 + onCleanup(() => log('transaction rollback')) 1603 + return { id: 1, db: database } 1604 + }) 1605 + .extend('query', async ({ transaction }) => { 1606 + expectTypeOf(transaction).toEqualTypeOf<{ id: number; db: { url: string; connected: boolean } }>() 1607 + log('query setup', transaction.id) 1608 + return `SELECT * FROM ${transaction.id}` 1609 + }) 1610 + }, { 1611 + 'basic.test.ts': ({ extendedTest, expect, expectTypeOf }) => { 1612 + extendedTest('deep chain works correctly', ({ config, connection, database, transaction, query }) => { 1613 + expectTypeOf(config).toEqualTypeOf<{ port: number; host: string }>() 1614 + expectTypeOf(connection).toEqualTypeOf<string>() 1615 + expectTypeOf(database).toEqualTypeOf<{ url: string; connected: boolean }>() 1616 + expectTypeOf(transaction).toEqualTypeOf<{ id: number; db: { url: string; connected: boolean } }>() 1617 + expectTypeOf(query).toEqualTypeOf<string>() 1618 + 1619 + expect(config).toEqual({ port: 3000, host: 'localhost' }) 1620 + expect(connection).toBe('localhost:3000') 1621 + expect(database).toEqual({ url: 'localhost:3000', connected: true }) 1622 + expect(transaction).toEqual({ id: 1, db: { url: 'localhost:3000', connected: true } }) 1623 + expect(query).toBe('SELECT * FROM 1') 1624 + }) 1625 + }, 1626 + }) 1627 + 1628 + expect(stderr).toBe('') 1629 + expect(fixtures).toMatchInlineSnapshot(` 1630 + ">> fixture | config setup | deep chain works correctly 1631 + >> fixture | connection setup 3000 | deep chain works correctly 1632 + >> fixture | database setup localhost:3000 | deep chain works correctly 1633 + >> fixture | transaction setup true | deep chain works correctly 1634 + >> fixture | query setup 1 | deep chain works correctly 1635 + >> fixture | transaction rollback | deep chain works correctly 1636 + >> fixture | database cleanup | deep chain works correctly 1637 + >> fixture | connection cleanup | deep chain works correctly 1638 + >> fixture | config cleanup | deep chain works correctly" 1639 + `) 1640 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > deep chain works correctly <time>"`) 1641 + }) 1642 + 1643 + test('fixture without onCleanup works correctly', async () => { 1644 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => { 1645 + return it 1646 + .extend('noCleanup', async () => { 1647 + log('noCleanup setup') 1648 + return 'no cleanup needed' 1649 + }) 1650 + .extend('withCleanup', async ({ noCleanup }, { onCleanup }) => { 1651 + log('withCleanup setup', noCleanup) 1652 + onCleanup(() => log('withCleanup cleanup')) 1653 + return 'has cleanup' 1654 + }) 1655 + }, { 1656 + 'basic.test.ts': ({ extendedTest, expect }) => { 1657 + extendedTest('mixed cleanup works', ({ noCleanup, withCleanup }) => { 1658 + expect(noCleanup).toBe('no cleanup needed') 1659 + expect(withCleanup).toBe('has cleanup') 1660 + }) 1661 + }, 1662 + }) 1663 + 1664 + expect(stderr).toBe('') 1665 + expect(fixtures).toMatchInlineSnapshot(` 1666 + ">> fixture | noCleanup setup | mixed cleanup works 1667 + >> fixture | withCleanup setup no cleanup needed | mixed cleanup works 1668 + >> fixture | withCleanup cleanup | mixed cleanup works" 1669 + `) 1670 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > mixed cleanup works <time>"`) 1671 + }) 1672 + 1673 + test('fixture reuses value across multiple tests in same scope', async () => { 1674 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => { 1675 + return it 1676 + .extend('fileCounter', { scope: 'file' }, async ({}, { onCleanup }) => { 1677 + log('fileCounter setup') 1678 + onCleanup(() => log('fileCounter cleanup')) 1679 + return { count: 0 } 1680 + }) 1681 + .extend('testValue', async ({ fileCounter }) => { 1682 + fileCounter.count++ 1683 + log('testValue setup', fileCounter.count) 1684 + return fileCounter.count 1685 + }) 1686 + }, { 1687 + 'basic.test.ts': ({ extendedTest, expect }) => { 1688 + extendedTest('first test', ({ testValue }) => { 1689 + expect(testValue).toBe(1) 1690 + }) 1691 + extendedTest('second test', ({ testValue }) => { 1692 + expect(testValue).toBe(2) 1693 + }) 1694 + extendedTest('third test', ({ testValue }) => { 1695 + expect(testValue).toBe(3) 1696 + }) 1697 + }, 1698 + }) 1699 + 1700 + expect(stderr).toBe('') 1701 + expect(fixtures).toMatchInlineSnapshot(` 1702 + ">> fixture | fileCounter setup | first test 1703 + >> fixture | testValue setup 1 | first test 1704 + >> fixture | testValue setup 2 | second test 1705 + >> fixture | testValue setup 3 | third test 1706 + >> fixture | fileCounter cleanup | third test" 1707 + `) 1708 + expect(tests).toMatchInlineSnapshot(` 1709 + " ✓ basic.test.ts > first test <time> 1710 + ✓ basic.test.ts > second test <time> 1711 + ✓ basic.test.ts > third test <time>" 1712 + `) 1713 + }) 1714 + 1715 + test('async cleanup functions work correctly', async () => { 1716 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => { 1717 + return it.extend('asyncResource', async ({}, { onCleanup }) => { 1718 + log('asyncResource setup') 1719 + onCleanup(async () => { 1720 + log('async cleanup start') 1721 + await new Promise(resolve => setTimeout(resolve, 10)) 1722 + log('async cleanup done') 1723 + }) 1724 + return 'async' 1725 + }) 1726 + }, { 1727 + 'basic.test.ts': ({ extendedTest, expect }) => { 1728 + extendedTest('async cleanup works', ({ asyncResource }) => { 1729 + expect(asyncResource).toBe('async') 1730 + }) 1731 + }, 1732 + }) 1733 + 1734 + expect(stderr).toBe('') 1735 + expect(fixtures).toMatchInlineSnapshot(` 1736 + ">> fixture | asyncResource setup | async cleanup works 1737 + >> fixture | async cleanup start | async cleanup works 1738 + >> fixture | async cleanup done | async cleanup works" 1739 + `) 1740 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > async cleanup works <time>"`) 1741 + }) 1742 + 1743 + test('file fixture cannot access test fixture (runtime and type error)', async () => { 1744 + const { stderr } = await runFixtureTests(({ log }) => { 1745 + return it 1746 + .extend('testValue', async () => { 1747 + log('testValue setup') 1748 + return 'test' 1749 + }) 1750 + // @ts-expect-error - file fixture cannot access test-scoped fixture 'testValue' 1751 + .extend('fileValue', { scope: 'file' }, async ({ testValue }) => { 1752 + log('fileValue setup', testValue) 1753 + return testValue.length 1754 + }) 1755 + }, { 1756 + 'basic.test.ts': ({ extendedTest }) => { 1757 + extendedTest('should fail', ({ fileValue: _fileValue }) => {}) 1758 + }, 1759 + }) 1760 + expect(stderr).toMatchInlineSnapshot(` 1761 + " 1762 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 1763 + 1764 + FAIL basic.test.ts [ basic.test.ts ] 1765 + FixtureDependencyError: The file "fileValue" fixture cannot depend on a test fixture "testValue". 1766 + ❯ test.js:8:6 1767 + 6| log("testValue setup"); 1768 + 7| return "test"; 1769 + 8| }).extend("fileValue", { scope: "file" }, async ({ testValue }) => { 1770 + | ^ 1771 + 9| log("fileValue setup", testValue); 1772 + 10| return testValue.length; 1773 + ❯ test.js:12:3 1774 + 1775 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 1776 + 1777 + " 1778 + `) 1779 + }) 1780 + 1781 + test('worker fixture cannot access file fixture (runtime and type error)', async () => { 1782 + const { stderr } = await runFixtureTests(({ log }) => { 1783 + return it 1784 + .extend('fileValue', { scope: 'file' }, async () => { 1785 + log('fileValue setup') 1786 + return 42 1787 + }) 1788 + // @ts-expect-error - worker fixture cannot access file-scoped fixture 'fileValue' 1789 + .extend('workerValue', { scope: 'worker' }, async ({ fileValue }) => { 1790 + log('workerValue setup', fileValue) 1791 + return fileValue > 0 1792 + }) 1793 + }, { 1794 + 'basic.test.ts': ({ extendedTest }) => { 1795 + extendedTest('should fail', ({ workerValue: _workerValue }) => {}) 1796 + }, 1797 + }) 1798 + expect(stderr).toMatchInlineSnapshot(` 1799 + " 1800 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 1801 + 1802 + FAIL basic.test.ts [ basic.test.ts ] 1803 + FixtureDependencyError: The worker "workerValue" fixture cannot depend on a file fixture "fileValue". 1804 + ❯ test.js:8:6 1805 + 6| log("fileValue setup"); 1806 + 7| return 42; 1807 + 8| }).extend("workerValue", { scope: "worker" }, async ({ fileValue }) … 1808 + | ^ 1809 + 9| log("workerValue setup", fileValue); 1810 + 10| return fileValue > 0; 1811 + ❯ test.js:12:3 1812 + 1813 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 1814 + 1815 + " 1816 + `) 1817 + }) 1818 + 1819 + test('worker fixture cannot access test fixture (runtime and type error)', async () => { 1820 + const { stderr } = await runFixtureTests(({ log }) => { 1821 + return it 1822 + .extend('testValue', async () => { 1823 + log('testValue setup') 1824 + return 'test' 1825 + }) 1826 + // @ts-expect-error - worker fixture cannot access test-scoped fixture 'testValue' 1827 + .extend('workerValue', { scope: 'worker' }, async ({ testValue }) => { 1828 + log('workerValue setup', testValue) 1829 + return testValue.length > 0 1830 + }) 1831 + }, { 1832 + 'basic.test.ts': ({ extendedTest }) => { 1833 + extendedTest('should fail', ({ workerValue: _workerValue }) => {}) 1834 + }, 1835 + }) 1836 + 1837 + expect(stderr).toMatchInlineSnapshot(` 1838 + " 1839 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 1840 + 1841 + FAIL basic.test.ts [ basic.test.ts ] 1842 + FixtureDependencyError: The worker "workerValue" fixture cannot depend on a test fixture "testValue". 1843 + ❯ test.js:8:6 1844 + 6| log("testValue setup"); 1845 + 7| return "test"; 1846 + 8| }).extend("workerValue", { scope: "worker" }, async ({ testValue }) … 1847 + | ^ 1848 + 9| log("workerValue setup", testValue); 1849 + 10| return testValue.length > 0; 1850 + ❯ test.js:12:3 1851 + 1852 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 1853 + 1854 + " 1855 + `) 1856 + }) 1857 + 1858 + test('cleanup error is reported', async () => { 1859 + const { stderr, fixtures } = await runFixtureTests(({ log }) => { 1860 + return it.extend('resource', async ({}, { onCleanup }) => { 1861 + log('resource setup') 1862 + onCleanup(() => { 1863 + log('cleanup - throwing') 1864 + throw new Error('cleanup error') 1865 + }) 1866 + return 'value' 1867 + }) 1868 + }, { 1869 + 'basic.test.ts': ({ extendedTest, expect }) => { 1870 + extendedTest('test runs but cleanup fails', ({ resource }) => { 1871 + expect(resource).toBe('value') 1872 + }) 1873 + }, 1874 + }) 1875 + 1876 + expect(fixtures).toMatchInlineSnapshot(` 1877 + ">> fixture | resource setup | test runs but cleanup fails 1878 + >> fixture | cleanup - throwing | test runs but cleanup fails" 1879 + `) 1880 + expect(stderr).toMatchInlineSnapshot(` 1881 + " 1882 + ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯ 1883 + 1884 + FAIL basic.test.ts > test runs but cleanup fails 1885 + FAIL basic.test.ts > test runs but cleanup fails 1886 + Error: cleanup error 1887 + ❯ test.js:9:13 1888 + 7| onCleanup(() => { 1889 + 8| log("cleanup - throwing"); 1890 + 9| throw new Error("cleanup error"); 1891 + | ^ 1892 + 10| }); 1893 + 11| return "value"; 1894 + 1895 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯ 1896 + 1897 + " 1898 + `) 1899 + }) 1900 + }) 1901 + 1902 + describe('test.override builder pattern', () => { 1903 + test('override with static value', async () => { 1904 + const { stderr, tests } = await runFixtureTests(({ expectTypeOf: _expectTypeOf }) => { 1905 + return it 1906 + .extend('config', { port: 3000, host: 'localhost' }) 1907 + .extend('url', ({ config }) => `http://${config.host}:${config.port}`) 1908 + }, { 1909 + 'basic.test.ts': ({ extendedTest, expect, expectTypeOf, describe }) => { 1910 + extendedTest('uses default', ({ config, url }) => { 1911 + expectTypeOf(config).toEqualTypeOf<{ port: number; host: string }>() 1912 + expect(config.port).toBe(3000) 1913 + expect(url).toBe('http://localhost:3000') 1914 + }) 1915 + 1916 + describe('with overwritten port', () => { 1917 + extendedTest.override('config', { port: 4000, host: 'localhost' }) 1918 + 1919 + extendedTest('uses overwritten value', ({ config, url }) => { 1920 + expect(config.port).toBe(4000) 1921 + expect(url).toBe('http://localhost:4000') 1922 + }) 1923 + }) 1924 + }, 1925 + }) 1926 + 1927 + expect(stderr).toBe('') 1928 + expect(tests).toMatchInlineSnapshot(` 1929 + " ✓ basic.test.ts > uses default <time> 1930 + ✓ basic.test.ts > with overwritten port > uses overwritten value <time>" 1931 + `) 1932 + }) 1933 + 1934 + test('override with function that uses dependencies from original test', async () => { 1935 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => { 1936 + return it 1937 + .extend('config', { port: 3000 }) 1938 + .extend('server', async ({ config }, { onCleanup }) => { 1939 + log('server setup', config.port) 1940 + onCleanup(() => log('server cleanup')) 1941 + return { port: config.port, running: true } 1942 + }) 1943 + }, { 1944 + 'basic.test.ts': ({ extendedTest, expect, describe }) => { 1945 + extendedTest('uses default server', ({ server }) => { 1946 + expect(server.port).toBe(3000) 1947 + }) 1948 + 1949 + describe('with custom server', () => { 1950 + // override with a function that uses 'config' from the original test 1951 + extendedTest.override('server', async ({ config }, { onCleanup }) => { 1952 + console.log('>> fixture | custom server setup', config.port, '|', expect.getState().currentTestName) 1953 + onCleanup(() => console.log('>> fixture | custom server cleanup |', expect.getState().currentTestName)) 1954 + return { port: config.port + 1000, running: false } 1955 + }) 1956 + 1957 + extendedTest('uses custom server', ({ server }) => { 1958 + expect(server.port).toBe(4000) 1959 + expect(server.running).toBe(false) 1960 + }) 1961 + }) 1962 + }, 1963 + }) 1964 + 1965 + expect(stderr).toBe('') 1966 + expect(fixtures).toMatchInlineSnapshot(` 1967 + ">> fixture | server setup 3000 | uses default server 1968 + >> fixture | server cleanup | uses default server 1969 + >> fixture | custom server setup 3000 | with custom server > uses custom server 1970 + >> fixture | custom server cleanup | with custom server > uses custom server" 1971 + `) 1972 + expect(tests).toMatchInlineSnapshot(` 1973 + " ✓ basic.test.ts > uses default server <time> 1974 + ✓ basic.test.ts > with custom server > uses custom server <time>" 1975 + `) 1976 + }) 1977 + 1978 + test('override with object syntax (backward compatible)', async () => { 1979 + const { stderr, tests } = await runFixtureTests(() => { 1980 + return it 1981 + .extend('value', 'original') 1982 + .extend('derived', ({ value }) => `derived-${value}`) 1983 + }, { 1984 + 'basic.test.ts': ({ extendedTest, expect, describe }) => { 1985 + extendedTest('uses default', ({ value, derived }) => { 1986 + expect(value).toBe('original') 1987 + expect(derived).toBe('derived-original') 1988 + }) 1989 + 1990 + describe('overwritten with object syntax', () => { 1991 + extendedTest.override({ value: 'overwritten' }) 1992 + 1993 + extendedTest('uses overwritten', ({ value, derived }) => { 1994 + expect(value).toBe('overwritten') 1995 + expect(derived).toBe('derived-overwritten') 1996 + }) 1997 + }) 1998 + }, 1999 + }) 2000 + 2001 + expect(stderr).toBe('') 2002 + expect(tests).toMatchInlineSnapshot(` 2003 + " ✓ basic.test.ts > uses default <time> 2004 + ✓ basic.test.ts > overwritten with object syntax > uses overwritten <time>" 2005 + `) 2006 + }) 2007 + 2008 + test('scoped is deprecated but still works', async () => { 2009 + const { stderr, tests } = await runFixtureTests(() => { 2010 + return it 2011 + .extend('value', 'original') 2012 + }, { 2013 + 'basic.test.ts': ({ extendedTest, expect, describe }) => { 2014 + describe('using deprecated scoped', () => { 2015 + // scoped is deprecated, use override instead 2016 + extendedTest.scoped({ value: 'scoped-value' }) 2017 + 2018 + extendedTest('scoped still works', ({ value }) => { 2019 + expect(value).toBe('scoped-value') 2020 + }) 2021 + }) 2022 + }, 2023 + }) 2024 + 2025 + expect(stderr).toContain('test.scoped() is deprecated and will be removed in future versions. Please use test.override() instead.') 2026 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > using deprecated scoped > scoped still works <time>"`) 2027 + }) 2028 + 2029 + test('override nested describe inheritance', async () => { 2030 + const { stderr, tests } = await runFixtureTests(() => { 2031 + return it 2032 + .extend('level', 'root') 2033 + }, { 2034 + 'basic.test.ts': ({ extendedTest, expect, describe }) => { 2035 + extendedTest('root level', ({ level }) => { 2036 + expect(level).toBe('root') 2037 + }) 2038 + 2039 + describe('level 1', () => { 2040 + extendedTest.override('level', 'one') 2041 + 2042 + extendedTest('at level 1', ({ level }) => { 2043 + expect(level).toBe('one') 2044 + }) 2045 + 2046 + describe('level 2', () => { 2047 + extendedTest.override('level', 'two') 2048 + 2049 + extendedTest('at level 2', ({ level }) => { 2050 + expect(level).toBe('two') 2051 + }) 2052 + }) 2053 + 2054 + extendedTest('still at level 1', ({ level }) => { 2055 + expect(level).toBe('one') 2056 + }) 2057 + }) 2058 + 2059 + extendedTest('back to root', ({ level }) => { 2060 + expect(level).toBe('root') 2061 + }) 2062 + }, 2063 + }) 2064 + 2065 + expect(stderr).toBe('') 2066 + expect(tests).toMatchInlineSnapshot(` 2067 + " ✓ basic.test.ts > root level <time> 2068 + ✓ basic.test.ts > level 1 > at level 1 <time> 2069 + ✓ basic.test.ts > level 1 > level 2 > at level 2 <time> 2070 + ✓ basic.test.ts > level 1 > still at level 1 <time> 2071 + ✓ basic.test.ts > back to root <time>" 2072 + `) 2073 + }) 2074 + 2075 + test('override with function that accesses other static fixtures', async () => { 2076 + const { stderr, tests } = await runFixtureTests(() => { 2077 + return it 2078 + .extend('basePort', 3000) 2079 + .extend('environment', 'development') 2080 + .extend('config', ({ basePort, environment }) => ({ 2081 + port: basePort, 2082 + env: environment, 2083 + debug: environment === 'development', 2084 + })) 2085 + }, { 2086 + 'basic.test.ts': ({ extendedTest, expect, describe }) => { 2087 + extendedTest('default config', ({ config }) => { 2088 + expect(config).toEqual({ port: 3000, env: 'development', debug: true }) 2089 + }) 2090 + 2091 + describe('production', () => { 2092 + // Chained overwrites 2093 + extendedTest 2094 + .override('environment', 'production') 2095 + .override('basePort', 8080) 2096 + 2097 + extendedTest('production config', ({ config }) => { 2098 + expect(config).toEqual({ port: 8080, env: 'production', debug: false }) 2099 + }) 2100 + }) 2101 + }, 2102 + }) 2103 + 2104 + expect(stderr).toBe('') 2105 + expect(tests).toMatchInlineSnapshot(` 2106 + " ✓ basic.test.ts > default config <time> 2107 + ✓ basic.test.ts > production > production config <time>" 2108 + `) 2109 + }) 2110 + 2111 + test('throws an error when overriding worker fixtures inside a suite', async () => { 2112 + const { stderr } = await runFixtureTests(() => { 2113 + return it 2114 + .extend('basePort', { scope: 'worker' }, () => 3000) 2115 + .extend('environment', { scope: 'worker' }, 'development') 2116 + .extend('config', { scope: 'worker' }, ({ basePort, environment }) => ({ 2117 + port: basePort, 2118 + env: environment, 2119 + debug: environment === 'development', 2120 + })) 2121 + }, { 2122 + 'basic.test.ts': ({ extendedTest, expect, describe }) => { 2123 + extendedTest('default config', ({ config }) => { 2124 + expect(config).toEqual({ port: 3000, env: 'development', debug: true }) 2125 + }) 2126 + 2127 + describe('production', () => { 2128 + // Chained overwrites 2129 + extendedTest 2130 + .override('environment', 'production') // scope automatically inherited 2131 + .override('basePort', () => 8080) 2132 + 2133 + extendedTest('production config', ({ config }) => { 2134 + expect(config).toEqual({ port: 8080, env: 'production', debug: false }) 2135 + }) 2136 + }) 2137 + }, 2138 + }) 2139 + 2140 + expect(stderr).toMatchInlineSnapshot(` 2141 + " 2142 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 2143 + 2144 + FAIL basic.test.ts [ basic.test.ts ] 2145 + FixtureDependencyError: The "environment" fixture cannot be defined with a worker scope (inherited from the base fixture) inside the describe block. Define it at the top level of the file instead. 2146 + ❯ basic.test.ts:8:24 2147 + 6| }); 2148 + 7| describe2("production", () => { 2149 + 8| extendedTest.override("environment", "production").override(… 2150 + | ^ 2151 + 9| extendedTest("production config", ({ config }) => { 2152 + 10| expect2(config).toEqual({ port: 8080, env: "production", d… 2153 + 2154 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 2155 + 2156 + " 2157 + `) 2158 + }) 2159 + 2160 + test('throws an error when overriding file fixtures inside a describe', async () => { 2161 + const { stderr } = await runFixtureTests(() => { 2162 + return it 2163 + .extend('basePort', { scope: 'file' }, () => 3000) 2164 + .extend('environment', { scope: 'file' }, 'development') 2165 + .extend('config', { scope: 'file' }, ({ basePort, environment }) => ({ 2166 + port: basePort, 2167 + env: environment, 2168 + debug: environment === 'development', 2169 + })) 2170 + }, { 2171 + 'basic.test.ts': ({ extendedTest, expect, describe }) => { 2172 + extendedTest('default config', ({ config }) => { 2173 + expect(config).toEqual({ port: 3000, env: 'development', debug: true }) 2174 + }) 2175 + 2176 + describe('production', () => { 2177 + // Chained overwrites 2178 + extendedTest 2179 + .override('environment', { scope: 'file' }, 'production') 2180 + .override('basePort', { scope: 'file' }, () => 8080) 2181 + 2182 + extendedTest('production config', ({ config }) => { 2183 + expect(config).toEqual({ port: 8080, env: 'production', debug: false }) 2184 + }) 2185 + }) 2186 + }, 2187 + }) 2188 + 2189 + expect(stderr).toMatchInlineSnapshot(` 2190 + " 2191 + ⎯⎯⎯⎯⎯⎯ Failed Suites 1 ⎯⎯⎯⎯⎯⎯⎯ 2192 + 2193 + FAIL basic.test.ts [ basic.test.ts ] 2194 + FixtureDependencyError: The "environment" fixture cannot be defined with a file scope inside the describe block. Define it at the top level of the file instead. 2195 + ❯ basic.test.ts:8:24 2196 + 6| }); 2197 + 7| describe2("production", () => { 2198 + 8| extendedTest.override("environment", { scope: "file" }, "pro… 2199 + | ^ 2200 + 9| extendedTest("production config", ({ config }) => { 2201 + 10| expect2(config).toEqual({ port: 8080, env: "production", d… 2202 + 2203 + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 2204 + 2205 + " 2206 + `) 2207 + }) 2208 + 2209 + test('top-level override without nested suites', async () => { 2210 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => { 2211 + return it 2212 + .extend('staticValue', 'original-static') 2213 + .extend('functionValue', () => 'original-function') 2214 + .extend('asyncValue', async ({}, { onCleanup }) => { 2215 + log('async setup') 2216 + onCleanup(() => log('async cleanup')) 2217 + return 'original-async' 2218 + }) 2219 + .extend('fileScoped', { scope: 'file' }, () => 'original-file') 2220 + .extend('workerScoped', { scope: 'worker' }, () => 'original-worker') 2221 + }, { 2222 + 'basic.test.ts': ({ extendedTest, expect }) => { 2223 + // Override at top level (no describe) 2224 + extendedTest 2225 + .override('staticValue', 'overridden-static') 2226 + .override('functionValue', () => 'overridden-function') 2227 + .override('asyncValue', async ({}, { onCleanup }) => { 2228 + console.log('>> fixture | overridden async setup |', expect.getState().currentTestName) 2229 + onCleanup(() => console.log('>> fixture | overridden async cleanup |', expect.getState().currentTestName)) 2230 + return 'overridden-async' 2231 + }) 2232 + .override('fileScoped', { scope: 'file' }, () => 'overridden-file') 2233 + .override('workerScoped', { scope: 'worker' }, () => 'overridden-worker') 2234 + 2235 + extendedTest('all fixtures are overridden', ({ staticValue, functionValue, asyncValue, fileScoped, workerScoped }) => { 2236 + expect(staticValue).toBe('overridden-static') 2237 + expect(functionValue).toBe('overridden-function') 2238 + expect(asyncValue).toBe('overridden-async') 2239 + expect(fileScoped).toBe('overridden-file') 2240 + expect(workerScoped).toBe('overridden-worker') 2241 + }) 2242 + 2243 + extendedTest('second test uses same overrides', ({ staticValue, functionValue, asyncValue }) => { 2244 + expect(staticValue).toBe('overridden-static') 2245 + expect(functionValue).toBe('overridden-function') 2246 + expect(asyncValue).toBe('overridden-async') 2247 + }) 2248 + }, 2249 + }) 2250 + 2251 + expect(stderr).toBe('') 2252 + expect(fixtures).toMatchInlineSnapshot(` 2253 + ">> fixture | overridden async setup | all fixtures are overridden 2254 + >> fixture | overridden async cleanup | all fixtures are overridden 2255 + >> fixture | overridden async setup | second test uses same overrides 2256 + >> fixture | overridden async cleanup | second test uses same overrides" 2257 + `) 2258 + expect(tests).toMatchInlineSnapshot(` 2259 + " ✓ basic.test.ts > all fixtures are overridden <time> 2260 + ✓ basic.test.ts > second test uses same overrides <time>" 2261 + `) 2262 + }) 2263 + 2264 + test('top-level override with nested suites', async () => { 2265 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => { 2266 + return it 2267 + .extend('staticValue', 'original-static') 2268 + .extend('functionValue', () => 'original-function') 2269 + .extend('asyncValue', async ({}, { onCleanup }) => { 2270 + log('async setup') 2271 + onCleanup(() => log('async cleanup')) 2272 + return 'original-async' 2273 + }) 2274 + .extend('fileScoped', { scope: 'file' }, () => 'original-file') 2275 + .extend('workerScoped', { scope: 'worker' }, () => 'original-worker') 2276 + }, { 2277 + 'basic.test.ts': ({ extendedTest, expect, describe }) => { 2278 + // Override at top level 2279 + extendedTest 2280 + .override('staticValue', 'top-static') 2281 + .override('functionValue', () => 'top-function') 2282 + .override('asyncValue', async ({}, { onCleanup }) => { 2283 + console.log('>> fixture | top async setup |', expect.getState().currentTestName) 2284 + onCleanup(() => console.log('>> fixture | top async cleanup |', expect.getState().currentTestName)) 2285 + return 'top-async' 2286 + }) 2287 + .override('fileScoped', { scope: 'file' }, () => 'top-file') 2288 + .override('workerScoped', { scope: 'worker' }, () => 'top-worker') 2289 + 2290 + extendedTest('top level uses overrides', ({ staticValue, functionValue, asyncValue, fileScoped, workerScoped }) => { 2291 + expect(staticValue).toBe('top-static') 2292 + expect(functionValue).toBe('top-function') 2293 + expect(asyncValue).toBe('top-async') 2294 + expect(fileScoped).toBe('top-file') 2295 + expect(workerScoped).toBe('top-worker') 2296 + }) 2297 + 2298 + describe('nested suite', () => { 2299 + // Override only static and function fixtures inside describe 2300 + extendedTest 2301 + .override('staticValue', 'nested-static') 2302 + .override('functionValue', () => 'nested-function') 2303 + 2304 + extendedTest('nested uses mixed overrides', ({ staticValue, functionValue, asyncValue }) => { 2305 + expect(staticValue).toBe('nested-static') 2306 + expect(functionValue).toBe('nested-function') 2307 + // asyncValue still uses top-level override 2308 + expect(asyncValue).toBe('top-async') 2309 + }) 2310 + 2311 + describe('deeply nested suite', () => { 2312 + extendedTest.override('staticValue', 'deep-static') 2313 + 2314 + extendedTest('deeply nested override', ({ staticValue, functionValue }) => { 2315 + expect(staticValue).toBe('deep-static') 2316 + expect(functionValue).toBe('nested-function') 2317 + }) 2318 + }) 2319 + }) 2320 + 2321 + extendedTest('back at top level', ({ staticValue, functionValue, asyncValue }) => { 2322 + expect(staticValue).toBe('top-static') 2323 + expect(functionValue).toBe('top-function') 2324 + expect(asyncValue).toBe('top-async') 2325 + }) 2326 + }, 2327 + }) 2328 + 2329 + expect(stderr).toBe('') 2330 + expect(fixtures).toMatchInlineSnapshot(` 2331 + ">> fixture | top async setup | top level uses overrides 2332 + >> fixture | top async cleanup | top level uses overrides 2333 + >> fixture | top async setup | nested suite > nested uses mixed overrides 2334 + >> fixture | top async cleanup | nested suite > nested uses mixed overrides 2335 + >> fixture | top async setup | back at top level 2336 + >> fixture | top async cleanup | back at top level" 2337 + `) 2338 + expect(tests).toMatchInlineSnapshot(` 2339 + " ✓ basic.test.ts > top level uses overrides <time> 2340 + ✓ basic.test.ts > nested suite > nested uses mixed overrides <time> 2341 + ✓ basic.test.ts > nested suite > deeply nested suite > deeply nested override <time> 2342 + ✓ basic.test.ts > back at top level <time>" 2343 + `) 2344 + }) 2345 + 2346 + test('top-level override with dependency chain', async () => { 2347 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => { 2348 + return it 2349 + .extend('basePort', 3000) 2350 + .extend('config', ({ basePort }) => ({ port: basePort, host: 'localhost' })) 2351 + .extend('server', async ({ config }, { onCleanup }) => { 2352 + log('server start', config.port) 2353 + onCleanup(() => log('server stop')) 2354 + return { url: `http://${config.host}:${config.port}`, running: true } 2355 + }) 2356 + }, { 2357 + 'basic.test.ts': ({ extendedTest, expect, describe }) => { 2358 + // Override at top level 2359 + extendedTest.override('basePort', 8080) 2360 + 2361 + extendedTest('top level uses overridden port', ({ config, server }) => { 2362 + expect(config.port).toBe(8080) 2363 + expect(server.url).toBe('http://localhost:8080') 2364 + }) 2365 + 2366 + describe('with custom host', () => { 2367 + extendedTest.override('config', ({ basePort }) => ({ port: basePort, host: '0.0.0.0' })) 2368 + 2369 + extendedTest('nested uses custom host with top-level port', ({ config, server }) => { 2370 + expect(config.port).toBe(8080) 2371 + expect(config.host).toBe('0.0.0.0') 2372 + expect(server.url).toBe('http://0.0.0.0:8080') 2373 + }) 2374 + }) 2375 + }, 2376 + }) 2377 + 2378 + expect(stderr).toBe('') 2379 + expect(fixtures).toMatchInlineSnapshot(` 2380 + ">> fixture | server start 8080 | top level uses overridden port 2381 + >> fixture | server stop | top level uses overridden port 2382 + >> fixture | server start 8080 | with custom host > nested uses custom host with top-level port 2383 + >> fixture | server stop | with custom host > nested uses custom host with top-level port" 2384 + `) 2385 + expect(tests).toMatchInlineSnapshot(` 2386 + " ✓ basic.test.ts > top level uses overridden port <time> 2387 + ✓ basic.test.ts > with custom host > nested uses custom host with top-level port <time>" 2388 + `) 2389 + }) 2390 + }) 2391 + 693 2392 async function runFixtureTests<T>( 694 - extendedTest: ({ log }: { log: typeof console.log }) => TestAPI<T>, 695 - fs: Record<string, ((context: { extendedTest: TestAPI<T>; expect: ExpectStatic; describe: SuiteAPI }) => unknown) | ViteUserConfig>, 2393 + extendedTest: ({ log, expectTypeOf }: { log: typeof console.log; expectTypeOf: typeof ExpectTypeOfFn }) => TestAPI<T>, 2394 + fs: Record<string, ((context: { extendedTest: TestAPI<T>; expect: ExpectStatic; expectTypeOf: typeof ExpectTypeOfFn; describe: SuiteAPI }) => unknown) | ViteUserConfig>, 696 2395 config?: TestUserConfig, 697 2396 ) { 698 2397 if (typeof fs['vitest.config.js'] === 'object') { ··· 702 2401 'test.js': ` 703 2402 export const describe = globalThis.describe 704 2403 export const expect = globalThis.expect 705 - export const extendedTest = (${extendedTest.toString()})({ log: (...args) => console.log('>> fixture |', ...args, '| ' + expect.getState().currentTestName) }) 706 - `, 2404 + export const expectTypeOf = globalThis.expectTypeOf 2405 + export const extendedTest = (${extendedTest.toString()})({ log: (...args) => console.log('>> fixture |', ...args, '| ' + expect.getState().currentTestName), expectTypeOf }) 2406 + `.trim().replace(/^[ \t]+/gm, m => m.slice(4)), 707 2407 'vitest.config.js': { test: { globals: true } }, 708 2408 ...Object.entries(fs).reduce((acc, [key, value]) => { 709 2409 if (typeof value === 'object' && !Array.isArray(value)) { 710 2410 acc[key] = value 711 2411 } 712 2412 if (typeof value === 'function') { 713 - acc[key] = [value, { imports: { './test.js': ['extendedTest', 'expect', 'describe'] } }] 2413 + acc[key] = [value, { imports: { './test.js': ['extendedTest', 'expect', 'expectTypeOf', 'describe'] } }] 714 2414 } 715 2415 return acc 716 2416 }, {} as TestFsStructure),
+1 -1
test/core/test/diff.test.ts
··· 349 349 } 350 350 catch (e) { 351 351 const error = processError(e, options) 352 - return error.diff 352 + return error.diff! 353 353 } 354 354 return expect.unreachable() 355 355 }
+3 -3
test/core/test/error.test.ts
··· 64 64 expect(serialisedError.stack).toBeTypeOf('string') 65 65 expect(serialisedError.message).toBeTypeOf('string') 66 66 67 - expect(serialisedError.cause.name).toBeTypeOf('string') 68 - expect(serialisedError.cause.stack).toBeTypeOf('string') 69 - expect(serialisedError.cause.message).toBeTypeOf('string') 67 + expect(serialisedError.cause?.name).toBeTypeOf('string') 68 + expect(serialisedError.cause?.stack).toBeTypeOf('string') 69 + expect(serialisedError.cause?.message).toBeTypeOf('string') 70 70 }) 71 71 72 72 test('simple error has message, stack and name', () => {
+7 -7
test/core/test/expect.test.ts
··· 532 532 } 533 533 catch (err) { 534 534 const error = processError(err) 535 - const diff = stripVTControlCharacters(error.diff) 535 + const diff = stripVTControlCharacters(error.diff!) 536 536 expect(diff).toMatchInlineSnapshot(` 537 537 "- Expected: 538 538 SchemaMatching { ··· 572 572 } 573 573 catch (err) { 574 574 const error = processError(err) 575 - const diff = stripVTControlCharacters(error.diff) 575 + const diff = stripVTControlCharacters(error.diff!) 576 576 expect(diff).toMatchInlineSnapshot(` 577 577 "- Expected 578 578 + Received ··· 615 615 } 616 616 catch (err) { 617 617 const error = processError(err) 618 - const diff = stripVTControlCharacters(error.diff) 618 + const diff = stripVTControlCharacters(error.diff!) 619 619 expect(diff).toMatchInlineSnapshot(` 620 620 "- Expected 621 621 + Received ··· 652 652 } 653 653 catch (err) { 654 654 const error = processError(err) 655 - const diff = stripVTControlCharacters(error.diff) 655 + const diff = stripVTControlCharacters(error.diff!) 656 656 expect(diff).toContain('SchemaMatching') 657 657 expect(diff).toContain('ArrayContaining') 658 658 } ··· 670 670 } 671 671 catch (err) { 672 672 const error = processError(err) 673 - const diff = stripVTControlCharacters(error.diff) 673 + const diff = stripVTControlCharacters(error.diff!) 674 674 expect(diff).toMatchInlineSnapshot(` 675 675 "- Expected: 676 676 SchemaMatching ··· 720 720 } 721 721 catch (err) { 722 722 const error = processError(err) 723 - const diff = stripVTControlCharacters(error.diff) 723 + const diff = stripVTControlCharacters(error.diff!) 724 724 expect(diff).toMatchInlineSnapshot(` 725 725 "- Expected 726 726 + Received ··· 754 754 } 755 755 catch (err) { 756 756 const error = processError(err) 757 - const diff = stripVTControlCharacters(error.diff) 757 + const diff = stripVTControlCharacters(error.diff!) 758 758 expect(diff).toMatchInlineSnapshot(` 759 759 "- Expected 760 760 + Received
+21 -5
test/core/test/fixture-initialization.test.ts
··· 74 74 expectTypeOf(b).toEqualTypeOf<string>() 75 75 76 76 expect(fnA).toBeCalledTimes(1) 77 - expect(fnB).toBeCalledTimes(1) 77 + expect(fnB).toBeCalledTimes(0) 78 78 expect(fnB2).toBeCalledTimes(1) 79 79 80 80 expect(fnC).not.toBeCalled() ··· 95 95 expect(b).toBe('2') 96 96 97 97 expect(fnA).toBeCalledTimes(1) 98 - expect(fnB).toBeCalledTimes(1) 98 + expect(fnB).toBeCalledTimes(0) 99 99 expect(fnB2).toBeCalledTimes(1) 100 100 101 101 expect(fnC).not.toBeCalled() ··· 106 106 expect(c).toBe(3) 107 107 108 108 expect(fnA).toBeCalledTimes(1) 109 - expect(fnB).toBeCalledTimes(1) 109 + expect(fnB).toBeCalledTimes(0) 110 110 expect(fnB2).toBeCalledTimes(1) 111 111 expect(fnC).toBeCalledTimes(1) 112 112 ··· 117 117 expect(d).toBe(6) 118 118 119 119 expect(fnA).toBeCalledTimes(1) 120 - expect(fnB).toBeCalledTimes(1) 120 + expect(fnB).toBeCalledTimes(0) 121 121 expect(fnB2).toBeCalledTimes(1) 122 122 expect(fnC).toBeCalledTimes(1) 123 123 expect(fnD).toBeCalledTimes(1) ··· 130 130 expect(d).toBe(6) 131 131 132 132 expect(fnA).toBeCalledTimes(1) 133 - expect(fnB).toBeCalledTimes(1) 133 + expect(fnB).toBeCalledTimes(0) 134 134 expect(fnB2).toBeCalledTimes(1) 135 135 expect(fnC).toBeCalledTimes(1) 136 136 expect(fnD).toBeCalledTimes(1) ··· 199 199 expect(task).toBeTruthy() 200 200 }) 201 201 }) 202 + }) 203 + 204 + const myTest3 = test.extend<{ value: string }>({ 205 + value: [async ({}, use) => { await use('first-value') }, { scope: 'file' }], 206 + }) 207 + 208 + const myTest4 = test.extend<{ value: string }>({ 209 + value: [async ({}, use) => { await use('second-value') }, { scope: 'file' }], 210 + }) 211 + 212 + myTest3('test1', ({ value }) => { 213 + expect(value).toBe('first-value') 214 + }) 215 + 216 + myTest4('test2', ({ value }) => { 217 + expect(value).toBe('second-value') 202 218 })
+4 -4
test/core/test/jest-expect.test.ts
··· 1284 1284 } 1285 1285 catch (err) { 1286 1286 const error = processError(err) 1287 - const diff = stripVTControlCharacters(error.diff) 1287 + const diff = stripVTControlCharacters(error.diff!) 1288 1288 expect(diff).toContain('- "a": 2') 1289 1289 expect(diff).toContain('+ "a": 1') 1290 1290 } ··· 1297 1297 } 1298 1298 catch (err) { 1299 1299 const error = processError(new Error('wrapper', { cause: err })) 1300 - const diff = stripVTControlCharacters(error.cause.diff) 1300 + const diff = stripVTControlCharacters(error.cause!.diff!) 1301 1301 expect(diff).toContain('- "a": 2') 1302 1302 expect(diff).toContain('+ "a": 1') 1303 1303 } ··· 1313 1313 } 1314 1314 catch (err) { 1315 1315 const error = processError(err) 1316 - expect(stripVTControlCharacters(error.diff)).toMatchInlineSnapshot(` 1316 + expect(stripVTControlCharacters(error.diff!)).toMatchInlineSnapshot(` 1317 1317 "- Expected 1318 1318 + Received 1319 1319 ··· 1337 1337 } 1338 1338 catch (error) { 1339 1339 const processed = processError(error) 1340 - return [stripVTControlCharacters(processed.message), stripVTControlCharacters(trim(processed.diff))] 1340 + return [stripVTControlCharacters(processed.message), stripVTControlCharacters(trim(processed.diff!))] 1341 1341 } 1342 1342 return expect.unreachable() 1343 1343 }
+108 -19
test/core/test/test-extend.test.ts
··· 399 399 }) 400 400 401 401 describe('override dependency', () => { 402 - testAPI.scoped({ dependency: 'new' }) 402 + testAPI.override({ dependency: 'new' }) 403 403 404 404 testAPI('uses new values', ({ pkg }) => { 405 405 expect(pkg).toEqual({ dependency: 'new' }) ··· 412 412 }) 413 413 414 414 describe('override nested overridden scope', () => { 415 - testAPI.scoped({ dependency: 'override' }) 415 + testAPI.override({ dependency: 'override' }) 416 416 417 417 testAPI('keeps using new values', ({ pkg }) => { 418 418 expect(pkg).toEqual({ dependency: 'override' }) ··· 429 429 }) 430 430 431 431 describe('override the pkg too', () => { 432 - testAPI.scoped({ pkg: { dependency: 'override' } }) 432 + testAPI.override({ pkg: { dependency: 'override' } }) 433 433 434 434 testAPI('uses new values', ({ pkg }) => { 435 435 expect(pkg).toEqual({ dependency: 'override' }) ··· 437 437 }) 438 438 439 439 describe('override as dynamic', () => { 440 - testAPI.scoped({ dependency: ({}, use) => use('override') }) 440 + testAPI.override({ dependency: ({}, use) => use('override') }) 441 441 442 442 testAPI('uses new values', ({ pkg }) => { 443 443 expect(pkg).toEqual({ dependency: 'override' }) ··· 445 445 }) 446 446 447 447 describe.skip('type only', () => { 448 - testAPI.scoped({ 448 + testAPI.override({ 449 449 // @ts-expect-error nonExisting is not defined on the testAPI 450 450 nonExisting: false, 451 451 }) ··· 458 458 }) 459 459 460 460 describe('top level', () => { 461 - extendedTest.scoped({ foo: true }) 461 + extendedTest.override({ foo: true }) 462 462 463 463 describe('second level', () => { 464 464 extendedTest('foo is true', ({ foo }) => { ··· 474 474 }) 475 475 476 476 describe('foo is scoped to true', () => { 477 - extendedTest.scoped({ foo: true }) 477 + extendedTest.override({ foo: true }) 478 478 479 479 extendedTest('foo is true', ({ foo }) => { 480 480 expect(foo).toBe(true) ··· 499 499 numbers: async ({ a }, use) => use([a]), 500 500 }) 501 501 502 - describe('suite fails', () => { 503 - extendedTest.scoped({ 502 + describe('suite with overwritten fixture', () => { 503 + extendedTest.override({ 504 504 numbers: async ({ a, b }, use) => use([a, b]), 505 505 }) 506 506 507 - extendedTest.fails('test fails', async ({ 507 + extendedTest('scoped fixture can access dependencies from original test', async ({ 508 508 numbers, 509 509 }) => { 510 - expect(numbers).toStrictEqual([1, 2]) // fails because numbers is [undefined, undefined] 510 + expect(numbers).toStrictEqual([1, 2]) 511 511 }) 512 512 }) 513 513 }) ··· 522 522 }) 523 523 }, 100) 524 524 525 - describe('type-safe fixture hooks', () => { 526 - const counterTest = test.extend<{ 527 - counter: { value: number } 528 - fileCounter: { value: number } 529 - }>({ 530 - counter: async ({}, use) => { await use({ value: 0 }) }, 531 - fileCounter: [async ({}, use) => { await use({ value: 0 }) }, { scope: 'file' }], 532 - }) 525 + const counterTest = test.extend<{ 526 + counter: { value: number } 527 + fileCounter: { value: number } 528 + }>({ 529 + counter: async ({}, use) => { await use({ value: 0 }) }, 530 + fileCounter: [async ({}, use) => { await use({ value: 0 }) }, { scope: 'file' }], 531 + }) 533 532 533 + counterTest.describe('type-safe fixture hooks', () => { 534 534 counterTest.beforeEach(({ counter }) => { 535 535 // shouldn't have typescript error because of 'counter' here 536 536 counter.value += 1 ··· 549 549 550 550 counterTest('afterEach fixture hook can adapt type-safe context', ({ fileCounter }) => { 551 551 expect(fileCounter.value).toBe(2) 552 + }) 553 + }) 554 + 555 + // Use the scoped fixtures approach with { $test, $file, $worker } structure 556 + const helperTest = test.extend<{ 557 + $worker: { workerFixture: boolean } 558 + $file: { fileFixture: number } 559 + $test: { testFixture: string } 560 + }>({ 561 + workerFixture: [async ({}, use) => { 562 + await use(true) 563 + }, { scope: 'worker' }], 564 + fileFixture: [async ({ workerFixture }, use) => { 565 + expectTypeOf(workerFixture).toEqualTypeOf<boolean>() 566 + await use(workerFixture ? 42 : 0) 567 + }, { scope: 'file' }], 568 + testFixture: async ({ fileFixture, workerFixture }, use) => { 569 + expectTypeOf(fileFixture).toEqualTypeOf<number>() 570 + expectTypeOf(workerFixture).toEqualTypeOf<boolean>() 571 + await use(`test-${fileFixture}-${workerFixture}`) 572 + }, 573 + }) 574 + 575 + helperTest.describe('scoped fixtures with tuple syntax', () => { 576 + helperTest('fixtures should have correct types', ({ testFixture, fileFixture, workerFixture }) => { 577 + expectTypeOf(workerFixture).toEqualTypeOf<boolean>() 578 + expectTypeOf(fileFixture).toEqualTypeOf<number>() 579 + expectTypeOf(testFixture).toEqualTypeOf<string>() 580 + 581 + expect(workerFixture).toBe(true) 582 + expect(fileFixture).toBe(42) 583 + expect(testFixture).toBe('test-42-true') 584 + }) 585 + }) 586 + 587 + describe('builder pattern with non-function values', () => { 588 + const nonFnTest = test 589 + .extend('stringValue', 'hello') 590 + .extend('numberValue', 42) 591 + .extend('arrayValue', [1, 2, 3]) 592 + .extend('objectValue', { key: 'value', nested: { a: 1 } }) 593 + 594 + nonFnTest('non-function values are provided correctly', ({ stringValue, numberValue, arrayValue, objectValue }) => { 595 + expectTypeOf(stringValue).toEqualTypeOf<string>() 596 + expectTypeOf(numberValue).toEqualTypeOf<number>() 597 + expectTypeOf(arrayValue).toEqualTypeOf<number[]>() 598 + expectTypeOf(objectValue).toEqualTypeOf<{ key: string; nested: { a: number } }>() 599 + 600 + expect(stringValue).toBe('hello') 601 + expect(numberValue).toBe(42) 602 + expect(arrayValue).toEqual([1, 2, 3]) 603 + expect(objectValue).toEqual({ key: 'value', nested: { a: 1 } }) 604 + }) 605 + 606 + const mixedTest = test 607 + .extend('config', { port: 3000, host: 'localhost' }) 608 + .extend('url', async ({ config }) => { 609 + expectTypeOf(config).toEqualTypeOf<{ port: number; host: string }>() 610 + return `http://${config.host}:${config.port}` 611 + }) 612 + 613 + mixedTest('non-function values can be used by function fixtures', ({ config, url }) => { 614 + expectTypeOf(config).toEqualTypeOf<{ port: number; host: string }>() 615 + expectTypeOf(url).toEqualTypeOf<string>() 616 + 617 + expect(config).toEqual({ port: 3000, host: 'localhost' }) 618 + expect(url).toBe('http://localhost:3000') 619 + }) 620 + 621 + // Test that synchronous (non-async) functions work in the builder pattern 622 + const syncTest = test 623 + .extend('prefix', 'hello') 624 + .extend('syncValue', ({ prefix }) => { 625 + // This is a synchronous function - no async/await needed 626 + return `${prefix} world` 627 + }) 628 + .extend('chainedSync', ({ syncValue }) => { 629 + // Another sync function that depends on the previous one 630 + return syncValue.toUpperCase() 631 + }) 632 + 633 + syncTest('synchronous functions work in builder pattern', ({ prefix, syncValue, chainedSync }) => { 634 + expectTypeOf(prefix).toEqualTypeOf<string>() 635 + expectTypeOf(syncValue).toEqualTypeOf<string>() 636 + expectTypeOf(chainedSync).toEqualTypeOf<string>() 637 + 638 + expect(prefix).toBe('hello') 639 + expect(syncValue).toBe('hello world') 640 + expect(chainedSync).toBe('HELLO WORLD') 552 641 }) 553 642 })
-4
packages/runner/src/types/runner.ts
··· 224 224 */ 225 225 viteEnvironment?: string 226 226 227 - /** 228 - * Return the worker context for fixtures specified with `scope: 'worker'` 229 - */ 230 - getWorkerContext?: () => Record<string, unknown> 231 227 onCleanupWorkerContext?: (cleanup: () => unknown) => void 232 228 233 229 // eslint-disable-next-line ts/method-signature-style
+410 -29
packages/runner/src/types/tasks.ts
··· 1 1 import type { Awaitable, TestError } from '@vitest/utils' 2 - import type { FixtureItem } from '../fixture' 2 + import type { TestFixtures } from '../fixture' 3 3 import type { afterAll, afterEach, aroundAll, aroundEach, beforeAll, beforeEach } from '../hooks' 4 - import type { ChainableFunction } from '../utils/chain' 4 + import type { ChainableFunction, kChainableContext } from '../utils/chain' 5 5 6 6 export type RunMode = 'run' | 'skip' | 'only' | 'todo' | 'queued' 7 7 export type TaskState = RunMode | 'pass' | 'fail' ··· 104 104 * `includeTaskLocation` option is set. It is generated by calling `new Error` 105 105 * and parsing the stack trace, so the location might differ depending on the runtime. 106 106 */ 107 - location?: { 108 - line: number 109 - column: number 110 - } 107 + location?: Location 111 108 /** 112 109 * If the test was collected by parsing the file AST, and the name 113 110 * is not a static string, this property will be set to `true`. ··· 456 453 ): void 457 454 } 458 455 456 + export interface ChainableContext<API> { 457 + [kChainableContext]: { 458 + /** @internal */ 459 + mergeContext: (ctx: Partial<InternalTestContext>) => void 460 + /** @internal */ 461 + setContext: (key: keyof InternalTestContext, value: any) => void 462 + /** @internal */ 463 + withContext: () => API 464 + /** @internal */ 465 + getFixtures: () => TestFixtures 466 + } 467 + } 468 + 459 469 type ChainableTestAPI<ExtraContext = object> = ChainableFunction< 460 470 'concurrent' | 'sequential' | 'only' | 'skip' | 'todo' | 'fails', 461 471 TestCollectorCallable<ExtraContext>, ··· 600 610 } 601 611 602 612 export type TestAPI<ExtraContext = object> = ChainableTestAPI<ExtraContext> 603 - & ExtendedAPI<ExtraContext> & Hooks<ExtraContext> & { 604 - extend: <T extends Record<string, any> = object>( 605 - fixtures: Fixtures<T, ExtraContext>, 606 - ) => TestAPI<{ 607 - [K in keyof T | keyof ExtraContext]: K extends keyof T 608 - ? T[K] 609 - : K extends keyof ExtraContext 610 - ? ExtraContext[K] 611 - : never; 612 - }> 613 + & ExtendedAPI<ExtraContext> & Hooks<ExtraContext> & ChainableContext<TestAPI<ExtraContext>> & { 614 + /** 615 + * Extend the test API with custom fixtures. 616 + * 617 + * @example 618 + * ```ts 619 + * // Simple test fixtures (backward compatible) 620 + * const myTest = test.extend<{ foo: string }>({ 621 + * foo: 'value', 622 + * }) 623 + * 624 + * // With scoped fixtures - use $test/$file/$worker structure 625 + * const myTest = test.extend<{ 626 + * $test: { testData: string } 627 + * $file: { fileDb: Database } 628 + * $worker: { workerConfig: Config } 629 + * }>({ 630 + * testData: async ({ fileDb }, use) => { 631 + * await use(await fileDb.getData()) 632 + * }, 633 + * fileDb: [async ({ workerConfig }, use) => { 634 + * // File fixture can only access workerConfig, NOT testData 635 + * const db = new Database(workerConfig) 636 + * await use(db) 637 + * await db.close() 638 + * }, { scope: 'file' }], 639 + * workerConfig: [async ({}, use) => { 640 + * // Worker fixture can only access other worker fixtures 641 + * await use(loadConfig()) 642 + * }, { scope: 'worker' }], 643 + * }) 644 + * 645 + * // Builder pattern with automatic type inference 646 + * const myTest = test 647 + * .extend('config', { scope: 'worker' }, async ({}) => { 648 + * return { port: 3000 } // Type inferred as { port: number } 649 + * }) 650 + * .extend('db', { scope: 'file' }, async ({ config }, { onCleanup }) => { 651 + * // TypeScript knows config is { port: number } 652 + * const db = new Database(config.port) 653 + * onCleanup(() => db.close()) // Register cleanup 654 + * return db // Type inferred as Database 655 + * }) 656 + * .extend('data', async ({ db }) => { 657 + * // TypeScript knows db is Database 658 + * return await db.getData() // Type inferred from return 659 + * }) 660 + * ``` 661 + */ 662 + extend: { 663 + // Builder pattern overloads with automatic type inference from return value 664 + // MUST come first for correct TypeScript overload resolution 665 + 666 + // Function overloads (with cleanup support via onCleanup) 667 + // When extending with same key, T must match existing type (last value wins at runtime) 668 + // Overload 1: Worker scope function - can only access worker fixtures 669 + <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>( 670 + name: K, 671 + options: WorkerScopeFixtureOptions, 672 + fn: BuilderFixtureFn<T, WorkerScopeContext<ExtraContext>>, 673 + ): TestAPI<AddBuilderWorker<ExtraContext, K, T>> 674 + // Overload 2: File scope function - can access worker + file fixtures 675 + <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>( 676 + name: K, 677 + options: FileScopeFixtureOptions, 678 + fn: BuilderFixtureFn<T, FileScopeContext<ExtraContext>>, 679 + ): TestAPI<AddBuilderFile<ExtraContext, K, T>> 680 + // Overload 3: Test scope function with options - can access all fixtures 681 + <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>( 682 + name: K, 683 + options: TestScopeFixtureOptions, 684 + fn: BuilderFixtureFn<T, TestScopeContext<ExtraContext>>, 685 + ): TestAPI<AddBuilderTest<ExtraContext, K, T>> 686 + // Overload 4: Test scope function default (no options) - can access all fixtures 687 + <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>( 688 + name: K, 689 + fn: BuilderFixtureFn<T, TestScopeContext<ExtraContext>>, 690 + ): TestAPI<AddBuilderTest<ExtraContext, K, T>> 691 + 692 + // Non-function value overloads (simple values without cleanup) 693 + // Overload 5: Static value with worker scope options 694 + <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>( 695 + name: K, 696 + options: WorkerScopeFixtureOptions, 697 + value: T extends (...args: any[]) => any ? never : T, 698 + ): TestAPI<AddBuilderWorker<ExtraContext, K, T>> 699 + // Overload 6: Static value with file scope options 700 + <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>( 701 + name: K, 702 + options: FileScopeFixtureOptions, 703 + value: T extends (...args: any[]) => any ? never : T, 704 + ): TestAPI<AddBuilderFile<ExtraContext, K, T>> 705 + // Overload 7: Static value with test scope options 706 + <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>( 707 + name: K, 708 + options: TestScopeFixtureOptions, 709 + value: T extends (...args: any[]) => any ? never : T, 710 + ): TestAPI<AddBuilderTest<ExtraContext, K, T>> 711 + // Overload 8: Static value default (no options) - must exclude functions 712 + <K extends string, T extends (K extends keyof ExtraContext ? ExtraContext[K] : unknown)>( 713 + name: K, 714 + value: T extends (...args: any[]) => any ? never : T, 715 + ): TestAPI<AddBuilderTest<ExtraContext, K, T>> 716 + 717 + // Object syntax overloads 718 + // Overload 9: Scoped fixtures with { $test?, $file?, $worker? } structure 719 + <T extends ScopedFixturesDef>( 720 + fixtures: ScopedFixturesObject<T, ExtraContext>, 721 + ): TestAPI<ExtractScopedFixtures<T> & ExtraContext> 722 + // Overload 10: Legacy flat fixtures (backward compatible) 723 + <T extends Record<string, any> = object>( 724 + fixtures: Fixtures<T, ExtraContext>, 725 + ): TestAPI<{ 726 + [K in keyof T | keyof ExtraContext]: K extends keyof T 727 + ? T[K] 728 + : K extends keyof ExtraContext 729 + ? ExtraContext[K] 730 + : never 731 + }> 732 + } 733 + /** 734 + * Overwrite fixture values for the current suite scope. 735 + * Supports both object syntax and builder pattern. 736 + * 737 + * @example 738 + * ```ts 739 + * describe('with custom config', () => { 740 + * // Object syntax 741 + * test.override({ config: { port: 4000 } }) 742 + * 743 + * // Builder pattern - value 744 + * test.override('config', { port: 4000 }) 745 + * 746 + * // Builder pattern - function 747 + * test.override('config', () => ({ port: 4000 })) 748 + * 749 + * // Builder pattern - function with cleanup 750 + * test.override('db', async ({ config }, { onCleanup }) => { 751 + * const db = await createDb(config) 752 + * onCleanup(() => db.close()) 753 + * return db 754 + * }) 755 + * }) 756 + * ``` 757 + */ 758 + override: { 759 + // Builder pattern overloads 760 + // Overload 1: Function with options 761 + <K extends keyof ExtraContext>( 762 + name: K, 763 + options: FixtureOptions, 764 + fn: BuilderFixtureFn<ExtraContext[K], ExtraContext & TestContext>, 765 + ): TestAPI<ExtraContext> 766 + // Overload 2: Function without options 767 + <K extends keyof ExtraContext>( 768 + name: K, 769 + fn: BuilderFixtureFn<ExtraContext[K], ExtraContext & TestContext>, 770 + ): TestAPI<ExtraContext> 771 + // Overload 3: Static value with options 772 + <K extends keyof ExtraContext>( 773 + name: K, 774 + options: FixtureOptions, 775 + value: ExtraContext[K] extends (...args: any[]) => any ? never : ExtraContext[K], 776 + ): TestAPI<ExtraContext> 777 + // Overload 4: Static value without options 778 + <K extends keyof ExtraContext>( 779 + name: K, 780 + value: ExtraContext[K] extends (...args: any[]) => any ? never : ExtraContext[K], 781 + ): TestAPI<ExtraContext> 782 + // Overload 5: Object syntax 783 + (fixtures: Partial<Fixtures<ExtraContext>>): TestAPI<ExtraContext> 784 + } 785 + /** 786 + * @deprecated Use `test.override()` instead 787 + */ 613 788 scoped: ( 614 789 fixtures: Partial<Fixtures<ExtraContext>>, 615 - ) => void 790 + ) => TestAPI<ExtraContext> 616 791 describe: SuiteAPI<ExtraContext> 792 + suite: SuiteAPI<ExtraContext> 617 793 } 794 + 795 + export interface InternalTestContext extends Record< 796 + 'concurrent' | 'sequential' | 'skip' | 'only' | 'todo' | 'fails' | 'each', 797 + boolean | undefined 798 + > { 799 + fixtures: TestFixtures 800 + } 618 801 619 802 export interface FixtureOptions { 620 803 /** ··· 638 821 scope?: 'test' | 'worker' | 'file' 639 822 } 640 823 824 + /** 825 + * Options for test-scoped fixtures. 826 + * Test fixtures are set up before each test and have access to all fixtures. 827 + */ 828 + export interface TestScopeFixtureOptions extends Omit<FixtureOptions, 'scope'> { 829 + /** 830 + * @default 'test' 831 + */ 832 + scope?: 'test' 833 + } 834 + 835 + /** 836 + * Options for file-scoped fixtures. 837 + * File fixtures are set up once per file and can only access other file fixtures and worker fixtures. 838 + */ 839 + export interface FileScopeFixtureOptions extends Omit<FixtureOptions, 'scope'> { 840 + /** 841 + * Must be 'file' for file-scoped fixtures. 842 + */ 843 + scope: 'file' 844 + } 845 + 846 + /** 847 + * Options for worker-scoped fixtures. 848 + * Worker fixtures are set up once per worker and can only access other worker fixtures. 849 + */ 850 + export interface WorkerScopeFixtureOptions extends Omit<FixtureOptions, 'scope'> { 851 + /** 852 + * Must be 'worker' for worker-scoped fixtures. 853 + */ 854 + scope: 'worker' 855 + } 856 + 641 857 export type Use<T> = (value: T) => Promise<void> 858 + 859 + /** 860 + * Cleanup registration function for builder pattern fixtures. 861 + * Call this to register a cleanup function that runs after the test/file/worker completes. 862 + * 863 + * **Note:** This function can only be called once per fixture. If you need multiple 864 + * cleanup operations, either combine them into a single cleanup function or split 865 + * your fixture into multiple smaller fixtures. 866 + */ 867 + export type OnCleanup = (cleanup: () => Awaitable<void>) => void 868 + 869 + /** 870 + * Builder pattern fixture function with automatic type inference. 871 + * Returns the fixture value directly (type is inferred from return). 872 + * Use onCleanup to register teardown logic. 873 + * 874 + * Parameters can be omitted if not needed: 875 + * - `async () => value` - no dependencies, no cleanup 876 + * - `async ({ dep }) => value` - with dependencies, no cleanup 877 + * - `async ({ dep }, { onCleanup }) => value` - with dependencies and cleanup 878 + */ 879 + export type BuilderFixtureFn<T, Context> = ( 880 + context: Context, 881 + fixture: { onCleanup: OnCleanup }, 882 + ) => T | Promise<T> 883 + 884 + /** 885 + * Extracts worker-scoped fixtures from a context that includes scope info. 886 + */ 887 + export type ExtractBuilderWorker<C> = C extends { $__worker?: infer W } 888 + ? W extends Record<string, any> ? W : object 889 + : object 890 + 891 + /** 892 + * Extracts file-scoped fixtures from a context that includes scope info. 893 + */ 894 + export type ExtractBuilderFile<C> = C extends { $__file?: infer F } 895 + ? F extends Record<string, any> ? F : object 896 + : object 897 + 898 + /** 899 + * Extracts test-scoped fixtures from a context that includes scope info. 900 + */ 901 + export type ExtractBuilderTest<C> = C extends { $__test?: infer T } 902 + ? T extends Record<string, any> ? T : object 903 + : object 904 + 905 + /** 906 + * Adds a worker fixture to the context with proper scope tracking. 907 + */ 908 + export type AddBuilderWorker<C, K extends string, V> = Omit<C, '$__worker'> & Record<K, V> & { 909 + readonly $__worker?: ExtractBuilderWorker<C> & Record<K, V> 910 + readonly $__file?: ExtractBuilderFile<C> 911 + readonly $__test?: ExtractBuilderTest<C> 912 + } 913 + 914 + /** 915 + * Adds a file fixture to the context with proper scope tracking. 916 + */ 917 + export type AddBuilderFile<C, K extends string, V> = Omit<C, '$__file'> & Record<K, V> & { 918 + readonly $__worker?: ExtractBuilderWorker<C> 919 + readonly $__file?: ExtractBuilderFile<C> & Record<K, V> 920 + readonly $__test?: ExtractBuilderTest<C> 921 + } 922 + 923 + /** 924 + * Adds a test fixture to the context with proper scope tracking. 925 + */ 926 + export type AddBuilderTest<C, K extends string, V> = Omit<C, '$__test'> & Record<K, V> & { 927 + readonly $__worker?: ExtractBuilderWorker<C> 928 + readonly $__file?: ExtractBuilderFile<C> 929 + readonly $__test?: ExtractBuilderTest<C> & Record<K, V> 930 + } 931 + 932 + /** 933 + * Context available to worker-scoped fixtures. 934 + * Worker fixtures can only access other worker fixtures. 935 + * They do NOT have access to test context (task, expect, onTestFailed, etc.) 936 + * since they run once per worker, outside of any specific test. 937 + */ 938 + export type WorkerScopeContext<C> = ExtractBuilderWorker<C> 939 + 940 + /** 941 + * Context available to file-scoped fixtures. 942 + * File fixtures can access worker and other file fixtures. 943 + * They do NOT have access to test context (task, expect, onTestFailed, etc.) 944 + * since they run once per file, outside of any specific test. 945 + */ 946 + export type FileScopeContext<C> = ExtractBuilderWorker<C> & ExtractBuilderFile<C> 947 + 948 + /** 949 + * Context available to test-scoped fixtures (all fixtures + test context). 950 + */ 951 + export type TestScopeContext<C> = C & TestContext 642 952 export type FixtureFn<T, K extends keyof T, ExtraContext> = ( 643 953 context: Omit<T, K> & ExtraContext, 644 954 use: Use<T[K]>, ··· 653 963 | (T[K] extends any 654 964 ? FixtureFn<T, K, Omit<ExtraContext, Exclude<keyof T, K>>> 655 965 : never) 966 + 967 + /** 968 + * Fixture function with explicit context type for scoped fixtures. 969 + */ 970 + export type ScopedFixtureFn<Value, Context> = ( 971 + context: Context, 972 + use: Use<Value>, 973 + ) => Promise<void> 974 + 975 + /** 976 + * Fixtures definition for backward compatibility. 977 + * All fixtures are in T and any scope is allowed. 978 + */ 656 979 export type Fixtures<T, ExtraContext = object> = { 657 980 [K in keyof T]: 658 981 | Fixture<T, K, ExtraContext & TestContext> 659 - | [Fixture<T, K, ExtraContext & TestContext>, FixtureOptions?]; 982 + | [Fixture<T, K, ExtraContext & TestContext>, FixtureOptions?] 983 + } 984 + 985 + /** 986 + * Scoped fixtures definition using a single generic with optional scope keys. 987 + * This provides better ergonomics than multiple generics. 988 + * Uses $ prefix to avoid conflicts with fixture names. 989 + * 990 + * @example 991 + * ```ts 992 + * test.extend<{ 993 + * $worker?: { config: Config } 994 + * $file?: { db: Database } 995 + * $test?: { data: string } 996 + * }>({ ... }) 997 + * ``` 998 + */ 999 + export interface ScopedFixturesDef { 1000 + $test?: Record<string, any> 1001 + $file?: Record<string, any> 1002 + $worker?: Record<string, any> 1003 + } 1004 + 1005 + /** 1006 + * Extracts fixture types from a ScopedFixturesDef. 1007 + * Handles optional properties by using Exclude to remove undefined. 1008 + */ 1009 + export type ExtractScopedFixtures<T extends ScopedFixturesDef> 1010 + = ([Exclude<T['$test'], undefined>] extends [never] ? object : Exclude<T['$test'], undefined>) 1011 + & ([Exclude<T['$file'], undefined>] extends [never] ? object : Exclude<T['$file'], undefined>) 1012 + & ([Exclude<T['$worker'], undefined>] extends [never] ? object : Exclude<T['$worker'], undefined>) 1013 + 1014 + /** 1015 + * Creates the fixtures object type for ScopedFixturesDef with proper scope validation. 1016 + * - Test fixtures: can be defined as value, function, or tuple with optional scope 1017 + * - File fixtures: MUST have { scope: 'file' } 1018 + * - Worker fixtures: MUST have { scope: 'worker' } 1019 + */ 1020 + export type ScopedFixturesObject<T extends ScopedFixturesDef, ExtraContext = object> = { 1021 + // Test fixtures - scope is optional, have access to all fixtures + TestContext 1022 + [K in keyof NonNullable<T['$test']>]: 1023 + | NonNullable<T['$test']>[K] 1024 + | ScopedFixtureFn<NonNullable<T['$test']>[K], ExtractScopedFixtures<T> & ExtraContext & TestContext> 1025 + | [ScopedFixtureFn<NonNullable<T['$test']>[K], ExtractScopedFixtures<T> & ExtraContext & TestContext>, TestScopeFixtureOptions?] 1026 + } & { 1027 + // File fixtures - scope: 'file' is REQUIRED, NO TestContext access 1028 + [K in keyof NonNullable<T['$file']>]: 1029 + [ScopedFixtureFn<NonNullable<T['$file']>[K], (NonNullable<T['$file']> & NonNullable<T['$worker']>) & ExtraContext>, FileScopeFixtureOptions] 1030 + } & { 1031 + // Worker fixtures - scope: 'worker' is REQUIRED, NO TestContext access 1032 + [K in keyof NonNullable<T['$worker']>]: 1033 + [ScopedFixtureFn<NonNullable<T['$worker']>[K], NonNullable<T['$worker']> & ExtraContext>, WorkerScopeFixtureOptions] 660 1034 } 661 1035 662 1036 export type InferFixturesTypes<T> = T extends TestAPI<infer C> ? C : T ··· 683 1057 } 684 1058 > 685 1059 686 - export type SuiteAPI<ExtraContext = object> = ChainableSuiteAPI<ExtraContext> & { 1060 + export type SuiteAPI<ExtraContext = object> = ChainableSuiteAPI<ExtraContext> & ChainableContext<SuiteAPI<ExtraContext>> & { 687 1061 skipIf: (condition: any) => ChainableSuiteAPI<ExtraContext> 688 1062 runIf: (condition: any) => ChainableSuiteAPI<ExtraContext> 689 1063 } ··· 742 1116 /** 743 1117 * Task fixtures. 744 1118 */ 745 - fixtures?: FixtureItem[] 1119 + fixtures?: TestFixtures 746 1120 /** 747 1121 * Function that will be called when the task is executed. 748 1122 * If nothing is provided, the runner will try to get the function using `getFn(task)`. ··· 762 1136 | Test<ExtraContext> 763 1137 | SuiteCollector<ExtraContext> 764 1138 )[] 765 - scoped: (fixtures: Fixtures<any, ExtraContext>) => void 766 - fixtures: () => FixtureItem[] | undefined 767 - file?: File 1139 + file: File 768 1140 suite?: Suite 769 1141 task: (name: string, options?: TaskCustomOptions) => Test<ExtraContext> 770 1142 collect: (file: File) => Promise<Suite> ··· 865 1237 body?: string | Uint8Array 866 1238 } 867 1239 868 - /** 869 - * Source code location information for a test artifact. 870 - * 871 - * Indicates where in the source code the artifact originated from. 872 - */ 873 - export interface TestArtifactLocation { 1240 + export interface Location { 1241 + /** Line number in the source file (1-indexed) */ 1242 + line: number 1243 + /** Column number in the line (1-indexed) */ 1244 + column: number 1245 + } 1246 + 1247 + export interface FileLocation extends Location { 874 1248 /** Line number in the source file (1-indexed) */ 875 1249 line: number 876 1250 /** Column number in the line (1-indexed) */ ··· 878 1252 /** Path to the source file */ 879 1253 file: string 880 1254 } 1255 + 1256 + /** 1257 + * Source code location information for a test artifact. 1258 + * 1259 + * Indicates where in the source code the artifact originated from. 1260 + */ 1261 + export interface TestArtifactLocation extends FileLocation {} 881 1262 882 1263 /** 883 1264 * @experimental
+23 -8
packages/runner/src/utils/chain.ts
··· 1 + import type { ChainableContext } from '../types/tasks' 2 + 1 3 export type ChainableFunction< 2 4 T extends string, 3 5 F extends (...args: any) => any, ··· 8 10 fn: (this: Record<T, any>, ...args: Parameters<F>) => ReturnType<F> 9 11 } & C 10 12 13 + export const kChainableContext: unique symbol = Symbol('kChainableContext') 14 + 15 + export function getChainableContext(chainable: any): ChainableContext<any>[typeof kChainableContext] { 16 + return chainable[kChainableContext] 17 + } 18 + 11 19 export function createChainable<T extends string, Args extends any[], R = any>( 12 20 keys: T[], 13 21 fn: (this: Record<T, any>, ...args: Args) => R, 22 + context?: Record<string, any>, 14 23 ): ChainableFunction<T, (...args: Args) => R> { 15 24 function create(context: Record<T, any>) { 16 25 const chain = function (this: any, ...args: Args) { 17 26 return fn.apply(context, args) 18 27 } 19 28 Object.assign(chain, fn) 20 - chain.withContext = () => chain.bind(context) 21 - chain.setContext = (key: T, value: any) => { 22 - context[key] = value 23 - } 24 - chain.mergeContext = (ctx: Record<T, any>) => { 25 - Object.assign(context, ctx) 26 - } 29 + Object.defineProperty(chain, kChainableContext, { 30 + value: { 31 + withContext: () => chain.bind(context), 32 + getFixtures: () => (context as any).fixtures, 33 + setContext: (key: T, value: any) => { 34 + context[key] = value 35 + }, 36 + mergeContext: (ctx: Record<T, any>) => { 37 + Object.assign(context, ctx) 38 + }, 39 + }, 40 + enumerable: false, 41 + }) 27 42 for (const key of keys) { 28 43 Object.defineProperty(chain, key, { 29 44 get() { ··· 34 49 return chain 35 50 } 36 51 37 - const chain = create({} as any) as any 52 + const chain = create(context ?? {} as any) as any 38 53 chain.fn = fn 39 54 return chain 40 55 }
-2
packages/vitest/src/runtime/runVmTests.ts
··· 78 78 79 79 config.snapshotOptions.snapshotEnvironment = snapshotEnvironment 80 80 81 - testRunner.getWorkerContext = undefined 82 - 83 81 workerState.onCancel((reason) => { 84 82 closeInspector(config) 85 83 testRunner.cancel?.(reason)
+2 -2
test/core/test/environments/jsdom.spec.ts
··· 286 286 expect.unreachable() 287 287 } 288 288 catch (err: any) { 289 - expect(stripVTControlCharacters(processError(err).diff)).toMatchInlineSnapshot(` 289 + expect(stripVTControlCharacters(processError(err).diff!)).toMatchInlineSnapshot(` 290 290 "Expected: "flex flex-col flex-row" 291 291 Received: "flex flex-col"" 292 292 `) ··· 297 297 expect.unreachable() 298 298 } 299 299 catch (err: any) { 300 - expect(stripVTControlCharacters(processError(err).diff)).toMatchInlineSnapshot(` 300 + expect(stripVTControlCharacters(processError(err).diff!)).toMatchInlineSnapshot(` 301 301 "Expected: "flex-col" 302 302 Received: "flex flex-col"" 303 303 `)
-7
packages/vitest/src/runtime/runners/test.ts
··· 33 33 import { rpc } from '../rpc' 34 34 import { getWorkerState } from '../utils' 35 35 36 - // worker context is shared between all tests 37 - const workerContext = Object.create(null) 38 - 39 36 export class TestRunner implements VitestTestRunner { 40 37 private snapshotClient = getSnapshotClient() 41 38 private workerState = getWorkerState() ··· 89 86 onAfterRunFiles(): void { 90 87 this.snapshotClient.clear() 91 88 this.workerState.current = undefined 92 - } 93 - 94 - getWorkerContext(): Record<string, unknown> { 95 - return workerContext 96 89 } 97 90 98 91 async onAfterRunSuite(suite: Suite): Promise<void> {