Hone logo
Hone
Problems

Custom Jest Render Function for Component Testing

Testing React components effectively often requires simulating user interactions and checking their rendered output. While Jest and React Testing Library provide excellent tools, sometimes you need a more specialized rendering environment to mock specific dependencies or set up particular global states for your tests. This challenge focuses on creating a custom render function that enhances the testing experience by providing a pre-configured rendering context.

Problem Description

Your task is to create a custom Jest render function that wraps the standard render function from @testing-library/react. This custom function should allow you to:

  1. Provide a default set of providers that are common across your application (e.g., a Redux Provider, a Theme Provider, a React Query Client Provider).
  2. Allow overriding or extending these default providers on a per-test basis.
  3. Return additional utilities useful for testing, such as a mock navigate function if you are using a routing library.

The custom render function should accept the component to be rendered and an optional configuration object. This configuration object should allow users to pass their own providers array, which will merge with or replace the default ones.

Key Requirements:

  • Create a function named customRender that mimics the signature of @testing-library/react's render function.
  • Implement a mechanism to define and use default providers.
  • Implement a mechanism to allow users to specify custom providers for individual tests.
  • The custom providers should wrap the rendered component.
  • Consider how to handle common scenarios like routing.

Expected Behavior:

When customRender is called, it should render the provided React element wrapped in the specified providers. The return value should be the same as @testing-library/react's render function (e.g., getByText, queryByText, rerender, unmount).

Edge Cases:

  • What happens if no custom providers are provided?
  • What happens if the user provides an empty array of custom providers?
  • How do you ensure that the order of providers is correct (e.g., a ThemeProvider often needs to be outside a Redux Provider)?

Examples

Example 1: Basic Usage with Default Providers

Assume your application has a default AppProviders component that includes ThemeProvider and ReduxProvider.

// AppProviders.tsx
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { ReduxProvider } from './ReduxProvider'; // Assume this exists

const defaultTheme = { colors: { primary: 'blue' } };

export const AppProviders: React.FC<{ children: React.ReactNode }> = ({ children }) => (
  <ReduxProvider>
    <ThemeProvider theme={defaultTheme}>
      {children}
    </ThemeProvider>
  </ReduxProvider>
);

// MyComponent.tsx
import React from 'react';
import styled from 'styled-components';

const StyledDiv = styled.div`
  color: ${(props) => props.theme.colors.primary};
`;

const MyComponent: React.FC = () => (
  <StyledDiv>Hello, World!</StyledDiv>
);

// MyComponent.test.tsx
import { customRender, screen } from './customRender'; // Assume customRender is exported from here

test('renders MyComponent with default theme', () => {
  customRender(<MyComponent />);
  const divElement = screen.getByText('Hello, World!');
  expect(divElement).toHaveStyle('color: blue');
});

Explanation:

customRender automatically wraps MyComponent with AppProviders. The ThemeProvider within AppProviders provides the theme, allowing MyComponent to access props.theme.colors.primary.

Example 2: Overriding Default Providers

Let's say for a specific test, you want to use a different theme.

// MyComponent.test.tsx (continued)
import { customRender, screen } from './customRender';
import { ThemeProvider } from 'styled-components';

const customTheme = { colors: { primary: 'red' } };

test('renders MyComponent with a custom theme', () => {
  customRender(<MyComponent />, {
    providers: [({ children }) => <ThemeProvider theme={customTheme}>{children}</ThemeProvider>],
  });
  const divElement = screen.getByText('Hello, World!');
  expect(divElement).toHaveStyle('color: red');
});

Explanation:

The providers array in the second argument of customRender replaces the default providers. The ThemeProvider with customTheme is now the outermost provider for this specific test, overriding the default theme.

Example 3: Adding Providers and Mocking Navigation

Imagine you're testing a component that uses react-router-dom.

// MyComponentWithRouter.tsx
import React from 'react';
import { Link, useLocation } from 'react-router-dom';

const MyComponentWithRouter: React.FC = () => (
  <>
    <h1>Navigation Test</h1>
    <Link to="/about">About</Link>
  </>
);

// MyComponentWithRouter.test.tsx
import { customRender, screen, MockNavigate } from './customRender';
import { MemoryRouter, Route, Routes } from 'react-router-dom';

test('navigates to about page', async () => {
  const mockNavigate = jest.fn();
  const { userEvent } = customRender(
    <MemoryRouter initialEntries={['/']}>
      <Routes>
        <Route path="/" element={<MyComponentWithRouter />} />
      </Routes>
    </MemoryRouter>,
    {
      providers: [
        ({ children }) => <MockNavigate navigate={mockNavigate}>{children}</MockNavigate>,
      ],
      // Assume MockNavigate is a simple component that captures navigate calls
    }
  );

  // You would need to implement MockNavigate to achieve this.
  // For this example, assume we have a way to interact with links.
  // await userEvent.click(screen.getByText('About'));
  // expect(mockNavigate).toHaveBeenCalledWith('/about');
});

Explanation:

Here, we provide a custom provider (MockNavigate) that intercepts navigation calls. We also use MemoryRouter to simulate routing without a full browser environment. The mockNavigate function can then be asserted against.

Constraints

  • The customRender function must be implemented in TypeScript.
  • The solution should aim for clarity and maintainability.
  • Avoid importing the actual Redux or Theme Providers if they are complex; mock them conceptually or use simple placeholders for the challenge.
  • The custom render function should return the same utilities as @testing-library/react's render.

Notes

  • Consider how to manage the merging of default and custom providers. A common pattern is to allow custom providers to completely replace defaults or to prepend/append to them. For this challenge, let's assume custom providers replace defaults for simplicity, but consider how you might implement merging.
  • Think about the order in which providers should be applied. For example, ThemeProvider often needs to be outside of a global state manager.
  • The challenge encourages you to think about common testing patterns and how to abstract away boilerplate.
  • You'll likely need to mock or simulate routing behavior for more complex scenarios.
Loading editor...
typescript