Testing a Material UI slider with @testing-library/react
Asked Answered
Y

6

22

I'm trying to test a Slider component created with Material-UI, but I cannot get my tests to pass.

I would like test the the value changes using the fireEvent with @testing-library/react. I've been following this post to properly query the DOM, I cannot get the correct DOM nodes.

<Slider /> component

// @format
// @flow

import * as React from "react";
import styled from "styled-components";
import { Slider as MaterialUISlider } from "@material-ui/core";
import { withStyles, makeStyles } from "@material-ui/core/styles";
import { priceRange } from "../../../domain/Search/PriceRange/priceRange";

const Wrapper = styled.div`
  width: 93%;
  display: inline-block;
  margin-left: 0.5em;
  margin-right: 0.5em;
  margin-bottom: 0.5em;
`;

// ommited code pertaining props and styles for simplicity

function Slider(props: SliderProps) {
  const initialState = [1, 100];
  const [value, setValue] = React.useState(initialState);

  function onHandleChangeCommitted(e, latestValue) {
    e.preventDefault();
    const { onUpdate } = props;
    const newPriceRange = priceRange(latestValue);
    onUpdate(newPriceRange);
  }

  function onHandleChange(e, newValue) {
    e.preventDefault();
    setValue(newValue);
  }

  return (
    <Wrapper
      aria-label="range-slider"
    >
      <SliderWithStyles
        aria-labelledby="range-slider"
        defaultValue={initialState}
        // getAriaLabel={index =>
        //   index === 0 ? "Minimum Price" : "Maximum Price"
        // }
        getAriaValueText={valueText}
        onChange={onHandleChange}
        onChangeCommitted={onHandleChangeCommitted}
        valueLabelDisplay="auto"
        value={value}
      />
    </Wrapper>
  );
}

export default Slider;

Slider.test.js

// @flow

import React from "react";
import { cleanup,
  render,
  getAllByAltText,
  fireEvent,
  waitForElement } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

import Slider from "../Slider";


afterEach(cleanup);

describe("<Slider /> specs", () => {

  // [NOTE]: Works, but maybe a better way to do it ?
  xdescribe("<Slider /> component aria-label", () => {

    it("renders without crashing", () => {
      const { container } = render(<Slider />);
      expect(container.firstChild).toBeInTheDocument(); 
    });
  });

  // [ASK]: How to test the event handlers with fireEvent.
  describe("<Slider /> props", () => {

    it("display a initial min value of '1'", () => {
      const renderResult = render(<Slider />);
      // TODO
    });

    it("display a initial max value of '100'", () => {
      const renderResult = render(<Slider />);
      // TODO
    });

    xit("display to values via the onHandleChangeCommitted event when dragging stop", () => {
      const renderResult = render(<Slider />);
      console.log(renderResult)
      // fireEvent.change(renderResult.getByText("1"))
      // expect(onChange).toHaveBeenCalled(0);
    });

    // [NOTE]: Does not work, returns undefined
    xit("display to values via the onHandleChange event when dragging stop", () => {
      const renderResult = render(<Slider />);

      console.log(renderResult.container);
      
      const spanNodeWithAriaAttribute = renderResult.container.firstChild.getElementsByTagName("span")[0].getAttribute('aria-label')
      expect(spanNodeWithAriaAttribute).toBe(/range-slider/)
    });
  });

  // [ASK]: Works, but a snapshot is an overkill a better way of doing this ?
  xdescribe("<Slider /> snapshot", () => {

    it("renders without crashing", () => {
      const { container } = render(<Slider />);
      expect(container.firstChild).toMatchSnapshot();
    });
  });
});
Yokoyama answered 14/11, 2019 at 11:47 Comment(0)
C
10

I would recommend not to write tests for a custom component and believe that this component works for all our cases.

Read through this article for more details. In that they have mentioned how to write unit tests for a component that wraps react-select.

I followed the similar approach and wrote a mock for my third-party slider component.

in setupTests.js:

