背景
ファイルを選択する機能を含むコンポーネントをテストする際に、テスト用の File オブジェクトを取得する必要がある。
テストを行う際、従来は Node.js 上で Jest を起動してテストすることが多かったが、最近は storybook が推奨する playwright などブラウザのランタイム上でテストを実行することも増えてきたのではないかと思う。
ランタイムが Node.js の時と playwright のようなブラウザの時とでファイルを取得するテクニックが異なるので、それぞれでうまくいった方法を共有したい。
環境
- node: v16.18.1
- next: v13.2.1
- react: v18.2.0
- jest: v29.2.1
- @storybook/react: v6.5.16
- @testing-library/react: v13.4.0
- @testing-library/user-event: v13.5.0
テスト対象のコンポーネント
下記のような、選択したファイルを Data URL として読んでプレビュー画像として表示するコンポーネントのテストを考える。
import Image from "next/image";
import { useState } from "react";
const TestPage: React.FC = () => {
const [fileData, setFileData] = useState<string | null>(null);
const onSelectFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.currentTarget.files?.[0];
if (file) {
const reader = new FileReader();
reader.onload = () => {
setFileData(reader.result as string);
};
reader.readAsDataURL(file);
} else {
setFileData(null);
}
};
return (
<div>
<input
id="file"
name="file"
type="file"
aria-label="file-select"
onChange={onSelectFile}
/>
{fileData && (
<div>
<Image
src={fileData}
width={515}
height={645}
alt="avatar"
aria-label="preview"
/>
</div>
)}
</div>
);
};
export default TestPage;
ファイル選択後の storybook での見た目はこんな感じ。

storybook の場合
storybook の場合はブラウザの fetch API を利用してファイルを blob 形式で取得することができるので、それを利用する。
まず public/ ディレクトリ内に該当の画像ファイルを配置しておく。
├── public
│ └── dummy.png
├── src
│ ├── pages
│ │ ├── _app.tsx
│ │ ├── _document.tsx
│ │ ├── index.stories.tsx
│ │ ├── index.test.tsx
│ │ └── index.tsx
│ └── styles
│ ├── Home.module.css
│ └── globals.css
└── tsconfig.json
次に storybook をビルドする際のコマンドに、 public ディレクトリ内のファイルをサーブするオプションを追加する。
# 開発時
$ start-storybook -s public
# デプロイ時
$ build-storybook -s public
そして下記のように story を実装する。
import { ComponentMeta, ComponentStoryObj } from "@storybook/react";
import { within } from "@storybook/testing-library";
import userEvent from "@testing-library/user-event";
import TestPage from "./index";
type Story = ComponentStoryObj<typeof TestPage>;
export default {
component: TestPage,
} as ComponentMeta<typeof TestPage>;
export const Default: Story = {
async play({ canvasElement }) {
const { getByLabelText } = within(canvasElement);
// public/ 内の画像ファイルを Blob 形式で fetch API 経由で取得する
const response = await fetch("/dummy.png");
const blob = await response.blob();
const file = new File([blob], "image.png");
userEvent.upload(getByLabelText("file-select"), file);
},
};
fetch API を用いて取得した File オブジェクトを userEvent.upload に渡せば、無事にファイル選択をシミュレートできる。
jest の場合
先ほど public/ 内に配置したファイルを fs 経由で取得する。
import fs from "fs";
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import TestPage from "./index";
describe("when a file is selected", () => {
it("shows preview image", async () => {
const { findByRole, getByLabelText } = render(<TestPage />);
const buffer = fs.readFileSync("public/dummy.png");
const file = new File([buffer], "dummy.png");
userEvent.upload(getByLabelText("file-select"), file);
expect(await findByRole("img", { name: "preview" })).toBeInTheDocument();
});
});
実行してみると無事に下記の通り PASS した。
$ npm run test -- pages/index.test.tsx
PASS pages/index.test.tsx
when a file is selected
✓ shows preview image (201 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.638 s, estimated 1 s
Ran all test suites matching /src\/pages\/index.test.tsx/i.