Testing with Vitest

Install

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.

Configure Vitest

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'
  }
})

Render with the provider

Bolio UI components read the theme from BolioUIProvider, and useToasts only works inside it. Write the wrapper once and render every test through it:

src/test/render.tsx
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.

Write a test

Query by role and by name, as a person would. This test types in an Input, opens a Modal and checks the toast:

src/App.test.tsx
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()
  )
})

What to keep in mind

  • • A 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.
  • • A Modal fades out before it leaves the page. After you close it, wait with waitFor or waitForElementToBeRemoved before you expect it to be gone.
  • • An 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.
  • • jsdom has no layout, so sizes and positions are always zero. Test what the component shows and does, not where it is.
  • • matchMedia does not need a mock: Bolio UI checks that it exists before it uses it.