This skill activates when writing tests for Chakra UI components, setting up React Testing Library with Chakra, testing user interactions, handling async operations, or ensuring accessibility with jest-axe. It provides comprehensive guidance on testing patterns that work with both Vitest and Jest, supporting modern React testing practices.
/plugin marketplace add Lobbi-Docs/claude/plugin install chakra-react-toolkit@claude-orchestrationThis skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill activates when writing tests for Chakra UI components, setting up React Testing Library with Chakra, testing user interactions, handling async operations, or ensuring accessibility with jest-axe. It provides comprehensive guidance on testing patterns that work with both Vitest and Jest, supporting modern React testing practices.
Install required testing dependencies:
# Core testing libraries
npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event
# For accessibility testing
npm install --save-dev jest-axe
# For Vitest (alternative to Jest)
npm install --save-dev vitest jsdom @vitest/ui
Configure Vitest for React component testing:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test/setup.ts',
css: true,
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
'**/mockData',
],
},
},
});
Configure Jest for React component testing:
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/src/test/setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
transform: {
'^.+\\.(ts|tsx)$': ['ts-jest', {
tsconfig: {
jsx: 'react-jsx',
},
}],
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/test/**',
],
};
Create a shared setup file for test utilities:
// src/test/setup.ts
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest'; // or from '@jest/globals' for Jest
// Cleanup after each test
afterEach(() => {
cleanup();
});
// Mock matchMedia (required for responsive components)
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: (query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: () => {},
removeListener: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => false,
}),
});
// Mock IntersectionObserver (for lazy loading components)
global.IntersectionObserver = class IntersectionObserver {
constructor() {}
disconnect() {}
observe() {}
takeRecords() {
return [];
}
unobserve() {}
} as any;
// Mock ResizeObserver (for responsive components)
global.ResizeObserver = class ResizeObserver {
constructor() {}
disconnect() {}
observe() {}
unobserve() {}
} as any;
Create a custom render function with ChakraProvider:
// src/test/utils.tsx
import { ReactElement } from 'react';
import { render, RenderOptions } from '@testing-library/react';
import { ChakraProvider } from '@chakra-ui/react';
import theme from '../theme'; // Your custom theme
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
theme?: any;
colorMode?: 'light' | 'dark';
}
// Custom render function
export function renderWithChakra(
ui: ReactElement,
{ theme: customTheme, colorMode = 'light', ...options }: CustomRenderOptions = {}
) {
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<ChakraProvider theme={customTheme || theme}>
{children}
</ChakraProvider>
);
return render(ui, { wrapper: Wrapper, ...options });
}
// Export everything from React Testing Library
export * from '@testing-library/react';
// Override render method
export { renderWithChakra as render };
Test component rendering and props:
import { describe, it, expect } from 'vitest'; // or from '@jest/globals'
import { render, screen } from '../test/utils';
import { Button } from '@chakra-ui/react';
describe('Button Component', () => {
it('renders with text', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('renders with different sizes', () => {
const { rerender } = render(<Button size="sm">Small</Button>);
expect(screen.getByRole('button')).toHaveClass('chakra-button');
rerender(<Button size="lg">Large</Button>);
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('renders with custom color scheme', () => {
render(<Button colorScheme="blue">Blue Button</Button>);
const button = screen.getByRole('button');
expect(button).toBeInTheDocument();
});
it('renders disabled state', () => {
render(<Button isDisabled>Disabled</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
it('renders loading state', () => {
render(<Button isLoading>Loading</Button>);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('data-loading');
});
});
Test custom Chakra-based components:
import { describe, it, expect } from 'vitest';
import { render, screen } from '../test/utils';
import { Box, Heading, Text } from '@chakra-ui/react';
// Custom Card component
function Card({ title, description }: { title: string; description: string }) {
return (
<Box
borderWidth="1px"
borderRadius="lg"
p={6}
shadow="md"
data-testid="card"
>
<Heading size="md" mb={2}>
{title}
</Heading>
<Text color="gray.600">{description}</Text>
</Box>
);
}
describe('Card Component', () => {
it('renders title and description', () => {
render(
<Card
title="Test Card"
description="This is a test description"
/>
);
expect(screen.getByRole('heading', { name: /test card/i })).toBeInTheDocument();
expect(screen.getByText(/this is a test description/i)).toBeInTheDocument();
});
it('applies correct styles', () => {
render(<Card title="Styled Card" description="Description" />);
const card = screen.getByTestId('card');
expect(card).toHaveStyle({
borderRadius: expect.any(String),
});
});
});
Test click handlers and user interactions:
import { describe, it, expect, vi } from 'vitest'; // or jest.fn() for Jest
import { render, screen } from '../test/utils';
import userEvent from '@testing-library/user-event';
import { Button } from '@chakra-ui/react';
describe('Click Events', () => {
it('calls onClick handler when clicked', async () => {
const user = userEvent.setup();
const handleClick = vi.fn(); // or jest.fn()
render(<Button onClick={handleClick}>Click me</Button>);
const button = screen.getByRole('button', { name: /click me/i });
await user.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('does not call onClick when disabled', async () => {
const user = userEvent.setup();
const handleClick = vi.fn();
render(
<Button isDisabled onClick={handleClick}>
Disabled
</Button>
);
const button = screen.getByRole('button');
await user.click(button);
expect(handleClick).not.toHaveBeenCalled();
});
it('handles double click', async () => {
const user = userEvent.setup();
const handleDoubleClick = vi.fn();
render(<Button onDoubleClick={handleDoubleClick}>Double Click</Button>);
const button = screen.getByRole('button');
await user.dblClick(button);
expect(handleDoubleClick).toHaveBeenCalledTimes(1);
});
});
Test form inputs and submission:
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '../test/utils';
import userEvent from '@testing-library/user-event';
import {
FormControl,
FormLabel,
Input,
Button,
FormErrorMessage,
} from '@chakra-ui/react';
function LoginForm({ onSubmit }: { onSubmit: (data: any) => void }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!email || !password) {
setError('All fields are required');
return;
}
onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<FormControl isInvalid={!!error}>
<FormLabel>Email</FormLabel>
<Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</FormControl>
<FormControl isInvalid={!!error}>
<FormLabel>Password</FormLabel>
<Input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{error && <FormErrorMessage>{error}</FormErrorMessage>}
</FormControl>
<Button type="submit">Submit</Button>
</form>
);
}
describe('LoginForm', () => {
it('submits form with valid data', async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
const emailInput = screen.getByLabelText(/email/i);
const passwordInput = screen.getByLabelText(/password/i);
const submitButton = screen.getByRole('button', { name: /submit/i });
await user.type(emailInput, 'test@example.com');
await user.type(passwordInput, 'password123');
await user.click(submitButton);
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
it('shows error for empty fields', async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();
render(<LoginForm onSubmit={handleSubmit} />);
const submitButton = screen.getByRole('button', { name: /submit/i });
await user.click(submitButton);
expect(screen.getByText(/all fields are required/i)).toBeInTheDocument();
expect(handleSubmit).not.toHaveBeenCalled();
});
});
Test keyboard interactions:
import { describe, it, expect } from 'vitest';
import { render, screen } from '../test/utils';
import userEvent from '@testing-library/user-event';
import { Button, useDisclosure, Modal, ModalContent } from '@chakra-ui/react';
function ModalExample() {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Button onClick={onOpen}>Open Modal</Button>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalContent>
<Button onClick={onClose}>Close</Button>
</ModalContent>
</Modal>
</>
);
}
describe('Keyboard Navigation', () => {
it('closes modal on Escape key', async () => {
const user = userEvent.setup();
render(<ModalExample />);
const openButton = screen.getByRole('button', { name: /open modal/i });
await user.click(openButton);
expect(screen.getByRole('dialog')).toBeInTheDocument();
await user.keyboard('{Escape}');
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
it('navigates through buttons with Tab', async () => {
const user = userEvent.setup();
render(
<>
<Button>First</Button>
<Button>Second</Button>
<Button>Third</Button>
</>
);
const first = screen.getByRole('button', { name: /first/i });
const second = screen.getByRole('button', { name: /second/i });
first.focus();
expect(first).toHaveFocus();
await user.tab();
expect(second).toHaveFocus();
});
});
Test components with async operations:
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '../test/utils';
import { Button, Spinner, Text } from '@chakra-ui/react';
function AsyncDataComponent() {
const [data, setData] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result.message);
} finally {
setLoading(false);
}
};
return (
<div>
<Button onClick={fetchData}>Fetch Data</Button>
{loading && <Spinner />}
{data && <Text>{data}</Text>}
</div>
);
}
describe('AsyncDataComponent', () => {
it('fetches and displays data', async () => {
const user = userEvent.setup();
// Mock fetch
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ message: 'Hello from API' }),
})
) as any;
render(<AsyncDataComponent />);
const button = screen.getByRole('button', { name: /fetch data/i });
await user.click(button);
// Wait for loading spinner
expect(screen.getByRole('status')).toBeInTheDocument();
// Wait for data to appear
await waitFor(() => {
expect(screen.getByText(/hello from api/i)).toBeInTheDocument();
});
// Spinner should be gone
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
it('handles fetch error', async () => {
const user = userEvent.setup();
global.fetch = vi.fn(() =>
Promise.reject(new Error('API Error'))
) as any;
render(<AsyncDataComponent />);
const button = screen.getByRole('button');
await user.click(button);
await waitFor(() => {
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
});
});
Test Chakra's toast notifications:
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '../test/utils';
import { Button, useToast } from '@chakra-ui/react';
function ToastExample() {
const toast = useToast();
const showToast = () => {
toast({
title: 'Success',
description: 'Operation completed',
status: 'success',
duration: 3000,
});
};
return <Button onClick={showToast}>Show Toast</Button>;
}
describe('Toast', () => {
it('displays toast on button click', async () => {
const user = userEvent.setup();
render(<ToastExample />);
const button = screen.getByRole('button', { name: /show toast/i });
await user.click(button);
await waitFor(() => {
expect(screen.getByText(/success/i)).toBeInTheDocument();
expect(screen.getByText(/operation completed/i)).toBeInTheDocument();
});
});
});
Mock color mode for testing:
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '../test/utils';
import * as ChakraUI from '@chakra-ui/react';
// Mock useColorMode
vi.spyOn(ChakraUI, 'useColorMode').mockImplementation(() => ({
colorMode: 'dark',
toggleColorMode: vi.fn(),
setColorMode: vi.fn(),
}));
describe('Dark Mode Component', () => {
it('renders with dark mode styles', () => {
function DarkModeComponent() {
const { colorMode } = ChakraUI.useColorMode();
const bg = colorMode === 'dark' ? 'gray.800' : 'white';
return <ChakraUI.Box bg={bg}>Content</ChakraUI.Box>;
}
render(<DarkModeComponent />);
// Test dark mode specific behavior
});
});
Mock responsive hooks:
import { describe, it, expect, vi } from 'vitest';
import * as ChakraUI from '@chakra-ui/react';
vi.spyOn(ChakraUI, 'useBreakpointValue').mockReturnValue('md');
describe('Responsive Component', () => {
it('renders mobile layout', () => {
// Override for this test
vi.spyOn(ChakraUI, 'useBreakpointValue').mockReturnValue('base');
// Test mobile-specific rendering
});
});
Test for accessibility violations:
import { describe, it, expect } from 'vitest';
import { render } from '../test/utils';
import { axe, toHaveNoViolations } from 'jest-axe';
import { FormControl, FormLabel, Input } from '@chakra-ui/react';
expect.extend(toHaveNoViolations);
describe('Accessibility', () => {
it('form has no accessibility violations', async () => {
const { container } = render(
<FormControl>
<FormLabel>Email</FormLabel>
<Input type="email" />
</FormControl>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('button has no accessibility violations', async () => {
const { container } = render(
<Button>Click me</Button>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
});
Test proper ARIA implementation:
import { describe, it, expect } from 'vitest';
import { render, screen } from '../test/utils';
import { IconButton } from '@chakra-ui/react';
import { CloseIcon } from '@chakra-ui/icons';
describe('ARIA Attributes', () => {
it('icon button has aria-label', () => {
render(
<IconButton
aria-label="Close dialog"
icon={<CloseIcon />}
/>
);
const button = screen.getByRole('button', { name: /close dialog/i });
expect(button).toHaveAttribute('aria-label', 'Close dialog');
});
it('loading button has aria-busy', () => {
render(<Button isLoading>Submit</Button>);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('data-loading');
});
});
Use these testing patterns to ensure your Chakra UI components work correctly, handle user interactions properly, and remain accessible to all users.