Claude
Skills
Sign in
Back

testing

Included with Lifetime
$97 forever

Testing MUI components with React Testing Library — theme mocking, portal components, DataGrid, DatePicker, and accessibility testing

Web Dev

What this skill does


# Testing MUI Components

Comprehensive patterns for testing Material UI components with React Testing Library, covering theme integration, complex widgets, accessibility, and common pitfalls.

## 1. ThemeProvider Wrapper

MUI components rely on `ThemeProvider` for styling and behavior. Always wrap renders in your custom theme to get accurate test results.

### renderWithTheme Utility

```tsx
// test-utils.tsx
import { render, RenderOptions } from '@testing-library/react';
import { ThemeProvider, createTheme, Theme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { ReactElement } from 'react';

// Import your app's theme or create a default
const defaultTheme = createTheme({
  palette: {
    primary: { main: '#1976d2' },
    secondary: { main: '#dc004e' },
  },
  typography: {
    fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
  },
});

interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
  theme?: Theme;
}

export function renderWithTheme(
  ui: ReactElement,
  { theme = defaultTheme, ...options }: CustomRenderOptions = {},
) {
  function Wrapper({ children }: { children: React.ReactNode }) {
    return (
      <ThemeProvider theme={theme}>
        <CssBaseline />
        {children}
      </ThemeProvider>
    );
  }

  return render(ui, { wrapper: Wrapper, ...options });
}

// Re-export everything from RTL so tests import from one place
export * from '@testing-library/react';
export { renderWithTheme as render };
```

### Usage

```tsx
import { render, screen } from '../test-utils';
import { Button } from '@mui/material';

test('renders a primary button with theme colors', () => {
  render(<Button variant="contained" color="primary">Save</Button>);
  const button = screen.getByRole('button', { name: /save/i });
  expect(button).toBeInTheDocument();
  expect(button).toHaveClass('MuiButton-containedPrimary');
});
```

## 2. Portal Components

MUI Dialog, Menu, Popover, Snackbar, and Tooltip render into portals (appended to `document.body`). They are still found by RTL queries because RTL searches the entire document by default.

### Testing Dialog

```tsx
import { render, screen, waitFor } from '../test-utils';
import userEvent from '@testing-library/user-event';
import {
  Dialog,
  DialogTitle,
  DialogContent,
  DialogActions,
  Button,
} from '@mui/material';
import { useState } from 'react';

function ConfirmDialog() {
  const [open, setOpen] = useState(false);
  const [confirmed, setConfirmed] = useState(false);

  return (
    <>
      <Button onClick={() => setOpen(true)}>Delete</Button>
      {confirmed && <span>Item deleted</span>}
      <Dialog open={open} onClose={() => setOpen(false)}>
        <DialogTitle>Confirm Delete</DialogTitle>
        <DialogContent>Are you sure you want to delete this item?</DialogContent>
        <DialogActions>
          <Button onClick={() => setOpen(false)}>Cancel</Button>
          <Button onClick={() => { setConfirmed(true); setOpen(false); }}>
            Confirm
          </Button>
        </DialogActions>
      </Dialog>
    </>
  );
}

test('opens dialog, confirms, and shows result', async () => {
  const user = userEvent.setup();
  render(<ConfirmDialog />);

  // Dialog not visible yet
  expect(screen.queryByRole('dialog')).not.toBeInTheDocument();

  // Open the dialog
  await user.click(screen.getByRole('button', { name: /delete/i }));

  // Dialog is now visible (portal renders to body, but RTL finds it)
  expect(screen.getByRole('dialog')).toBeInTheDocument();
  expect(screen.getByText(/are you sure/i)).toBeInTheDocument();

  // Confirm
  await user.click(screen.getByRole('button', { name: /confirm/i }));

  // Dialog closed, result shown
  await waitFor(() => {
    expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
  });
  expect(screen.getByText(/item deleted/i)).toBeInTheDocument();
});
```

### Testing Menu

