C3 AI Documentation Home

Authoring Tests Using React Testing Library

Overview

React testing library is a robust testing solution for writing React component tests. It promotes writing tests from the user's perspective rather than testing for implementation details.

This document will go over the basics of using React Testing Library.

Rendering components

When using react-testing-library, there is a single render function that is used to render components. For example for test_SDLCard:

JavaScript
import * as React from 'react';
import SDLCard from '@c3/sdl-react/reactComponents/cardList/SDLCard';
import { render } from '@testing-library/react';
import JasmineDOM from '@testing-library/jasmine-dom';
import ReactTesting from '@c3/ui/UiSdlReactTesting';

describe('test_SDLCard', function () {
  beforeAll(function () {
    jasmine.getEnv().addMatchers(JasmineDOM);
    this.testString = "test String";
    this.render = (props) => render(<SDLCard {...props} />);

    this.translationText = "Dummy translation"

    this.renderWithTranslations = (props) =>
      ReactTesting.renderWithIntl(<SDLCard {...props} />, {'uiSdlReact.translationKey': this.translationText});
  });
});

To use this, we just need to pass the React element in that we want to render.

Accessing rendered components and cleanup

Once the component is rendered, it is actually rendered on the screen. The screen is imported independently and can access all rendered HTMLElements. Since everything is rendered on the same screen object, it is helpful to cleanup the screen between renders. For this, it's helpful to add cleanup() to an afterEach like below: For example from test_SDLCard:

JavaScript

import * as React from 'react';
import SDLCard from '@c3/sdl-react/reactComponents/cardList/SDLCard';
import  { render, screen, cleanup } from '@testing-library/react';
import JasmineDOM from '@testing-library/jasmine-dom';
import ReactTesting from '@c3/ui/UiSdlReactTesting';

describe('test_SDLCard', function () {
  beforeAll(function () {
    jasmine.getEnv().addMatchers(JasmineDOM);
    this.testString = "test String";
    this.render = (props) => render(<SDLCard {...props} />);

    this.translationText = "Dummy translation"

    this.renderWithTranslations = (props) =>
      ReactTesting.renderWithIntl(<SDLCard {...props} />, {'uiSdlReact.translationKey': this.translationText});
  });

  afterEach(function () {
    cleanup();
  });

  describe('Rendering SDLCard', function () {
    describe('when a content text prop is passed', function () {
      it('renders a card with content text', function () {
        this.render({
          contentText: this.testString,
          showContentBodyText: true,
        });
        expect(screen.getByText(this.testString)).toBeVisible(true);
      });
    });
  });
});

For more advanced use cases, render does return a renderResult object, but it will not be needed in most cases. Also in this test, we are actually looking for the text. Since the text test String (this.testString) is what the user sees, that should be the first choice in testing this. That being said, it is not impossible to search for a classname when using React Testing Library, but it should only be a last resort behind using real end results. This can be achieved using document.querySelector to find the selector on the screen.

Adding Matchers

For adding matchers from @testing-library/jasmine-dom there is a small extra step. Just in the beforeAll, add jasmine.getEnv().addMatchers(JasmineDOM); like below:

JavaScript
import * as React from 'react';
import SDLCard from '@c3/sdl-react/reactComponents/cardList/SDLCard';
import { render, screen, cleanup } from '@testing-library/react';
import JasmineDOM from '@testing-library/jasmine-dom';
import ReactTesting from '@c3/ui/UiSdlReactTesting';

describe('test_SDLCard', function () {
  beforeAll(function () {
    jasmine.getEnv().addMatchers(JasmineDOM);
    this.testString = "test String";
    this.render = (props) => render(<SDLCard {...props} />);

    this.translationText = "Dummy translation"

    this.renderWithTranslations = (props) =>
      ReactTesting.renderWithIntl(<SDLCard {...props} />, {'uiSdlReact.translationKey': this.translationText});
  });
});

User Interactions

To help us better recreate real user interactions, we can use @testing-library/user-event.

This allows us to have more confidence with our interaction testing while passing in several options. For example, in test_UiSdlAreaChartReactRTL, we can see the extra delay added for the hover event as follows:

JavaScript
import * as React from 'react';
import { render, screen, cleanup } from '@testing-library/react';
import JasmineDOM from '@testing-library/jasmine-dom';
import ReactTesting from '@c3/ui/UiSdlReactTesting';
import userEvent from '@testing-library/user-event';
import UiSdlAreaChart from '@c3/ui/UiSdlAreaChartReact';

it('does render a popup if there is hoverDescription', async function () {
  // Semantic ui popup has a 50ms delay for showing up on hover
  const user = userEvent.setup({delay: 50});
  await user.hover(document.querySelector('.c3-card-title-subtitle-container'));
  expect(screen.getByText('hoverDescription')).toBeVisible();
});

This will give us more power to control testing interactions in unit tests rather than relying on more complicated end to end tests for small interactions.

There are, however, times that userEvent will not be able to provide all of the required functionality. For example, dragging and dropping is very complicated using userEvent, since it requires using the mouse pointer and specifying exact coordinates like the code block below:

JavaScript
it('on dragging the stage', async function () {
  const stage = document.querySelector('.stage canvas');
  await this.user.hover(stage);
  await this.user.pointer('[MouseLeft>]');
  await this.user.pointer({ coords: { x: 244, y: 257 } });
  await this.user.pointer('[/MouseLeft]');
});

This case first moves the mouse to the stage with hover, and then uses the userEvent pointer to mouse down, move, and then mouse up.

Alternatively, when you want to just ensure that dropping something over a drop point will work, you can use @testing-library/react's included fireEvent functionality to dispatch an exact event. This should only be done if using userEvent is not possible. For example, if you have a file upload and need to ensure that you can drag a file onto the upload, you can use fireEvent like so:

JavaScript
import { render, screen, cleanup, fireEvent, createEvent} from '@testing-library/react';

it('should let you upload via drop', function () {
  const dropEvent = createEvent.drop(this.renderResult.container.querySelector('.file-drag-and-drop-box'));
  Object.defineProperty(dropEvent, 'dataTransfer', {
    value: {
      files: [new File(['(⌐□_□)'], 'chucknorris.png', { type: 'image/png' })],
    },
  });
  fireEvent(screen.getByTestId('file-drag-and-drop-box'), dropEvent);
  expect(screen.getByText('chucknorris.png')).toBeVisible();
});

In this example, we are first creating the drop event we would like to use, and then firing that event directly to the file upload.

Extra functionality

We also provide UiSdlReactTesting for extra functionality when writing tests with React Testing Library. This allows us to create more complicated spies than jasmine alone as well as provides extra functionality when rendering components.

Was this page helpful?