Files
monitoring-app/.agents/skills/mantine-dev/references/testing.md
bipproduction 39d659acd0 skills
2026-04-01 10:43:03 +08:00

9.1 KiB

Testing Reference

Guide for testing Mantine applications with Vitest and React Testing Library.

Installation

npm install -D vitest jsdom @testing-library/dom @testing-library/jest-dom @testing-library/react @testing-library/user-event

Vitest Configuration

Add to vite.config.ts:

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './vitest.setup.mjs',
  },
});

Setup File

Create vitest.setup.mjs:

import '@testing-library/jest-dom/vitest';
import { vi } from 'vitest';

// Fix for getComputedStyle
const { getComputedStyle } = window;
window.getComputedStyle = (elt) => getComputedStyle(elt);

// Mock scrollIntoView
window.HTMLElement.prototype.scrollIntoView = () => {};

// Mock matchMedia (required by Mantine)
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(),
  })),
});

// Mock ResizeObserver (required by some Mantine components)
class ResizeObserver {
  observe() {}
  unobserve() {}
  disconnect() {}
}
window.ResizeObserver = ResizeObserver;

Custom Render Function

All Mantine components require MantineProvider. Create custom render:

// test-utils/render.tsx
import { render as testingLibraryRender } from '@testing-library/react';
import { MantineProvider } from '@mantine/core';
import { theme } from '../src/theme'; // Your theme if any

export function render(ui: React.ReactNode) {
  return testingLibraryRender(<>{ui}</>, {
    wrapper: ({ children }) => (
      <MantineProvider theme={theme} env="test">
        {children}
      </MantineProvider>
    ),
  });
}

Important: env="test"

Setting env="test" on MantineProvider:

  • Disables CSS transitions (tests run faster)
  • Disables portals (elements render in place, easier to query)

Export Test Utilities

// test-utils/index.ts
import userEvent from '@testing-library/user-event';

export * from '@testing-library/react';
export { render } from './render';
export { userEvent };

Writing Tests

Basic Component Test

// Button.test.tsx
import { render, screen } from '../test-utils';
import { Button } from '@mantine/core';

describe('Button', () => {
  it('renders children', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
  });

  it('handles click events', async () => {
    const onClick = vi.fn();
    render(<Button onClick={onClick}>Click</Button>);
    
    await userEvent.click(screen.getByRole('button'));
    expect(onClick).toHaveBeenCalledTimes(1);
  });
});

Testing Form Inputs

import { render, screen } from '../test-utils';
import { userEvent } from '../test-utils';
import { TextInput } from '@mantine/core';

describe('TextInput', () => {
  it('accepts user input', async () => {
    const onChange = vi.fn();
    render(<TextInput onChange={onChange} label="Name" />);
    
    const input = screen.getByLabelText('Name');
    await userEvent.type(input, 'John Doe');
    
    expect(input).toHaveValue('John Doe');
    expect(onChange).toHaveBeenCalled();
  });

  it('displays error', () => {
    render(<TextInput label="Email" error="Invalid email" />);
    expect(screen.getByText('Invalid email')).toBeInTheDocument();
  });
});

Testing useForm

import { render, screen, waitFor } from '../test-utils';
import { userEvent } from '../test-utils';
import { useForm } from '@mantine/form';
import { TextInput, Button } from '@mantine/core';

function TestForm() {
  const form = useForm({
    mode: 'uncontrolled',
    initialValues: { email: '' },
    validate: {
      email: (v) => (!v.includes('@') ? 'Invalid email' : null),
    },
  });

  return (
    <form onSubmit={form.onSubmit((values) => console.log(values))}>
      <TextInput
        label="Email"
        key={form.key('email')}
        {...form.getInputProps('email')}
      />
      <Button type="submit">Submit</Button>
    </form>
  );
}

describe('Form', () => {
  it('validates on submit', async () => {
    render(<TestForm />);
    
    await userEvent.click(screen.getByRole('button', { name: /submit/i }));
    
    expect(screen.getByText('Invalid email')).toBeInTheDocument();
  });

  it('clears error on valid input', async () => {
    render(<TestForm />);
    
    await userEvent.type(screen.getByLabelText('Email'), 'test@email.com');
    await userEvent.click(screen.getByRole('button', { name: /submit/i }));
    
    expect(screen.queryByText('Invalid email')).not.toBeInTheDocument();
  });
});

