カスタムフックを使った機能拡張
オードアドバンス機能
Vitest の場合、vi.useFakeTimers を使うとsetTimeout や setInterval などのタイマーを生成する関数が制御可能な関数に置き換えられ、タイマーをモックすることが可能 ここでは、すべてのテストでモックされたタイマーを使いたいので、セットアップファイルで呼び出しておく
code:test-setup.ts
vi.useFakeTimers();
warning.icon 2024/09 現在、userEvent でユーザの操作をシミュレートしている箇所がすべて落ちる
Vitest がタイマーが置き換えられたことを認識できないことに起因する
回避策
vite.config.ts に shouldAdvanceTime というオプションを設定する
code:vite.config.ts
export default defineConfig({
// ...
test: {
globals: true,
environment: "happy-dom",
fakeTimers: { shouldAdvanceTime: true },
},
テスト
code:Carousel.test.tsx
describe("with auto-advance", () => {
it("advances the slide according to autoAdvancedInterval", () => {
const autoAdvanceInrterval = 5_000;
render(
<Carousel
slides={slides}
autoAdvanceInrterval={autoAdvanceInrterval}
/>,
);
const img = screen.getByRole("img");
expect(img).toHaveAttribute("src", slides0.imgUrl); act(() => {
vi.advanceTimersByTime(autoAdvanceInrterval);
});
expect(img).toHaveAttribute("src", slides1.imgUrl); act(() => {
vi.advanceTimersByTime(autoAdvanceInrterval);
});
expect(img).toHaveAttribute("src", slides2.imgUrl); });
});
実装
タイマーを生成するカスタムフック
code:useTimeout.tsx
import { useEffect } from "react";
export const useTimeout = (delay: number | undefined, callback: () => void) => {
useEffect(() => {
if (!delay) return;
const timeout = setTimeout(callback, delay);
return () => clearTimeout(timeout);
};
useEffect で return すると、再レンダリングやアンマウントされるときに呼び出される スライドの状態値を管理するカスタムフック
code:useSlideIndex.tsx
export const useSlideIndex = (
slides?: unknown[],
slideIndexProp?: number,
onSlideIndexChange?: (newSlideIndex: number) => void,
autoAdvanceInterval?: number,
) => {
// ...
useTimeout(autoAdvanceInterval, incrementSlideIndex);
};
スライドが変更されるたびに incrementSlideIndex 関数は再生成される
これにより、スライドが変更されるたびに新しい タイマー がセットされる コンポーネント
code:Carousel.tsx
export type CarouselProps = {
slides?: Slide[];
slideIndex?: number;
onSlideIndexChange?: (newSlideIndex: number) => void;
autoAdvanceInterval?: number;
};
const Carousel = ({
slides,
slideIndex: slideIndexProp,
onSlideIndexChange,
autoAdvanceInterval,
defaultImgHeight,
DefaultImgComponent,
}: CarouselProps) => {
slides,
slideIndexProp,
onSlideIndexChange,
autoAdvanceInterval,
);
return (
<div data-testid="carousel">
<CarouselSlide
imgHeight={defaultImgHeight}
ImgComponent={DefaultImgComponent}
/>
<CarouselButton data-testid="prev-button" onClick={decrementSlideIndex}>
Prev
</CarouselButton>
<CarouselButton data-testid="next-button" onClick={incrementSlideIndex}>
Next
</CarouselButton>
</div>
);
};