```tsx
test('opens menu and selects an option', async () => {
  const user = userEvent.setup();
  const onSelect = vi.fn();

  render(
    <MenuExample onSelect={onSelect} />
  );

  // Open menu
  await user.click(screen.getByRole('button', { name: /options/i }));

  // Menu items are in a portal but queryable
  const menuItems = screen.getAllByRole('menuitem');
  expect(menuItems).toHaveLength(3);

  // Select an item
  await user.click(screen.getByRole('menuitem', { name: /edit/i }));

  expect(onSelect).toHaveBeenCalledWith('edit');
  // Menu closes after selection
  await waitFor(() => {
    expect(screen.queryByRole('menu')).not.toBeInTheDocument();
  });
});
```

### Testing Snackbar

```tsx
test('shows snackbar on action and auto-hides', async () => {
  const user = userEvent.setup();
  // Use fake timers for autoHideDuration
  vi.useFakeTimers({ shouldAdvanceTime: true });

  render(<SnackbarExample />);

  await user.click(screen.getByRole('button', { name: /save/i }));

  expect(screen.getByRole('alert')).toHaveTextContent(/saved successfully/i);

  // Advance past autoHideDuration (default 6000ms)
  vi.advanceTimersByTime(6500);

  await waitFor(() => {
    expect(screen.queryByRole('alert')).not.toBeInTheDocument();
  });

  vi.useRealTimers();
});
```

## 3. DataGrid Testing

MUI DataGrid renders asynchronously and uses virtualization. Always wait for rows to appear.

### Waiting for Rows

```tsx
import { render, screen, within, waitFor } from '../test-utils';
import userEvent from '@testing-library/user-event';
import { DataGrid, GridColDef } from '@mui/x-data-grid';

const columns: GridColDef[] = [
  { field: 'id', headerName: 'ID', width: 70 },
  { field: 'name', headerName: 'Name', width: 200 },
  { field: 'email', headerName: 'Email', width: 250 },
];

const rows = [
  { id: 1, name: 'Alice Johnson', email: '[email protected]' },
  { id: 2, name: 'Bob Smith', email: '[email protected]' },
  { id: 3, name: 'Charlie Brown', email: '[email protected]' },
];

test('renders DataGrid with rows', async () => {
  render(
    <div style={{ height: 400, width: '100%' }}>
      <DataGrid rows={rows} columns={columns} />
    </div>
  );

  // DataGrid renders asynchronously - wait for rows
  await waitFor(() => {
    expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
  });

  // All rows present
  expect(screen.getByText('Bob Smith')).toBeInTheDocument();
  expect(screen.getByText('[email protected]')).toBeInTheDocument();
});
```

### Testing Sort

```tsx
test('sorts DataGrid by name column', async () => {
  const user = userEvent.setup();
  render(
    <div style={{ height: 400, width: '100%' }}>
      <DataGrid rows={rows} columns={columns} />
    </div>
  );

  await waitFor(() => {
    expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
  });

  // Click the Name column header to sort
  const nameHeader = screen.getByRole('columnheader', { name: /name/i });
  await user.click(nameHeader);

  // Verify sort indicator appears
  await waitFor(() => {
    expect(nameHeader).toHaveAttribute('aria-sort', 'ascending');
  });

  // Click again for descending
  await user.click(nameHeader);
  await waitFor(() => {
    expect(nameHeader).toHaveAttribute('aria-sort', 'descending');
  });
});
```

### Testing Filter

```tsx
test('filters DataGrid rows', async () => {
  const user = userEvent.setup();
  render(
    <div style={{ height: 400, width: '100%' }}>
      <DataGrid
        rows={rows}
        columns={columns}
        slots={{ toolbar: GridToolbar }}
      />
    </div>
  );

  await waitFor(() => {
    expect(screen.getByText('Alice Johnson')).toBeInTheDocument();
  });

  // Open filter panel via toolbar
  const filterButton = screen.getByRole('button', { name: /filter/i });
  await user.click(filterButton);

  // Select column, operator, enter value
  // (Filter panel renders in a portal)
  await waitFor(() => {
    expect(screen.getByRole('tooltip')).toBeInTheDocument();
  });
});
```

**Important DataGrid testing notes:**
- Always wrap DataGrid in a container with ex

Related in Web Dev