Testing Modals

With env="test", modals render in place (no portal):

import { render, screen } from '../test-utils';
import { userEvent } from '../test-utils';
import { useDisclosure } from '@mantine/hooks';
import { Modal, Button } from '@mantine/core';

function ModalDemo() {
  const [opened, { open, close }] = useDisclosure(false);
  return (
    <>
      <Button onClick={open}>Open</Button>
      <Modal opened={opened} onClose={close} title="Test Modal">
        Modal Content
      </Modal>
    </>
  );
}

describe('Modal', () => {
  it('opens when button is clicked', async () => {
    render(<ModalDemo />);
    
    expect(screen.queryByText('Modal Content')).not.toBeInTheDocument();
    
    await userEvent.click(screen.getByRole('button', { name: /open/i }));
    
    expect(screen.getByText('Modal Content')).toBeInTheDocument();
  });
});

Testing Select/Dropdown

import { render, screen } from '../test-utils';
import { userEvent } from '../test-utils';
import { Select } from '@mantine/core';

describe('Select', () => {
  it('selects an option', async () => {
    const onChange = vi.fn();
    render(
      <Select
        label="Country"
        data={['USA', 'Canada', 'UK']}
        onChange={onChange}
      />
    );
    
    // Open dropdown
    await userEvent.click(screen.getByLabelText('Country'));
    
    // Select option
    await userEvent.click(screen.getByRole('option', { name: 'Canada' }));
    
    expect(onChange).toHaveBeenCalledWith('Canada');
  });
});

Testing Color Scheme

import { render, screen } from '../test-utils';
import { useMantineColorScheme } from '@mantine/core';

function ColorSchemeToggle() {
  const { colorScheme, toggleColorScheme } = useMantineColorScheme();
  return (
    <button onClick={toggleColorScheme}>
      Current: {colorScheme}
    </button>
  );
}

describe('Color Scheme', () => {
  it('toggles color scheme', async () => {
    render(<ColorSchemeToggle />);
    
    expect(screen.getByText(/current: light/i)).toBeInTheDocument();
    
    await userEvent.click(screen.getByRole('button'));
    
    expect(screen.getByText(/current: dark/i)).toBeInTheDocument();
  });
});

Testing Notifications

import { render, screen, waitFor } from '../test-utils';
import { notifications } from '@mantine/notifications';
import { Notifications } from '@mantine/notifications';
import { Button } from '@mantine/core';

function NotificationDemo() {
  return (
    <>
      <Notifications />
      <Button onClick={() => notifications.show({ message: 'Hello!' })}>
        Show Notification
      </Button>
    </>
  );
}

describe('Notifications', () => {
  it('shows notification', async () => {
    render(<NotificationDemo />);
    
    await userEvent.click(screen.getByRole('button'));
    
    await waitFor(() => {
      expect(screen.getByText('Hello!')).toBeInTheDocument();
    });
  });
});

Testing Hooks

import { renderHook, act } from '@testing-library/react';
import { useDisclosure } from '@mantine/hooks';

describe('useDisclosure', () => {
  it('toggles state', () => {
    const { result } = renderHook(() => useDisclosure(false));
    
    expect(result.current[0]).toBe(false);
    
    act(() => {
      result.current[1].open();
    });
    
    expect(result.current[0]).toBe(true);
    
    act(() => {
      result.current[1].close();
    });
    
    expect(result.current[0]).toBe(false);
  });
});

Scripts

Add to package.json:

{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage"
  }
}

Common Issues

matchMedia Error

Ensure matchMedia mock is in setup file.

ResizeObserver Error

Ensure ResizeObserver mock is in setup file.

Portal Elements Not Found

Use env="test" on MantineProvider to disable portals.

Transitions Cause Timing Issues

Use env="test" to disable transitions.

Elements Not in Document

Make sure to use custom render that includes MantineProvider.

Testing Checklist

  • Setup file mocks matchMedia and ResizeObserver
  • Custom render includes MantineProvider with env="test"
  • Use userEvent for user interactions (not fireEvent)
  • Use waitFor for async operations
  • Use getByRole with accessible names when possible
  • Test error states and edge cases