Mastering Vitest + React Testing Library: Fixing ‘beforeEach’, ‘toBeInTheDocument’, and JSDOM Gotchas



This content originally appeared on DEV Community and was authored by Cristian Sifuentes

Mastering Vitest + React Testing Library: Fixing ‘beforeEach’, ‘toBeInTheDocument’, and JSDOM Gotchas

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


This content originally appeared on DEV Community and was authored by Cristian Sifuentes