← Back to DevBytes

Testing Chakra UI Components: From Unit to E2E Tests

Testing Chakra UI Components: From Unit to E2E Tests

Chakra UI has become one of the most popular React component libraries thanks to its accessibility, theming system, and composable API. But as your application grows, so does the need for a robust testing strategy. This tutorial walks you through testing Chakra UI components at every level — from isolated unit tests to full end-to-end (E2E) flows — so you can ship with confidence.

Why Testing Chakra UI Components Matters

Although Chakra UI is well-tested internally, your usage of its components introduces custom logic, props, theming overrides, and composition patterns that can break. Testing helps you:

Prerequisites and Project Setup

Start with a React project that already uses Chakra UI. Then install the testing dependencies. We will use Jest, React Testing Library (RTL), and Playwright for E2E tests.

npm install --save-dev jest jest-environment-jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event @testing-library/dom
npm install --save-dev @playwright/test

Create a Jest setup file at src/setupTests.js:

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

// Mock matchMedia for Chakra UI responsive helpers
window.matchMedia = window.matchMedia || function () {
  return {
    matches: false,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  };
};

// Mock ResizeObserver used by some Chakra components
class ResizeObserverStub {
  observe() {}
  unobserve() {}
  disconnect() {}
}
window.ResizeObserver = ResizeObserverStub;

Update your jest.config.js to use this setup file:

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
  moduleNameMapper: {
    '\\.(css|less|scss)$': 'identity-obj-proxy',
  },
  transform: {
    '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
  },
};

Unit Testing Chakra UI Components

Unit tests verify that a single component renders correctly and behaves as expected in isolation. The key principle with React Testing Library is to test behavior, not implementation details. Query elements the way users would — by text, role, or label — rather than by class names or internal state.

Testing a Simple Button Component

Suppose you have a custom button built on top of Chakra's Button:

// src/components/SubmitButton.jsx
import { Button } from '@chakra-ui/react';

export function SubmitButton({ loading, children, ...props }) {
  return (
    <Button
      type="submit"
      colorScheme="blue"
      isLoading={loading}
      loadingText="Submitting"
      {...props}
    >
      {children}
    </Button>
  );
}

Here is the unit test:

// src/components/SubmitButton.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ChakraProvider } from '@chakra-ui/react';
import theme from '../theme';
import { SubmitButton } from './SubmitButton';

const renderWithChakra = (ui) => {
  return render(<ChakraProvider theme={theme}>{ui}</ChakraProvider>);
};

describe('SubmitButton', () => {
  it('renders the label text', () => {
    renderWithChakra(<SubmitButton>Save</SubmitButton>);
    expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
  });

  it('shows loading text when loading is true', () => {
    renderWithChakra(<SubmitButton loading>Save</SubmitButton>);
    expect(screen.getByRole('button', { name: /submitting/i })).toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /^save$/i })).not.toBeInTheDocument();
  });

  it('calls onClick when clicked', async () => {
    const user = userEvent.setup();
    const handleClick = jest.fn();
    renderWithChakra(<SubmitButton onClick={handleClick}>Save</SubmitButton>);
    await user.click(screen.getByRole('button', { name: /save/i }));
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when loading', () => {
    renderWithChakra(<SubmitButton loading>Save</SubmitButton>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});

Notice that we wrap every render in ChakraProvider. Without it, Chakra components will throw because they depend on the theme context. Creating a custom renderWithChakra helper keeps tests clean.

Testing Theme-Driven Styles

Chakra components derive their appearance from theme tokens. To verify that a custom color scheme is applied, you can inspect computed styles. However, jsdom does not compute CSS, so a more reliable approach is to assert on the rendered class or data attributes, or to use snapshot testing for style objects.

// src/components/AlertBanner.test.jsx
import { render, screen } from '@testing-library/react';
import { ChakraProvider } from '@chakra-ui/react';
import { AlertBanner } from './AlertBanner';
import theme from '../theme';

describe('AlertBanner', () => {
  it('renders with error status', () => {
    render(
      <ChakraProvider theme={theme}>
        <AlertBanner status="error" title="Something went wrong" />
      </ChakraProvider>
    );
    expect(screen.getByRole('alert')).toBeInTheDocument();
    expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
  });

  it('renders with success status', () => {
    render(
      <ChakraProvider theme={theme}>
        <AlertBanner status="success" title="Saved successfully" />
      </ChakraProvider>
    );
    expect(screen.getByRole('status')).toBeInTheDocument();
  });
});

Testing Form Components

Chakra's form components like Input, Select, and Checkbox work seamlessly with RTL because they render native HTML elements. Here is an example testing a login form:

// src/components/LoginForm.jsx
import { useState } from 'react';
import { Box, Button, FormControl, FormLabel, Input, Alert, AlertIcon } from '@chakra-ui/react';

export function LoginForm({ onSubmit }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!email || !password) {
      setError('Both fields are required');
      return;
    }
    setError('');
    onSubmit({ email, password });
  };

  return (
    <Box as="form" onSubmit={handleSubmit} data-testid="login-form">
      {error && (
        <Alert status="error" mb={4}>
          <AlertIcon />
          {error}
        </Alert>
      )}
      <FormControl mb={4}>
        <FormLabel htmlFor="email">Email</FormLabel>
        <Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
      </FormControl>
      <FormControl mb={4}>
        <FormLabel htmlFor="password">Password</FormLabel>
        <Input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} />
      </FormControl>
      <Button type="submit" colorScheme="blue" width="full">Log In</Button>
    </Box>
  );
}
// src/components/LoginForm.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ChakraProvider } from '@chakra-ui/react';
import { LoginForm } from './LoginForm';
import theme from '../theme';

