Back to all questions

How can I test Modal/Drawer/Popover components?

Last updated

Getting started

Before jumping into the testing part, make sure that you've configured Jest or Vitest in your project as specified in the documentation. Assume that render, screen and userEvent variables are imported from your project test-utils file.

This guide is applicable to:

Testing example

In all following examples we will use AuthModal component, it contains a button and a modal with a simple authentication form:

import { Button, Modal, PasswordInput, TextInput } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

export function AuthModal() {
  const [opened, { open, close }] = useDisclosure();

  return (
    <>
      <Modal title="Authenticate" opened={opened} onClose={close}>
        <form
          onSubmit={(event) => {
            event.preventDefault();
            close();
          }}
        >
          <TextInput data-autofocus label="Username" placeholder="Enter your username" />
          <PasswordInput label="Password" placeholder="Enter your password" />
          <Button type="submit">Log in</Button>
        </form>
      </Modal>

      <Button onClick={open}>Open authentication modal</Button>
    </>
  );
}

Failing tests

If try to write tests for AuthModal without any additional configuration, you will notice that they fail because, by default, modals use Transition component to animate opening and closing. Transition component uses setTimeout to delay animation start and @testing-library/react does not wait for setTimeout to finish.

Example of failing tests:

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

describe('AuthModal', () => {
  it('opens modal when button is clicked', async () => {
    render(<AuthModal />);
    await userEvent.click(screen.getByRole('button', { name: 'Open authentication modal' }));
    // ⛔ Test fails, modal heading is not in the document yet
    // Error message: TestingLibraryElementError: Unable to find an accessible element
    // with the role "heading" and name "Authenticate"
    expect(screen.getByRole('heading', { name: 'Authenticate' })).toBeInTheDocument();
  });
});

Fixing failing tests

The easiest way to fix this issue is to disable transitions in your tests. This can be done by creating a separate theme for tests. In this theme, you need to disable transitions for all components that you plan to test.

To create a custom theme for tests, replace your render function in test-utils folder with the following code:

import { render as testingLibraryRender } from '@testing-library/react';
import { createTheme, MantineProvider, mergeThemeOverrides, Modal } from '@mantine/core';
// Your project theme
import { theme } from '../theme';

// Merge your project theme with tests specific overrides
const testTheme = mergeThemeOverrides(
  theme,
  createTheme({
    components: {
      Modal: Modal.extend({
        defaultProps: {
          transitionProps: { duration: 0 },
        },
      }),
    },
  })
);

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

✅ Now the test from the previous example should pass is passing!

How to test that the modal is opened/closed?

To verify that the modal is opened, you can check that the modal heading is in the document and an interactive element with data-autofocus attribute has focus:

describe('AuthModal', () => {
  it('opens modal when button is clicked', async () => {
    render(<AuthModal />);
    await userEvent.click(screen.getByRole('button', { name: 'Open authentication modal' }));
    expect(screen.getByRole('heading', { name: 'Authenticate' })).toBeInTheDocument();
    expect(screen.getByRole('textbox', { name: 'Username' })).toHaveFocus();
  });
});

To verify that the modal has been closed, check that the modal heading is not in the document:

describe('AuthModal', () => {
  it('closes modal after the form has been submitted', async () => {
    render(<AuthModal />);
    await userEvent.click(screen.getByRole('button', { name: 'Open authentication modal' }));
    await userEvent.type(screen.getByRole('textbox', { name: 'Username' }), 'john.doe');
    await userEvent.type(screen.getByLabelText('Password'), 'password');
    await userEvent.click(screen.getByRole('button', { name: 'Log in' }));
    expect(screen.queryByRole('heading', { name: 'Authenticate' })).not.toBeInTheDocument();
  });
});