😺

rangeやsliderのテストの書き方

2024/03/31に公開

はじめに

sliderをテストしようとなった時に困ったのでまとめておきます。

sliderとは何かというと以下のgifのようなUIパーツのことです。

ライブラリによってテストの書き方が違ったため、すべてやってたらきりがないので今回は3つのライブラリのsliderのテスト方法をまとめました。

Material UI

みんな大好きMaterial UIです。

MaterialUIのsliderはtouchイベントに反応するように作られてるため、touchイベントでのドラッグを実装します。

またjsdomでは実装されない関数をdrag対象の要素にモックします。

import Slider from "@mui/material/Slider";
import { fireEvent, render, screen } from "@testing-library/react";

describe("Mui Slider", () => {
  it("should render correctly", async () => {
    const handleChange = jest.fn();
    render(
      <Slider
        onChange={handleChange}
        min={0}
        max={1}
        step={0.1}
        value={0}
      />
    );

    const element = screen.getByRole("slider");
    Object.defineProperties(element, {
	    hasPointerCapture: {
	      value: jest.fn().mockReturnValue(true),
	    },
	    setPointerCapture: {
	      value: jest.fn(),
	    },
	    releasePointerCapture: {
	      value: jest.fn(),
	    },
	  });
    
    const current = {
      clientX: 0,
      clientY: 0,
    };
    const step = {
      x: 100,
      y: 0,
    };
    const duration = 500, steps = 20;
    fireEvent.touchStart(element, current);
    for (let i = 0; i < steps; i++) {
      current.clientX += step.x;
      current.clientY += step.y;
      await sleep(duration / steps);
      fireEvent.touchMove(element, current);
    }
    fireEvent.touchEnd(element, current);
    expect(handleChange).toHaveBeenCalled();
  });
});

const sleep = (ms: number) =>
  new Promise((resolve) => {
    setTimeout(resolve, ms);
  });

Radix UI(shadcn/ui)

今度はpointerイベントに反応するように作られてるためpointerイベントで実装します。

また、トップレベルの要素の大きさから値の変更量が計算されるため、要素の大きさをモックします。

import { Slider } from "@/components/ui/slider";
import { fireEvent, render, screen } from "@testing-library/react";

describe("shadcn/ui Slider", () => {
  it("should render correctly", async () => {
    const handleChange = jest.fn();
    render(
      <Slider
        aria-label="slider"
        onValueChange={handleChange}
        min={0}
        max={1}
        step={0.1}
        value={0}
      />
    );
    const target = screen.getByLabelText("slider");
    Object.defineProperty(target, "getBoundingClientRect", {
      value: jest
        .fn()
        .mockReturnValue({ left: 0, top: 0, width: 100, height: 10 }),
    });
    
    const element = screen.getByRole("slider");
    const current = {
      clientX: 0,
      clientY: 0,
    };
    const step = {
      x: 100,
      y: 0,
    };
    const duration = 500, steps = 20;
    fireEvent.pointerDown(element, current);
    for (let i = 0; i < steps; i++) {
      current.clientX += step.x;
      current.clientY += step.y;
      await sleep(duration / steps);
      fireEvent.pointerMove(element, current);
    }
    fireEvent.pointerMove(element, current);
    expect(handleChange).toHaveBeenCalled();
  });
});

const sleep = (ms: number) =>
  new Promise((resolve) => {
    setTimeout(resolve, ms);
  });

Ant Design

antdのsliderはmouseイベントを対象に実装されているのですが、clientX,YじゃなくてpageX,Yをもとに計算されてます。

しかし、testing-libraryではpageX,Yを設定できないので、イベントを作ってから無理やりプロパティを生やします。

加えてmousemove, mouseupイベントがdocumentに対して設定されるため、それらはdocumentに対して発火します。

またshadcn/ui同様トップレベルの要素の大きさを設定します。

import { Slider } from "antd";
import { fireEvent, render, screen } from "@testing-library/react";

describe("shadcn/ui Slider", () => {
  it("should render correctly", async () => {
    const handleChange = jest.fn();
    const { container } = render(
      <Slider
        onChange={handleChange}
        min={0}
        max={1}
        step={0.1}
        value={0}
      />,
    );
    const target = container.querySelector(".ant-slider");
    Object.defineProperty(target, "getBoundingClientRect", {
      value: jest
        .fn()
        .mockReturnValue({ left: 0, top: 0, width: 100, height: 10 }),
    });
    
    const element = screen.getByRole("slider");
    const current = {
      clientX: 0,
      clientY: 0,
    };
    const step = {
      x: 100,
      y: 0,
    };
    const duration = 500, steps = 20;
    await act(async () => {
		  const mousedown = new MouseEvent("mousedown", {
		    bubbles: true,
		    cancelable: true,
		  });
		  Object.assign(mousedown, current);
		  fireEvent(elm, mousedown);
		  
		  const mousemove = new MouseEvent("mousemove", {
		    bubbles: true,
		    cancelable: true,
		  });
		  for (let i = 0; i < steps; i++) {
		    current.pageX += step.x;
		    current.pageY += step.y;
		    await sleep(duration / steps);
		    Object.assign(mousemove, current);
		    fireEvent(elm, mousemove);
		  }
		  
		  fireEvent.mouseUp(document);
		});
    expect(handleChange).toHaveBeenCalled();
  });
});

const sleep = (ms: number) =>
  new Promise((resolve) => {
    setTimeout(resolve, ms);
  });

まとめ

全部実装を読みに行ってどんな処理してるのかを確認したので、なかなか大変でした。
他のUIライブラリもfireEvent.dragが効かない場合は実装を読まなきゃいけないと思います。

Discussion