9.1 KiB
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
userEventfor user interactions (notfireEvent) - Use
waitForfor async operations - Use
getByRolewith accessible names when possible - Test error states and edge cases