Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { render } from 'vitest-browser-react'
import { expect, test } from 'vitest'

test('counter button increments the count', async () => {
const screen = render(<Component count={1} />)
const screen = await render(<Component count={1} />)

await screen.getByRole('button', { name: 'Increment' }).click()

Expand All @@ -29,9 +29,9 @@ import { renderHook } from 'vitest-browser-react'
import { expect, test } from 'vitest'

test('should increment counter', async () => {
const { result, act } = renderHook(() => useCounter())
const { result, act } = await renderHook(() => useCounter())

act(() => {
await act(() => {
result.current.increment()
})

Expand Down Expand Up @@ -64,9 +64,9 @@ import 'vitest-browser-react'
import { page } from '@vitest/browser/context'

test('counter button increments the count', async () => {
const screen = page.render(<Component count={1} />)
const screen = await page.render(<Component count={1} />)

screen.cleanup()
await screen.cleanup()
})
```

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,17 +69,17 @@
},
"devDependencies": {
"@antfu/eslint-config": "^2.24.1",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.3.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.3",
"@vitest/browser": "^3.1.0",
"bumpp": "^9.4.2",
"changelogithub": "^0.13.9",
"eslint": "^9.8.0",
"playwright": "^1.46.0",
"react": "^18.0.0",
"react": "^19.0.0",
"react-aria-components": "^1.10.1",
"react-dom": "^18.0.0",
"react-dom": "^19.0.0",
"tsup": "^8.2.4",
"tsx": "^4.17.0",
"typescript": "^5.5.4",
Expand Down
1,785 changes: 884 additions & 901 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ page.extend({
[Symbol.for('vitest:component-cleanup')]: cleanup,
})

beforeEach(() => {
cleanup()
beforeEach(async () => {
await cleanup()
})

declare module '@vitest/browser/context' {
Expand Down
52 changes: 27 additions & 25 deletions src/pure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import ReactDOMClient from 'react-dom/client'
// we call act only when rendering to flush any possible effects
// usually the async nature of Vitest browser mode ensures consistency,
// but rendering is sync and controlled by React directly
function act(cb: () => unknown) {
async function act(cb: () => unknown) {
// @ts-expect-error unstable_act is not typed, but exported
const _act = React.act || React.unstable_act
if (typeof _act !== 'function') {
Expand All @@ -16,7 +16,7 @@ function act(cb: () => unknown) {
else {
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true
try {
_act(cb)
await _act(cb)
}
finally {
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = false
Expand All @@ -32,8 +32,8 @@ export interface RenderResult extends LocatorSelectors {
maxLength?: number,
options?: PrettyDOMOptions
) => void
unmount: () => void
rerender: (ui: React.ReactNode) => void
unmount: () => Promise<void>
rerender: (ui: React.ReactNode) => Promise<void>
asFragment: () => DocumentFragment
}

Expand All @@ -51,10 +51,10 @@ const mountedRootEntries: {
root: ReturnType<typeof createConcurrentRoot>
}[] = []

export function render(
export async function render(
ui: React.ReactNode,
{ container, baseElement, wrapper: WrapperComponent }: ComponentRenderOptions = {},
): RenderResult {
): Promise<RenderResult> {
if (!baseElement) {
// default to document.body instead of documentElement to avoid output of potentially-large
// head elements (such as JSS style blocks) in debug output
Expand Down Expand Up @@ -87,7 +87,7 @@ export function render(
})
}

act(() => {
await act(() => {
root!.render(
strictModeIfNeeded(wrapUiIfNeeded(ui, WrapperComponent)),
)
Expand All @@ -97,13 +97,13 @@ export function render(
container,
baseElement,
debug: (el, maxLength, options) => debug(el, maxLength, options),
unmount: () => {
act(() => {
unmount: async () => {
await act(() => {
root.unmount()
})
},
rerender: (newUi: React.ReactNode) => {
act(() => {
rerender: async (newUi: React.ReactNode) => {
await act(() => {
root.render(
strictModeIfNeeded(wrapUiIfNeeded(newUi, WrapperComponent)),
)
Expand All @@ -128,7 +128,7 @@ export interface RenderHookResult<Result, Props> {
/**
* Triggers a re-render. The props will be passed to your renderHook callback.
*/
rerender: (props?: Props) => void
rerender: (props?: Props) => Promise<void>
/**
* This is a stable reference to the latest value returned by your renderHook
* callback
Expand All @@ -143,14 +143,14 @@ export interface RenderHookResult<Result, Props> {
* Unmounts the test component. This is useful for when you need to test
* any cleanup your useEffects have.
*/
unmount: () => void
unmount: () => Promise<void>
/**
* A test helper to apply pending React updates before making assertions.
*/
act: (callback: () => unknown) => void
act: (callback: () => unknown) => Promise<void>
}

export function renderHook<Props, Result>(renderCallback: (initialProps?: Props) => Result, options: RenderHookOptions<Props> = {}): RenderHookResult<Result, Props> {
export async function renderHook<Props, Result>(renderCallback: (initialProps?: Props) => Result, options: RenderHookOptions<Props> = {}): Promise<RenderHookResult<Result, Props>> {
const { initialProps, ...renderOptions } = options

const result = React.createRef<Result>() as unknown as { current: Result }
Expand All @@ -165,7 +165,7 @@ export function renderHook<Props, Result>(renderCallback: (initialProps?: Props)
return null
}

const { rerender: baseRerender, unmount } = render(
const { rerender: baseRerender, unmount } = await render(
<TestComponent renderCallbackProps={initialProps} />,
renderOptions,
)
Expand All @@ -179,15 +179,17 @@ export function renderHook<Props, Result>(renderCallback: (initialProps?: Props)
return { result, rerender, unmount, act }
}

export function cleanup(): void {
mountedRootEntries.forEach(({ root, container }) => {
act(() => {
root.unmount()
})
if (container.parentNode === document.body) {
document.body.removeChild(container)
}
})
export async function cleanup(): Promise<void> {
await Promise.all(
mountedRootEntries.map(async ({ root, container }) => {
await act(() => {
root.unmount()
})
if (container.parentNode === document.body) {
document.body.removeChild(container)
}
}),
)
mountedRootEntries.length = 0
mountedContainers.clear()
}
Expand Down
4 changes: 2 additions & 2 deletions test/fixtures/HelloWorld.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export function HelloWorld(): React.ReactElement {
return <div>Hello World</div>
export function HelloWorld({ name = 'World' }: { name?: string }): React.ReactElement {
return <div>{`Hello ${name}`}</div>
}
11 changes: 11 additions & 0 deletions test/fixtures/SuspendedHelloWorld.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { use } from 'react'

const fakeCacheLoadPromise = new Promise<void>((resolve) => {
setTimeout(() => resolve(), 100)
})

export function SuspendedHelloWorld({ name }: { name: string }): React.ReactElement {
use(fakeCacheLoadPromise)

return <div>{`Hello ${name}`}</div>
}
12 changes: 6 additions & 6 deletions test/render-hook.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import React from 'react'
import { renderHook } from '../src/index'
import { useCounter } from './fixtures/useCounter'

test('should increment counter', () => {
const { result, act } = renderHook(() => useCounter())
test('should increment counter', async () => {
const { result, act } = await renderHook(() => useCounter())

act(() => {
await act(() => {
result.current.increment()
})

expect(result.current.count).toBe(1)
})

test('allows rerendering', () => {
const { result, rerender } = renderHook(
test('allows rerendering', async () => {
const { result, rerender } = await renderHook(
(initialProps) => {
const [left, setLeft] = React.useState('left')
const [right, setRight] = React.useState('right')
Expand Down Expand Up @@ -47,7 +47,7 @@ test('allows wrapper components', async () => {
function Wrapper({ children }: PropsWithChildren) {
return <Context.Provider value="provided">{children}</Context.Provider>
}
const { result } = renderHook(
const { result } = await renderHook(
() => {
return React.useContext(Context)
},
Expand Down
30 changes: 26 additions & 4 deletions test/render.test.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,28 @@
import { expect, test, vi } from 'vitest'
import { afterEach, beforeEach, expect, test, vi } from 'vitest'
import { page, userEvent } from '@vitest/browser/context'
import { Button } from 'react-aria-components'
import { Suspense } from 'react'
import { render } from '../src/index'
import { HelloWorld } from './fixtures/HelloWorld'
import { Counter } from './fixtures/Counter'
import { SuspendedHelloWorld } from './fixtures/SuspendedHelloWorld'

beforeEach(() => {
vi.useFakeTimers()
})

afterEach(() => {
vi.useRealTimers()
})

test('renders simple component', async () => {
const screen = render(<HelloWorld />)
const screen = await render(<HelloWorld />)
await expect.element(page.getByText('Hello World')).toBeVisible()
expect(screen.container).toMatchSnapshot()
})

test('renders counter', async () => {
const screen = render(<Counter initialCount={1} />)
const screen = await render(<Counter initialCount={1} />)

await expect.element(screen.getByText('Count is 1')).toBeVisible()
await screen.getByRole('button', { name: 'Increment' }).click()
Expand All @@ -21,8 +31,20 @@ test('renders counter', async () => {

test('should fire the onPress/onClick handler', async () => {
const handler = vi.fn()
const screen = page.render(<Button onPress={handler}>Button</Button>)
const screen = await page.render(<Button onPress={handler}>Button</Button>)
await userEvent.click(screen.getByRole('button'))
// await screen.getByRole('button').click()
expect(handler).toHaveBeenCalled()
})

test('waits for suspended boundaries', async () => {
const result = render(<SuspendedHelloWorld name="Vitest" />, {
wrapper: ({ children }) => (
<Suspense fallback={<div>Suspended!</div>}>{children}</Suspense>
),
})
await expect.element(page.getByText('Suspended!')).toBeInTheDocument()
vi.runAllTimers()
await result
await expect.element(page.getByText('Hello Vitest')).toBeInTheDocument()
})