describe('LoginForm', () => {
  const onSubmit = jest.fn();

  beforeEach(() => {
    onSubmit.mockClear();
  });

  it('shows an error when fields are empty', async () => {
    const user = userEvent.setup();
    render(
      <ChakraProvider theme={theme}>
        <LoginForm onSubmit={onSubmit} />
      </ChakraProvider>
    );
    await user.click(screen.getByRole('button', { name: /log in/i }));
    expect(screen.getByRole('alert')).toHaveTextContent(/both fields are required/i);
    expect(onSubmit).not.toHaveBeenCalled();
  });

  it('submits valid credentials', async () => {
    const user = userEvent.setup();
    render(
      <ChakraProvider theme={theme}>
        <LoginForm onSubmit={onSubmit} />
      </ChakraProvider>
    );
    await user.type(screen.getByLabelText(/email/i), 'user@example.com');
    await user.type(screen.getByLabelText(/password/i), 'secret123');
    await user.click(screen.getByRole('button', { name: /log in/i }));
    expect(onSubmit).toHaveBeenCalledWith({
      email: 'user@example.com',
      password: 'secret123',
    });
  });
});

Testing Responsive and Dark Mode Behavior

Chakra UI's responsive styles and color mode rely on matchMedia and a ColorModeScript. To test dark mode, you can use the useColorMode hook within a test wrapper.

// src/components/ThemeToggle.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';
import { ThemeToggle } from './ThemeToggle';
import theme from '../theme';

describe('ThemeToggle', () => {
  it('toggles between light and dark mode', async () => {
    const user = userEvent.setup();
    render(
      <>
        <ColorModeScript initialColorMode={theme.config.initialColorMode} />
        <ChakraProvider theme={theme}>
          <ThemeToggle />
        </ChakraProvider>
      </>
    );
    const toggle = screen.getByRole('button', { name: /toggle color mode/i });
    expect(toggle).toBeInTheDocument();
    await user.click(toggle);
    // The button label or icon should change after toggle
    expect(toggle).toBeInTheDocument();
  });
});

For responsive testing, you can mock matchMedia to return different values per test:

function setViewport(matches) {
  window.matchMedia = jest.fn().mockImplementation((query) => ({
    matches: query.includes('min-width') ? matches : false,
    media: query,
    addListener: jest.fn(),
    removeListener: jest.fn(),
    addEventListener: jest.fn(),
    removeEventListener: jest.fn(),
    dispatchEvent: jest.fn(),
  }));
}

it('renders mobile layout on small screens', () => {
  setViewport(false);
  renderWithChakra(<ResponsiveLayout />);
  expect(screen.getByTestId('mobile-menu')).toBeInTheDocument();
});

it('renders desktop layout on large screens', () => {
  setViewport(true);
  renderWithChakra(<ResponsiveLayout />);
  expect(screen.getByTestId('desktop-nav')).toBeInTheDocument();
});

Integration Testing with Mocked APIs

Integration tests verify that multiple components work together, often with mocked API calls. Use Mock Service Worker (MSW) to intercept network requests in your tests.

npm install --save-dev msw
// src/components/UserProfile.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { ChakraProvider } from '@chakra-ui/react';
import { UserProfile } from './UserProfile';
import theme from '../theme';