jest.mock('@material-ui/core/Slider', () => (props) => {
  const { id, name, min, max, onChange, testid } = props;
  return (
    <input
      data-testid={testid}
      type="range"
      id={id}
      name={name}
      min={min}
      max={max}
      onChange={(event) => onChange(event.target.value)}
    />
  );
});

With this mock, you can simply fire a change event in your tests like this:

fireEvent.change(getByTestId(`slider`), { target: { value: 25 } });

Make sure to pass proper testid as a prop to your SliderWithStyles component

Curvaceous answered 6/5, 2020 at 6:32 Comment(2)
fireEvent.change(getByTestId('slider'), { target: { value: 25 } }); Just this change. Thanks for this idea bdw. Much cleaner than above solutionsOpening
While this would be a valid unit test, I don't think it adds as much value as Orville's solution. If material ui is ever removed, the api changes, or this component has to be rewritten custom, all the tests would have to be rewritten. There would also be less certainty that it behaves as it use to. If the range dom node is tested directly, it more closely resembles how the user interacts with the component, and the test should not have to change when the component is rewritten.Carlile
T
8

After battling this for hours I was able to solve my case related to testing MUI slider

It really depends on how you need to test yours, in my case I have to check if a label text content has changed after clicking a mark using marks slider props.

The problems

1) The slider component computes the return value base on elements getBoundingClientRect and MouseEvent

2) How to query the slider and fire the event.

3) JSDOM limitation on reading element actual height and width which causes the problem no.1

The solution

1) mock getBoundingClientRect should also fix the problem no.3

2) add test id to slider and use use fireEvent.mouseDown(contaner, {....})

const sliderLabel = screen.getByText("Default text that the user should see")

// add data-testid to slider
const sliderInput = screen.getByTestId("slider")

// mock the getBoundingClientRect
    sliderInput.getBoundingClientRect = jest.fn(() => {
      return {
        bottom: 286.22918701171875,
        height: 28,
        left: 19.572917938232422,
        right: 583.0937919616699,
        top: 258.22918701171875,
        width: 563.5208740234375,
        x: 19.572917938232422,
        y: 258.22918701171875,
      }
    })

    expect(sliderInput).toBeInTheDocument()

    expect(sliderLabel).toHaveTextContent("Default text that the user should see")
    await fireEvent.mouseDown(sliderInput, { clientX: 162, clientY: 302 })
    expect(sliderLabel).toHaveTextContent(
      "New text that the user should see"
    )

Tymes answered 7/6, 2020 at 3:53 Comment(0)
L
3

I turned the solution mentioned above into simple (Typescript) helper

export class Slider {
  private static height = 10

  // For simplicity pretend that slider's width is 100
  private static width = 100

  private static getBoundingClientRectMock() {
    return {
      bottom: Slider.height,
      height: Slider.height,
      left: 0,
      right: Slider.width,
      top: 0,
      width: Slider.width,
      x: 0,
      y: 0,
    } as DOMRect
  }

  static change(element: HTMLElement, value: number, min: number = 0, max: number = 100) {
    const getBoundingClientRect = element.getBoundingClientRect
    element.getBoundingClientRect = Slider.getBoundingClientRectMock
    fireEvent.mouseDown(
        element,
        {
            clientX: ((value - min) / (max - min)) * Slider.width,
            clientY: Slider.height
        }
    )
    element.getBoundingClientRect = getBoundingClientRect
  }
}

Usage:

Slider.change(getByTestId('mySlider'), 40) // When min=0, max=100 (default)
// Otherwise
Slider.change(getByTestId('mySlider'), 4, 0, 5) // Sets 4 with scale set to 0-5
Lanthanum answered 9/7, 2020 at 17:16 Comment(0)
P
1

Building on @rehman_00001's answer, I created a file mock for the component. I wrote it in TypeScript, but it should work just as well without the types.

__mocks__/@material-ui/core/Slider.tsx

import { SliderTypeMap } from '@material-ui/core';
import React from 'react';

export default function Slider(props: SliderTypeMap['props']): JSX.Element {
    const { onChange, ...others } = props;
    return (
        <input
            type="range"
            onChange={(event) => {
                onChange && onChange(event, parseInt(event.target.value));
            }}
            {...(others as any)}
        />
    );
}

