DEV Community

Sujal
Sujal

Posted on

2

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

Playwright CLI Flags Tutorial

5 Playwright CLI Flags That Will Transform Your Testing Workflow

  • 0:56 --last-failed: Zero in on just the tests that failed in your previous run
  • 2:34 --only-changed: Test only the spec files you've modified in git
  • 4:27 --repeat-each: Run tests multiple times to catch flaky behavior before it reaches production
  • 5:15 --forbid-only: Prevent accidental test.only commits from breaking your CI pipeline
  • 5:51 --ui --headed --workers 1: Debug visually with browser windows and sequential test execution

Learn how these powerful command-line options can save you time, strengthen your test suite, and streamline your Playwright testing experience. Click on any timestamp above to jump directly to that section in the tutorial!

Watch Full Video 📹️

Top comments (0)

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

👋 Kindness is contagious

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

Okay