DEV Community

Sujal
Sujal

Posted on

3

Vitest + React Testing Library for Remix & React Router v7 (with TypeScript): A Complete Setup Guide

As a frontend developer working with Remix v2 and React Router v7, setting up a testing environment can be tricky. After battling through configuration issues and TypeScript conflicts, I've created this guide to solve three specific challenges:

  1. Configuring Vitest to work with Remix's path aliases
  2. Properly mocking React Router's useOutletContext hook with TypeScript support
  3. Handling window.matchMedia errors when testing UI components (especially with libraries like Mantine)

Let's dive into a complete solution that addresses all these issues!

Required Dependencies

First, install these packages:

npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom
Enter fullscreen mode Exit fullscreen mode

Optional but recommended:

npm install -D @vitest/ui @vitest/coverage-v8
Enter fullscreen mode Exit fullscreen mode

Package.json Scripts

Add these testing scripts:

{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "coverage": "vitest run --coverage"
  }
}
Enter fullscreen mode Exit fullscreen mode

Configuration Files

Create a separate vitest.config.ts in your project root:

import path from 'path';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./vitest.setup.ts'],
    include: ['**/*.{test,spec}.{ts,tsx}']
  },
  resolve: {
    alias: {
      '~': path.resolve(__dirname, './app')
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Create vitest.setup.ts:

import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

afterEach(() => {
  cleanup();
});

// Mock window.matchMedia for required components
Object.defineProperty(window, 'matchMedia', {
  writable: true,
  value: vi.fn().mockImplementation(query => ({
    matches: false,
    media: query,
    onchange: null,
    addListener: vi.fn(),
    removeListener: vi.fn(),
    addEventListener: vi.fn(),
    removeEventListener: vi.fn(),
    dispatchEvent: vi.fn(),
  })),
});
Enter fullscreen mode Exit fullscreen mode

Test Organization

For a feature-based folder structure (remix-flat-routes):

app/routes/feature+/
├── __tests__/
│   ├── routes/
│   │   └── index.test.tsx
│   ├── components/
│   │   └── FeatureCard.test.tsx
│   └── hooks/
│       └── useFeature.test.ts
Enter fullscreen mode Exit fullscreen mode

This structure offers several advantages:

  • Tests are co-located with the features they test, making them easier to find
  • The separation between route, component, and hook tests clarifies the test's purpose
  • It follows the same organization as your source code, maintaining consistency
  • When using flat routes, this structure ensures tests are excluded from the route generation

Mocking React Router's useOutletContext

One of the trickiest parts of testing Remix components is mocking the useOutletContext hook:

import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { MantineProvider } from '@mantine/core';

import { OutletContext } from '~/types';
import YourComponent from '../_components/YourComponent';

// Mock useOutletContext with TypeScript type safety
// replace 'react-router' with '@remix-run/react' if using remix
vi.mock('react-router', () => ({
  // Preserve all original exports from react-router
  ...vi.importActual('react-router'), // or '@remix-run/react'

  // Override only the useOutletContext function
  // The 'satisfies' operator ensures type safety without changing the return type
  useOutletContext: () => ({ language: 'en' } satisfies Partial<OutletContext>) // Using Partial allows us to mock only what we need
}));

describe('YourComponent', () => {
  it('should render correctly', () => {
    render(<YourComponent />);

    // Your assertions here
  });
});
Enter fullscreen mode Exit fullscreen mode

A Complete Component Test Example

Here's a real-world example testing a component that displays course metadata:

import { MantineProvider } from '@mantine/core';
import { render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

import { OutletContext } from '~/types';
import CourseMetaDataSummary from '../_components/CourseMetaDataSummary';

vi.mock('react-router', () => ({
  ...vi.importActual('react-router'),
  useOutletContext: () => ({ language: 'en' } satisfies Partial<OutletContext>)
}));

describe('CourseMetaDataSummary', () => {
  it('should render lessons count as 8', () => {
    render(
      <MantineProvider>
        <CourseMetaDataSummary lessons_count={8} members_count={20} />
      </MantineProvider>
    );

    const lessonsCount = screen.getByText('8 Lessons');
    expect(lessonsCount).toBeInTheDocument();
  });

  it('should render members count as 20', () => {
    render(
      <MantineProvider>
        <CourseMetaDataSummary lessons_count={8} members_count={20} />
      </MantineProvider>
    );

    const membersCount = screen.getByText('20 Members');
    expect(membersCount).toBeInTheDocument();
  });

  it('should render 1 lesson and 1 member', () => {
    render(
      <MantineProvider>
        <CourseMetaDataSummary lessons_count={1} members_count={1} />
      </MantineProvider>
    );

    const lessonsCount = screen.getByText('1 Lesson');
    const membersCount = screen.getByText('1 Member');

    expect(lessonsCount).toBeInTheDocument();
    expect(membersCount).toBeInTheDocument();
  });
});
Enter fullscreen mode Exit fullscreen mode

Common Gotchas & Solutions

  1. Path resolution issues

    • Add the resolve.alias configuration to match your tsconfig paths
  2. Mocking useOutletContext

    • Use vi.importActual to preserve the rest of the module
    • Use TypeScript's satisfies operator for type safety
  3. Components requiring window.matchMedia

    • Add the matchMedia mock to vitest.setup.ts

References

Hot sauce if you're wrong - web dev trivia for staff engineers

Hot sauce if you're wrong · web dev trivia for staff engineers (Chris vs Jeremy, Leet Heat S1.E4)

  • Shipping Fast: Test your knowledge of deployment strategies and techniques
  • Authentication: Prove you know your OAuth from your JWT
  • CSS: Demonstrate your styling expertise under pressure
  • Acronyms: Decode the alphabet soup of web development
  • Accessibility: Show your commitment to building for everyone

Contestants must answer rapid-fire questions across the full stack of modern web development. Get it right, earn points. Get it wrong? The spice level goes up!

Watch Video 🌶️🔥

Top comments (0)

Image of DataStax

Langflow: Simplify AI Agent Building

Langflow is the easiest way to build and deploy AI-powered agents. Try it out for yourself and see why.

Get started for free

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay