react-transition-groupのTransitionを利用したコンポーネントをテストするとき
react-transition-groupの<Transition />
を利用して、アニメーションしながら表示・非表示の状態が切り替わる要素の表示状態をJest と Testing Library でテストしたとき、どのようにアニメーションを確実に完了させてからテストするかを確認したメモ。
状況としては、ボタンのクリックをトリガーにしてある要素の表示・非表示の切り替えをreact-transition-groupの<Transition />
を利用して行っているようなケース。 entering: { opacity: 0 }
のようにentering
の時点では不可視にしているとボタンクリック直後はアニメーションが完了していない状態になるので、そのままexpect(element).toBeVisible()
としてもテストに失敗する。
ここで言及しているのは例えば以下のようなコンポーネント。
import { useState } from "react";
import Transition from "react-transition-group/Transition";
const duration = 1000;
const defaultStyle = {
transition: `opacity ${duration}ms ease-in-out`,
opacity: 0,
};
const transitionStyles = {
entering: { opacity: 0 },
entered: { opacity: 1 },
exiting: { opacity: 0 },
exited: { opacity: 0 },
};
export const FadeInOut = () => {
const [inProp, setInProp] = useState(false);
return (
<div>
<Transition
in={inProp}
timeout={duration}
mountOnEnter={true}
unmountOnExit={true}
>
{(state) => (
<div
style={{
...defaultStyle,
...transitionStyles[state as keyof typeof transitionStyles],
}}
>
content
</div>
)}
</Transition>
<button type="button" onClick={() => setInProp((v) => !v)}>
toggle
</button>
</div>
);
};
ボタンをクリックしたときに表示・非表示の状態がTransition
でアニメーションして切り替わるように実装しているので、ただuserEvent.click(ボタン要素)
の直後にexpect(テスト対象の要素).toBeVisible()
としても可視状態になっていないのでテストは失敗する。
テスト時に時間の経過を待たないようにしたいということもあるけど、このようなケースで要素の表示状態をテストするにはどうするか。
選択肢としては、
- react-transition-group の
config.disabled
を利用する - 偽のタイマーを利用する
-
<Transition />
コンポーネント自体をモックする
などがありそう。
config.disabled
が利用できる
react-transition-group が v4.1.0 以上の場合、v4.1.0 以上であればconfig.disabled
でentering
やexisting
の途中の遷移を無効にしてテストできるようになっている。 http://reactcommunity.org/react-transition-group/testing/ を見る限りはテスト用途で使われることを想定してのもののよう。
import { act, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { config } from "react-transition-group";
import { FadeInOut } from "./FadeInOut";
describe("config.disabled true", () => {
beforeAll(() => {
config.disabled = true;
});
afterAll(() => {
config.disabled = false;
});
it("should fade in", async () => {
render(<FadeInOut />);
const button = await screen.findByText("toggle");
expect(button).toBeInTheDocument();
expect(screen.queryByText("content")).not.toBeInTheDocument();
userEvent.click(button);
expect(screen.queryByText("content")).toBeVisible();
});
});
これによってアニメーション途中のことは気にせずテストを書けるようになる。
jest.useFakeTimers
react-transition-group が v4.1.0 以上のバージョンではない状況でテストするケースもある。
Jest に限らずsetTimeout
をモックすることでテスト時にタイマーを進める形がある。
describe("jest fakeTimers", () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});
it("should fade in", async () => {
render(<FadeInOut />);
const button = await screen.findByText("toggle");
expect(button).toBeInTheDocument();
expect(screen.queryByText("content")).not.toBeInTheDocument();
userEvent.click(button);
act(() => {
jest.advanceTimersByTime(100);
});
await waitFor(() => {
expect(screen.queryByText("content")).toBeVisible();
});
});
});
beforeEach
でテスト前にjest.useFakeTimers()
を実行してタイマーを偽物に置き換えて、タイマーを進めたいときにjest.advanceTimersByTime(...)
で指定ミリ秒進めらる。
テスト後、待機中のタイマーを実行してから本物のタイマーに戻しておかないと予期しない問題が起こり得るので、それを避けるためafterEach
でjest.runOnlyPendingTimers()
を実行してからjest.useRealTimers()
を実行する。
It's important to also call runOnlyPendingTimers before switching to real timers. This will ensure you flush all the pending timers before you switch to real timers. If you don't progress the timers and just switch to real timers, the scheduled tasks won't get executed and you'll get an unexpected behavior. This is mostly important for 3rd parties that schedule tasks without you being aware of it.
Discussion