Mastering Vitest + React Testing Library: Fixing ‘beforeEach’, ‘toBeInTheDocument’, and JSDOM Gotchas
If you've started testing React apps with Vitest and React Testing Library, you've likely seen these errors:
Cannot find name 'beforeEach'.ts(2304)
Property 'toBeInTheDocument' does not exist on type 'Assertion'.
They look small, but they mean TypeScript doesn’t understand your Vitest globals or custom matchers yet.
This article is your go‑to reference for setting up type‑safe, fast, and realistic tests for React Query or any frontend project using Vitest.
Understanding the Setup
You might have code like this:
import { describe, it, expect, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import Todos from '../features/todos/Todos';
import { withQueryClient } from './test-utils';
describe('Todos (React Query)', () => {
beforeEach(() => {
// mock fetch calls
});
afterEach(() => {
// restore mocks
});
it('renders and can add a todo', async () => {
const { ui, Wrapper } = withQueryClient(<Todos />);
render(ui, { wrapper: Wrapper });
expect(await screen.findByText('First')).toBeInTheDocument();
});
});
If you see 'beforeEach'
or 'toBeInTheDocument'
errors, your TypeScript setup isn’t aware of Vitest globals or jest‑dom matchers.
Fix #1 — Enable Vitest Globals
In vitest.config.ts
:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true, // ✅ allows beforeEach, afterEach, describe, etc.
environment: 'jsdom', // ✅ enables browser-like testing
setupFiles: ['./src/setupTests.ts'], // optional setup
},
});
Then tell TypeScript about it.
tsconfig.json:
{
"compilerOptions": {
"types": ["vitest/globals"]
}
}
Now beforeEach
, afterEach
, describe
, and it
are all globally recognized.
Fix #2 — Add jest-dom
Matchers for toBeInTheDocument()
React Testing Library’s matchers (toBeInTheDocument
, toHaveTextContent
, etc.) come from @testing-library/jest-dom.
Add a setup file:
src/setupTests.ts
import { expect, afterEach } from 'vitest';
import matchers from '@testing-library/jest-dom/matchers';
import { cleanup } from '@testing-library/react';
// Extend Vitest's expect
expect.extend(matchers);
// Auto-clean between tests
afterEach(() => {
cleanup();
});
And load the matcher types:
tsconfig.json
{
"compilerOptions": {
"types": ["vitest/globals", "@testing-library/jest-dom"]
}
}
Now TypeScript knows about toBeInTheDocument()
and other DOM assertions.
Fix #3 — Import Instead of Globals (Alternative)
If you prefer explicit imports over global configuration:
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
This works perfectly with globals: false
.
Bonus — Realistic Testing Example with React Query
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import Todos from '../features/todos/Todos';
import { withQueryClient } from './test-utils';
const originalFetch = global.fetch;
describe('Todos (React Query)', () => {
beforeEach(() => {
global.fetch = vi.fn((input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url.includes('/todos?_limit=10')) {
return Promise.resolve(new Response(JSON.stringify([{ id: 1, title: 'First', completed: false }])));
}
if (url.endsWith('/todos') && init?.method === 'POST') {
return Promise.resolve(new Response(JSON.stringify({ id: 999, title: 'New', completed: false })));
}
if (url.includes('/todos/') && init?.method === 'PATCH') {
return Promise.resolve(new Response(JSON.stringify({})));
}
return Promise.resolve(new Response('{}', { status: 404 }));
}) as any;
});
afterEach(() => {
global.fetch = originalFetch;
});
it('renders and can add a todo (optimistic)', async () => {
const { ui, Wrapper } = withQueryClient(<Todos />);
render(ui, { wrapper: Wrapper });
expect(await screen.findByText('First')).toBeInTheDocument();
const input = screen.getByPlaceholderText('New todo…') as HTMLInputElement;
fireEvent.change(input, { target: { value: 'Write tests' } });
fireEvent.submit(input.closest('form')!);
expect(await screen.findByText(/Write tests/)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getAllByRole('checkbox').length).toBeGreaterThan(0);
});
});
});
Best Practices
Rule | Why |
---|---|
✅ globals: true in vitest.config.ts |
Removes repetitive imports |
✅ environment: 'jsdom'
|
Enables DOM & browser APIs |
✅ setupTests.ts
|
Centralized cleanup + matchers |
✅ Extend expect with @testing-library/jest-dom
|
Readable assertions |
✅ Include types in tsconfig.json
|
Removes TS errors |
🚫 Avoid mixing Jest + Vitest configs | Causes subtle type issues |
The Mental Model — TypeScript + Vitest + JSDOM
Layer | Purpose |
---|---|
Vitest | Runs the tests, provides globals (describe , it , vi , etc.) |
React Testing Library | Simulates user interactions & DOM behavior |
JSDOM | Virtual DOM for headless testing |
jest-dom | Adds human-readable matchers (toBeInTheDocument ) |
TypeScript | Provides static type-safety across all layers |
When they’re configured correctly, you get real browser‑like tests that TypeScript understands perfectly.
Why It Matters
Testing is part of production‑grade React engineering.
Vitest is fast, but with TypeScript’s stricter checks (especially with "verbatimModuleSyntax": true
), your imports must be precise — especially type‑only imports like import type { PropsWithChildren } from 'react'
and matcher types like @testing-library/jest-dom
.
This setup makes your test suite:
- Type‑safe (no stray global errors)
- Fast (Vitest + JSDOM = instant feedback)
- Predictable (automatic cleanup + matchers)
- Modern (aligned with React Query & TS 5+ ecosystem)
Conclusion
Next time you see:
Cannot find name 'beforeEach'
Property 'toBeInTheDocument' does not exist on type 'Assertion'
...don’t panic. They’re just signs that TypeScript needs a little more context.
By combining:
✅ globals: true
in Vitest
✅ expect.extend(matchers)
in setupTests.ts
✅ proper type references in tsconfig.json
You’ll have a modern, type-safe, lightning-fast testing setup for React apps — perfect for React Query, Suspense, or RSC projects.
✍️ Written by Cristian Sifuentes — Full‑stack developer & AI/JS enthusiast, passionate about React, TypeScript, and scalable testing architectures.
✅ Tags: #react #typescript #vitest #testing #frontend
Top comments (0)