DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

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

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'.
Enter fullscreen mode Exit fullscreen mode

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();
  });
});
Enter fullscreen mode Exit fullscreen mode

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
  },
});
Enter fullscreen mode Exit fullscreen mode

Then tell TypeScript about it.

tsconfig.json:

{
  "compilerOptions": {
    "types": ["vitest/globals"]
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

And load the matcher types:

tsconfig.json

{
  "compilerOptions": {
    "types": ["vitest/globals", "@testing-library/jest-dom"]
  }
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

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);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

...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)