const server = setupServer(
  rest.get('/api/user/1', (req, res, ctx) => {
    return res(ctx.json({ id: 1, name: 'Ada Lovelace', email: 'ada@example.com' }));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('UserProfile', () => {
  it('displays user data after loading', async () => {
    render(
      <ChakraProvider theme={theme}>
        <UserProfile userId={1} />
      </ChakraProvider>
    );
    expect(screen.getByText(/loading/i)).toBeInTheDocument();
    await waitFor(() => {
      expect(screen.getByText(/ada lovelace/i)).toBeInTheDocument();
    });
    expect(screen.getByText(/ada@example.com/i)).toBeInTheDocument();
  });

  it('shows an error alert when the request fails', async () => {
    server.use(
      rest.get('/api/user/1', (req, res, ctx) => {
        return res(ctx.status(500));
      })
    );
    render(
      <ChakraProvider theme={theme}>
        <UserProfile userId={1} />
      </ChakraProvider>
    );
    await waitFor(() => {
      expect(screen.getByRole('alert')).toHaveTextContent(/failed to load/i);
    });
  });
});

End-to-End Testing with Playwright

E2E tests run against a real browser and a running dev server. They validate the entire user journey, including Chakra UI rendering, routing, and API integration. Playwright is an excellent choice because it is fast, reliable, and supports multiple browsers.

Initialize Playwright:

npx playwright init

This creates a playwright.config.ts file and an e2e directory. Configure the config to start your dev server automatically:

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120000,
  },
});

Now write an E2E test for a login flow:

// e2e/login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Login flow', () => {
  test('user can log in with valid credentials', async ({ page }) => {
    await page.goto('/login');

    // Verify Chakra UI rendered the form
    await expect(page.getByLabel(/email/i)).toBeVisible();
    await expect(page.getByLabel(/password/i)).toBeVisible();

    // Fill in the form
    await page.getByLabel(/email/i).fill('user@example.com');
    await page.getByLabel(/password/i).fill('secret123');

    // Submit
    await page.getByRole('button', { name: /log in/i }).click();

    // Verify navigation to dashboard
    await expect(page).toHaveURL(/\/dashboard/);
    await expect(page.getByText(/welcome/i)).toBeVisible();
  });

  test('shows error for empty fields', async ({ page }) => {
    await page.goto('/login');
    await page.getByRole('button', { name: /log in/i }).click();
    await expect(page.getByRole('alert')).toContainText(/both fields are required/i);
  });

  test('dark mode toggle changes background color', async ({ page }) => {
    await page.goto('/');
    const body = page.locator('body');
    const lightBg = await body.evaluate((el) => getComputedStyle(el).backgroundColor);

    await page.getByRole('button', { name: /toggle color mode/i }).click();
    await page.waitForTimeout(300);

    const darkBg = await body.evaluate((el) => getComputedStyle(el).backgroundColor);
    expect(darkBg).not.toBe(lightBg);
  });
});

Because Playwright runs in a real browser, you can assert on actual computed CSS values, making it ideal for verifying that Chakra theme tokens produce the expected visual output.

Testing Accessibility

Chakra UI components are built with accessibility in mind, but composition can introduce issues. Use jest-axe in unit tests and Playwright's axe-core integration in E2E tests.

npm install --save-dev jest-axe @axe-core/playwright
// src/components/ModalDialog.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ChakraProvider, Button, Modal, ModalBody, ModalContent, ModalHeader, ModalOverlay, ModalCloseButton, ModalFooter, useDisclosure } from '@chakra-ui/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import theme from '../theme';

expect.extend(toHaveNoViolations);

function DemoModal() {
  const { isOpen, onOpen, onClose } = useDisclosure();
  return (
    <>
      <Button onClick={onOpen}>Open Modal</Button>
      <Modal isOpen={isOpen} onClose={onClose}>
        <ModalOverlay />
        <ModalContent>
          <ModalHeader>Confirm Action</ModalHeader>
          <ModalCloseButton />
          <ModalBody>Are you sure you want to proceed?</ModalBody>
          <ModalFooter>
            <Button colorScheme="blue" mr={3} onClick={onClose}>Confirm</Button>
            <Button variant="ghost" onClick={onClose}>Cancel</Button>
          </ModalFooter>
        </ModalContent>
      </Modal>
    </>
  );
}

describe('DemoModal accessibility', () => {
  it('has no accessibility violations when open', async () => {
    const user = userEvent.setup();
    const { container } = render(
      <ChakraProvider theme={theme}><DemoModal /></ChakraProvider>
    );
    await user.click(screen.getByRole('button', { name: /open modal/i }));
    const results = await axe(container);
    expect(results).toHaveNoViolations();
  });
});

Best Practices

Conclusion

Testing Chakra UI components effectively requires a layered approach. Unit tests with React Testing Library validate individual component behavior and accessibility, integration tests with MSW confirm that components work together with real data flows, and Playwright E2E tests verify the complete user experience in a real browser. By wrapping every test in ChakraProvider, mocking browser APIs like matchMedia, and focusing on user-facing behavior rather than implementation details, you build a test suite that remains stable across Chakra UI upgrades. Combine these techniques with automated accessibility checks and CI integration, and you will catch regressions early while maintaining a fast, confident development workflow.

— Ad —

Google AdSense will appear here after approval

← Back to all articles