From 99e52fe583196f9ed3ce1d9247c258045cc99550 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 10 Mar 2026 16:34:01 +0900 Subject: [PATCH 1/2] fix: resolve fixture overrides from test's suite in `beforeEach` hooks (#9826) Co-authored-by: Claude Opus 4.6 --- packages/runner/src/context.ts | 2 +- packages/runner/src/fixture.ts | 7 +- packages/runner/src/hooks.ts | 17 +++- test/core/test/test-extend.test.ts | 133 +++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 5 deletions(-) diff --git a/packages/runner/src/context.ts b/packages/runner/src/context.ts index f793b03c6901..00dea0a9b3a0 100644 --- a/packages/runner/src/context.ts +++ b/packages/runner/src/context.ts @@ -136,7 +136,7 @@ export function withCancel any>( const abortControllers = new WeakMap() -export function abortIfTimeout([context]: [TestContext?], error: Error): void { +export function abortIfTimeout([context]: [TestContext?, unknown?], error: Error): void { if (context) { abortContextSignal(context, error) } diff --git a/packages/runner/src/fixture.ts b/packages/runner/src/fixture.ts index 470a15145bbf..002bb7bc6063 100644 --- a/packages/runner/src/fixture.ts +++ b/packages/runner/src/fixture.ts @@ -291,13 +291,18 @@ export interface WithFixturesOptions { * Current fixtures from the context. */ fixtures?: TestFixtures + /** + * The suite to use for fixture lookups. + * Used by beforeEach/afterEach/aroundEach hooks to pick up fixture overrides from the test's describe block. + */ + suite?: Suite } const contextHasFixturesCache = new WeakMap>() export function withFixtures(fn: Function, options?: WithFixturesOptions) { const collector = getCurrentSuite() - const suite = collector.suite || collector.file + const suite = options?.suite || collector.suite || collector.file return async (hookContext?: TestContext): Promise => { const context: (TestContext & { [key: string]: any }) | undefined = hookContext || options?.context as TestContext diff --git a/packages/runner/src/hooks.ts b/packages/runner/src/hooks.ts index 9e3d122b3537..a8b800cb61f3 100644 --- a/packages/runner/src/hooks.ts +++ b/packages/runner/src/hooks.ts @@ -157,11 +157,17 @@ export function beforeEach( ): void { assertTypes(fn, '"beforeEach" callback', ['function']) const stackTraceError = new Error('STACK_TRACE_ERROR') + + const wrapper: BeforeEachListener = (context, suite) => { + const fixtureResolver = withFixtures(fn, { suite }) + return fixtureResolver(context) + } + return getCurrentSuite().on( 'beforeEach', Object.assign( withTimeout( - withFixtures(fn), + wrapper, timeout ?? getDefaultHookTimeout(), true, stackTraceError, @@ -197,10 +203,15 @@ export function afterEach( timeout?: number, ): void { assertTypes(fn, '"afterEach" callback', ['function']) + const wrapper: AfterEachListener = (context, suite) => { + const fixtureResolver = withFixtures(fn, { suite }) + return fixtureResolver(context) + } + return getCurrentSuite().on( 'afterEach', withTimeout( - withFixtures(fn), + wrapper, timeout ?? getDefaultHookTimeout(), true, new Error('STACK_TRACE_ERROR'), @@ -375,7 +386,7 @@ export function aroundEach( const innerFn = (ctx: any) => fn(runTest, ctx, suite) configureProps(innerFn, { index: 1, original: fn }) - const fixtureResolver = withFixtures(innerFn) + const fixtureResolver = withFixtures(innerFn, { suite }) return fixtureResolver(context) } diff --git a/test/core/test/test-extend.test.ts b/test/core/test/test-extend.test.ts index 9ef88738229d..4cdc453a76af 100644 --- a/test/core/test/test-extend.test.ts +++ b/test/core/test/test-extend.test.ts @@ -640,3 +640,136 @@ describe('builder pattern with non-function values', () => { expect(chainedSync).toBe('HELLO WORLD') }) }) + +// https://github.com/vitest-dev/vitest/issues/9810 +describe('override auto fixture with outer beforeEach', () => { + const myTest = test + .extend('base', { auto: true }, () => 'base:default') + .extend('derived', { auto: true }, ({ base }) => { + return `derived:${base}` + }) + + beforeEach(({ task }) => { + expect(task).toBeTruthy() + }) + + describe('with override', () => { + myTest.override('base', 'base:override') + + myTest('auto fixture sees overridden dependency', ({ base, derived }) => { + expect(base).toBe('base:override') + expect(derived).toBe('derived:base:override') + }) + }) +}) + +describe('override auto fixture with co-located beforeEach', () => { + const myTest = test + .extend('base', { auto: true }, () => 'base:default') + .extend('derived', { auto: true }, ({ base }) => { + return `derived:${base}` + }) + + myTest.override('base', 'base:override') + + beforeEach(({ task }) => { + expect(task).toBeTruthy() + }) + + myTest('override applies when beforeEach is co-located', ({ base, derived }) => { + expect(base).toBe('base:override') + expect(derived).toBe('derived:base:override') + }) +}) + +describe('override non-auto fixture with outer beforeEach', () => { + const myTest = test + .extend('base', () => 'base:default') + .extend('derived', ({ base }) => `derived:${base}`) + + beforeEach(({ task }) => { + expect(task).toBeTruthy() + }) + + describe('with override', () => { + myTest.override('base', 'base:override') + + myTest('override applies to non-auto dependency', ({ base, derived }) => { + expect(base).toBe('base:override') + expect(derived).toBe('derived:base:override') + }) + }) +}) + +describe('override fixture accessed in outer beforeEach', () => { + const myTest = test + .extend('base', () => 'base:default') + .extend('derived', ({ base }) => `derived:${base}`) + + const hookValues: string[] = [] + + myTest.beforeEach(({ base }) => { + hookValues.push(base) + }) + + describe('with override', () => { + myTest.override('base', 'base:override') + + myTest('beforeEach sees overridden fixture', ({ derived }) => { + expect(hookValues).toEqual(['base:override']) + expect(derived).toBe('derived:base:override') + }) + }) +}) + +describe('nested overrides with outer beforeEach', () => { + const myTest = test + .extend('base', { auto: true }, () => 'base:default') + .extend('derived', { auto: true }, ({ base }) => { + return `derived:${base}` + }) + + beforeEach(({ task }) => { + expect(task).toBeTruthy() + }) + + describe('outer', () => { + myTest.override('base', 'base:outer') + + myTest('outer override', ({ base, derived }) => { + expect(base).toBe('base:outer') + expect(derived).toBe('derived:base:outer') + }) + + describe('inner', () => { + myTest.override('base', 'base:inner') + + myTest('inner override wins', ({ base, derived }) => { + expect(base).toBe('base:inner') + expect(derived).toBe('derived:base:inner') + }) + }) + }) +}) + +describe('override fixture accessed in aroundEach', () => { + const myTest = test + .extend('base', () => 'base:default') + .extend('derived', ({ base }) => `derived:${base}`) + + const hookValues: string[] = [] + + myTest.aroundEach(async (runTest, { base }) => { + hookValues.push(base) + await runTest() + }) + + describe('with override', () => { + myTest.override('base', 'base:override') + + myTest('aroundEach sees overridden fixture', ({ derived }) => { + expect(hookValues).toEqual(['base:override']) + expect(derived).toBe('derived:base:override') + }) + }) +}) From 228067e3fe8ee516e05cc192e57c2dec8db04823 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Tue, 10 Mar 2026 16:35:20 +0900 Subject: [PATCH 2/2] docs: trace mark integration with vitest-browser-react/vue/svelte (#9828) Co-authored-by: Claude Opus 4.6 --- docs/api/browser/react.md | 6 +++++ docs/api/browser/svelte.md | 49 +++++++++++++++++++++++++------------- docs/api/browser/vue.md | 43 ++++++++++++++++++++++++--------- 3 files changed, 70 insertions(+), 28 deletions(-) diff --git a/docs/api/browser/react.md b/docs/api/browser/react.md index 6555db3061d1..4e87cd7176f6 100644 --- a/docs/api/browser/react.md +++ b/docs/api/browser/react.md @@ -39,6 +39,8 @@ export function render( ): Promise ``` +The `render` function records a `react.render` trace mark, visible in the [Trace View](/guide/browser/trace-view). + :::warning Note that `render` is asynchronous, unlike in other packages. This is to support [`Suspense`](https://react.dev/reference/react/Suspense) correctly. @@ -154,6 +156,8 @@ This method is a shortcut for `console.log(prettyDOM(baseElement))`. It will pri function rerender(ui: React.ReactNode): Promise ``` +Also records a `react.rerender` trace mark in the [Trace View](/guide/browser/trace-view). + It is better if you test the component that's doing the prop updating to ensure that the props are being updated correctly to avoid relying on implementation details in your tests. That said, if you'd prefer to update the props of a rendered component in your test, this function can be used to update props of the rendered component. ```jsx @@ -171,6 +175,8 @@ await rerender() function unmount(): Promise ``` +Also records a `react.unmount` trace mark in the [Trace View](/guide/browser/trace-view). + This will cause the rendered component to be unmounted. This is useful for testing what happens when your component is removed from the page (like testing that you don't leave event handlers hanging around causing memory leaks). ```jsx diff --git a/docs/api/browser/svelte.md b/docs/api/browser/svelte.md index a249f848cd6b..4ae2c964f820 100644 --- a/docs/api/browser/svelte.md +++ b/docs/api/browser/svelte.md @@ -12,7 +12,7 @@ import { expect, test } from 'vitest' import Component from './Component.svelte' test('counter button increments the count', async () => { - const screen = render(Component, { + const screen = await render(Component, { initialCount: 1, }) @@ -39,15 +39,26 @@ export function render( Component: ComponentImport, options?: ComponentOptions, renderOptions?: SetupOptions -): RenderResult +): RenderResult & PromiseLike> ``` +The `render` function records a `svelte.render` trace mark, visible in the [Trace View](/guide/browser/trace-view). + +::: warning +Synchronous usage of `render` is deprecated and will be removed in the next major version. Please always `await` the result: + +```ts +const screen = render(Component) // [!code --] +const screen = await render(Component) // [!code ++] +``` +::: + ### Options The `render` function supports either options that you can pass down to [`mount`](https://svelte.dev/docs/svelte/imperative-component-api#mount) or props directly: ```ts -const screen = render(Component, { +const screen = await render(Component, { props: { // [!code --] initialCount: 1, // [!code --] }, // [!code --] @@ -68,7 +79,7 @@ For example, if you are unit testing a `tbody` element, it cannot be a child of ```ts const table = document.createElement('table') -const screen = render(TableBody, { +const screen = await render(TableBody, { props, // ⚠️ appending the element to `body` manually before rendering target: document.body.appendChild(table), @@ -86,7 +97,7 @@ If the `target` is specified, then this defaults to that, otherwise this default In addition to documented return value, the `render` function also returns all available [locators](/api/browser/locators) relative to the [`baseElement`](#baseelement), including [custom ones](/api/browser/locators#custom-locators). ```ts -const screen = render(TableBody, props) +const screen = await render(TableBody, props) await screen.getByRole('link', { name: 'Expand' }).click() ``` @@ -104,7 +115,7 @@ If you find yourself using `container` to query for rendered elements then you s The mounted Svelte component instance. You can use this to access component methods and properties if needed. ```ts -const { component } = render(Counter, { +const { component } = await render(Counter, { initialCount: 0, }) @@ -118,7 +129,7 @@ The [locator](/api/browser/locators) of your `container`. It is useful to use qu ```ts import { render } from 'vitest-browser-svelte' -const { locator } = render(NumberDisplay, { +const { locator } = await render(NumberDisplay, { number: 2, }) @@ -139,15 +150,15 @@ This method is a shortcut for `console.log(prettyDOM(baseElement))`. It will pri #### rerender ```ts -function rerender(props: Partial>): void +function rerender(props: Partial>): Promise ``` -Updates the component's props and waits for Svelte to apply the changes. Use this to test how your component responds to prop changes. +Updates the component's props and waits for Svelte to apply the changes. Use this to test how your component responds to prop changes. Also records a `svelte.rerender` trace mark in the [Trace View](/guide/browser/trace-view). ```ts import { render } from 'vitest-browser-svelte' -const { rerender } = render(NumberDisplay, { +const { rerender } = await render(NumberDisplay, { number: 1, }) @@ -158,16 +169,20 @@ await rerender({ number: 2 }) #### unmount ```ts -function unmount(): void +function unmount(): Promise ``` -Unmount and destroy the Svelte component. This is useful for testing what happens when your component is removed from the page (like testing that you don't leave event handlers hanging around causing memory leaks). +Unmount and destroy the Svelte component. Also records a `svelte.unmount` trace mark in the [Trace View](/guide/browser/trace-view). This is useful for testing what happens when your component is removed from the page (like testing that you don't leave event handlers hanging around causing memory leaks). + +::: warning +Synchronous usage of `unmount` is deprecated and will be removed in the next major version. Please always `await` the result. +::: ```ts import { render } from 'vitest-browser-svelte' -const { container, unmount } = render(Component) -unmount() +const { container, unmount } = await render(Component) +await unmount() // your component has been unmounted and now: container.innerHTML === '' ``` @@ -193,7 +208,7 @@ locators.extend({ }, }) -const screen = render(Component) +const screen = await render(Component) await expect.element( screen.getByArticleTitle('Hello World') ).toBeVisible() @@ -211,7 +226,7 @@ import { expect, test } from 'vitest' import SubjectTest from './basic-snippet.test.svelte' test('basic snippet', async () => { - const screen = render(SubjectTest) + const screen = await render(SubjectTest) const heading = screen.getByRole('heading') const child = heading.getByTestId('child') @@ -250,7 +265,7 @@ import { expect, test } from 'vitest' import Subject from './complex-snippet.svelte' test('renders greeting in message snippet', async () => { - const screen = render(Subject, { + const screen = await render(Subject, { name: 'Alice', message: createRawSnippet(greeting => ({ render: () => `${greeting()}`, diff --git a/docs/api/browser/vue.md b/docs/api/browser/vue.md index 5fa64356e6f0..4695a47f787a 100644 --- a/docs/api/browser/vue.md +++ b/docs/api/browser/vue.md @@ -12,7 +12,7 @@ import { expect, test } from 'vitest' import Component from './Component.vue' test('counter button increments the count', async () => { - const screen = render(Component, { + const screen = await render(Component, { props: { initialCount: 1, } @@ -40,9 +40,20 @@ The package exposes two entry points: `vitest-browser-vue` and `vitest-browser-v export function render( component: Component, options?: ComponentRenderOptions, -): RenderResult +): RenderResult & PromiseLike ``` +The `render` function records a `vue.render` trace mark, visible in the [Trace View](/guide/browser/trace-view). + +::: warning +Synchronous usage of `render` is deprecated and will be removed in the next major version. Please always `await` the result: + +```ts +const screen = render(Component) // [!code --] +const screen = await render(Component) // [!code ++] +``` +::: + ### Options The `render` function supports all [`mount` options](https://test-utils.vuejs.org/api/#mount) from `@vue/test-utils` (except `attachTo` - use `container` instead). In addition to them, there are also `container` and `baseElement`. @@ -56,7 +67,7 @@ For example, if you are unit testing a `tbody` element, it cannot be a child of ```js const table = document.createElement('table') -const { container } = render(TableBody, { +const { container } = await render(TableBody, { props, // ⚠️ appending the element to `body` manually before rendering container: document.body.appendChild(table), @@ -72,7 +83,7 @@ If the `container` is specified, then this defaults to that, otherwise this defa In addition to documented return value, the `render` function also returns all available [locators](/api/browser/locators) relative to the [`baseElement`](#baseelement), including [custom ones](/api/browser/locators#custom-locators). ```ts -const screen = render(TableBody, { props }) +const screen = await render(TableBody, { props }) await screen.getByRole('link', { name: 'Expand' }).click() ``` @@ -102,7 +113,7 @@ The [locator](/api/browser/locators) of your `container`. It is useful to use qu ```js import { render } from 'vitest-browser-vue' -const { locator } = render(NumberDisplay, { +const { locator } = await render(NumberDisplay, { props: { number: 2 } }) @@ -125,27 +136,37 @@ This method is a shortcut for `console.log(prettyDOM(baseElement))`. It will pri #### rerender ```ts -function rerender(props: Partial): void +function rerender(props: Partial): void & PromiseLike ``` +Also records a `vue.rerender` trace mark in the [Trace View](/guide/browser/trace-view). + It is better if you test the component that's doing the prop updating to ensure that the props are being updated correctly to avoid relying on implementation details in your tests. That said, if you'd prefer to update the props of a rendered component in your test, this function can be used to update props of the rendered component. +::: warning +Synchronous usage of `rerender` is deprecated and will be removed in the next major version. Please always `await` the result. +::: + ```js import { render } from 'vitest-browser-vue' -const { rerender } = render(NumberDisplay, { props: { number: 1 } }) +const { rerender } = await render(NumberDisplay, { props: { number: 1 } }) // re-render the same component with different props -rerender({ number: 2 }) +await rerender({ number: 2 }) ``` #### unmount ```ts -function unmount(): void +function unmount(): void & PromiseLike ``` -This will cause the rendered component to be unmounted. This is useful for testing what happens when your component is removed from the page (like testing that you don't leave event handlers hanging around causing memory leaks). +This will cause the rendered component to be unmounted. Also records a `vue.unmount` trace mark in the [Trace View](/guide/browser/trace-view). This is useful for testing what happens when your component is removed from the page (like testing that you don't leave event handlers hanging around causing memory leaks). + +::: warning +Synchronous usage of `unmount` is deprecated and will be removed in the next major version. Please always `await` the result. +::: #### emitted @@ -182,7 +203,7 @@ locators.extend({ }, }) -const screen = render(Component) +const screen = await render(Component) await expect.element( screen.getByArticleTitle('Hello World') ).toBeVisible()