Now every usage of the Material UI <Slider/> component will be rendered as a simple HTML <input/> element during testing which is much easier to work with using Jest and react-testing-library.

{...(others as any)} is a hack that allows me to avoid worrying about ensuring that every possible prop of the original component is handled properly. Depending on what Slider props you rely on, you may need to pull out additional props during destructuring so that you can properly translate them into something that makes sense on a vanilla <input/> element. See this page in the Material UI docs for info on each possible property.

Paola answered 25/6, 2021 at 16:40 Comment(0)
G
1

The ui slider material element has an input, by modifying that input you simulate the value that the user would choose by clicking:

const slider = await container.querySelector('input[type="range"]');
fireEvent.change(slider , { target: { value: 15000} });

Ref: https://github.com/testing-library/user-event/issues/871#issuecomment-1059317998

Granophyre answered 21/6, 2023 at 12:34 Comment(0)
G
0

Here is an easy and flexible approach:

  1. Import Mui like this:
import * as MuiModule from '@mui/material';
  1. Mock the @mui/material library and specify a function for Slider
jest.mock('@mui/material', () => ({
  __esModule: true,
  ...jest.requireActual('@mui/material'),
  Slider: () => {}, // Important: resolves issues where Jest sees Slider as an object instead of a function and allows jest.spy to work
}));
  1. Spy on Slider and write your own implementation
describe('Given a MyCustomSlider component', () => {
  beforeEach(() => {
    jest.spyOn(MuiModule, 'Slider').mockImplementation((props) => {
      const { value, onChange, onChangeCommitted } = props;
      const simulateChange = (event) => {
        if (!onChange) return;
        onChange(event, [0, 10], 0);
      };
      const simulateChangeCommitted = (event) => {
        if (!onChangeCommitted) return;
        onChangeCommitted(event, [0, 10]);
      };
      return (
        <>
          Value: {value}
          <button onClick={simulateChange}>Trigger Change</button>
          <button onClick={simulateChangeCommitted}>
            Trigger Change Committed
          </button>
        </>
      );
    });
  });

  // ... tests here
});
  1. Render your component and trigger the change event:
describe('When it is rendered', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    render(<MyCustomSlider />);
  });

  describe('When a change event occurs', () => {
    beforeEach(async () => {
      jest.clearAllMocks();
      await userEvent.click(screen.getByText('Trigger Change'));
    });

    test('Then ... (your assertion here)', () => {
      // your test
    });
  });
});

Full example:

import userEvent from '@testing-library/user-event';
import * as MuiModule from '@mui/material';
import { render } from '../../../libs/test-utils';

jest.mock('@mui/material', () => ({
  __esModule: true,
  ...jest.requireActual('@mui/material'),
  Slider: () => {},
}));

describe('Given a MyCustomSlider component', () => {
  beforeEach(() => {
    jest.spyOn(MuiModule, 'Slider').mockImplementation((props) => {
      const { value, onChange, onChangeCommitted } = props;
      const simulateChange = (event) => {
        if (!onChange) return;
        onChange(event, [0, 10], 0);
      };
      const simulateChangeCommitted = (event) => {
        if (!onChangeCommitted) return;
        onChangeCommitted(event, [0, 10]);
      };
      return (
        <>
          Value: {value}
          <button onClick={simulateChange}>Trigger Change</button>
          <button onClick={simulateChangeCommitted}>
            Trigger Change Committed
          </button>
        </>
      );
    });
  });

  describe('When it is rendered', () => {
    beforeEach(() => {
      jest.clearAllMocks();
      render(<MyCustomSlider />);
    });

    describe('When a change event occurs', () => {
      beforeEach(async () => {
        jest.clearAllMocks();
        await userEvent.click(screen.getByText('Trigger Change'));
      });

      test('Then ... (your assertion here)', () => {
        // your test
      });
    });
  });
});

Garboard answered 2/12, 2022 at 16:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.