pnpm add -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
The Vite example with Vitest has all the files of this guide, and you can copy it.
Tell Vitest to use jsdom and to run a setup file before the tests:
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: './src/test/setup.ts'
}
})
Bolio UI components read the theme from BolioUIProvider, and useToasts only works inside it. Write the wrapper once and render every test through it:
import type { ReactElement } from 'react'
import { render } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import '@bolio-ui/core/styles.css'
import { BolioUIProvider } from '@bolio-ui/core'
export function renderWithBolio(ui: ReactElement) {
return {
user: userEvent.setup(),
...render(<BolioUIProvider>{ui}</BolioUIProvider>)
}
}
The stylesheet import is safe in tests: Vitest does not process it, and the component still renders.
Query by role and by name, as a person would. This test types in an Input, opens a Modal and checks the toast:
import { screen, waitFor, within } from '@testing-library/react'
import { expect, it } from 'vitest'
import App from './App.tsx'
import { renderWithBolio } from './test/render.tsx'
it('confirms in a modal and then shows a toast', async () => {
const { user } = renderWithBolio(<App />)
await user.type(screen.getByLabelText('Email'), 'ada@example.com')
await user.click(screen.getByRole('button', { name: 'Subscribe' }))
const dialog = await screen.findByRole('dialog', {
name: 'Confirm subscription'
})
await user.click(within(dialog).getByRole('button', { name: 'Confirm' }))
expect(await screen.findByText('Subscribed')).toBeInTheDocument()
await waitFor(() =>
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
)
})
Modal, a Drawer, a Popover and the toasts render in a portal, outside your component. Query the whole screen, not the container that render returns.Modal fades out before it leaves the page. After you close it, wait with waitFor or waitForElementToBeRemoved before you expect it to be gone.Input is named by its text child, such as <Input>Email</Input>, so getByLabelText('Email') finds it. Without a label, give it an aria-label.matchMedia does not need a mock: Bolio UI checks that it exists before it uses it.