😺
rangeやsliderのテストの書き方
はじめに
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