LoginSignup
2
0

Storybookでinput[type="file"]要素にファイルを添付する

Last updated at Posted at 2023-12-14

はじめに

ファイルアップロード機能の開発中に、Storybook上でtype="file"属性を持つinput要素のテストを行う必要が生じましたが、input要素の取得に手間取ったため実装方法を共有します。

この記事は「AlphaDrive Advent Calendar 2023」の15日目のエントリーです。

開発環境

  • node: v16.18.1
  • react: v18.2.0
  • next: v14.0.3
  • storybook: v7.6.4
  • typescript: v5.2.2
  • @storybook/react: v7.6.4
  • @storybook/jest: v0.2.3
  • @storybook/testing-library: v0.2.2

簡略化したUI

  • ファイル選択前(ファイル選択ボタンのみ表示する)
  • ファイル選択後(ファイル名と選択取消ボタンを表示する)

ファイル選択コンポーネントの作成

ファイル選択機能を持つコンポーネントを以下のとおり実装しました。

SampleForm.tsx
import React, { useState, useRef, ChangeEvent } from "react";

export const SampleForm: React.FC = () => {
  const [file, setFile] = useState<File | null>(null);
  const fileInputRef = useRef<HTMLInputElement>(null);

  const onChange = (event: ChangeEvent<HTMLInputElement>) => {
    const selectedFile = event.target.files?.[0];
    if (!selectedFile) {
      return;
    }
    setFile(selectedFile);
  };

  const handleClick = () => {
    if (!fileInputRef.current) {
      return;
    }
    fileInputRef.current.click();
  };

  const handleCancel = () => {
    setFile(null);
    if (fileInputRef.current) {
      fileInputRef.current.value = "";
    }
  };

  return (
    <div>
      <input
        type="file"
        ref={fileInputRef}
        hidden
        onChange={onChange}
      />

      {!file && <button onClick={handleClick}>ファイルを選択</button>}

      {file && (
        <div style={{ display: "flex", alignItems: "center" }}>
          <p style={{ marginRight: "10px" }}>{file.name}</p>
          <button onClick={handleCancel}>選択キャンセル</button>
        </div>
      )}
    </div>
  );
};

Storybookでのファイル選択機能のテスト実装

悩んだポイント

今回のケースでは、input[type="file"]には特定のアクセシビリティロールが存在しないため、Testing LibraryのByRoleクエリを使用できず、また、ラベルやプレイスホルダーからの取得もできなかったため実装方法に悩みました。

実装例

SampleForm.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { expect } from "@storybook/jest";
import { userEvent,within } from "@storybook/testing-library";

import { SampleForm } from "./SampleForm";

const meta: Meta<typeof SampleForm> = {
  component: SampleForm,
};

export default meta;
type Story = StoryObj<typeof SampleForm>;

export const selectFile: Story = {
  async play({ canvasElement }) {
    const { findByText, findByTestId } = within(canvasElement);

    // ここで 解決策① or 解決策② の方法でinput要素を取得する必要があります!

    expect(inputElement).toBeInTheDocument();

    // public配下に置いたテスト用のファイルをfetch
    const res = await fetch("/sample.csv");
    const blob = await res.blob();

    // 新しくFileオブジェクトのインスタンスを作成
    const file = new File([blob], "test.csv", {
      type: "text/csv",
    });

    userEvent.upload(inputElement, file);
    expect(await findByText("test.csv")).toBeInTheDocument();
  },
};

解決策① Testing LibraryのByTestIdを使うパターン

input要素にdata-testid属性を付けると、Testing LibraryのByTestIdクエリによって要素を取得できます。

SampleForm.tsx
<input
  type="file"
  data-testid="hidden-input" // 追加
  ref={fileInputRef}
  hidden
  onChange={onChange}
/>    
SampleForm.stories.tsx
const inputElement = await findByTestId("hidden-input");

解決策② querySelectorを使うパターン

querySelectorを使用して、CSSセレクタに基づきtype属性がfileであるinput要素を取得する方法もあります。

SampleForm.stories.tsx
const inputElement = document.querySelector<HTMLInputElement>("input[type=file]");

参考資料

フロントエンドテストの考え方を分かりやすくまとめてくださっているので、何度も拝読しています。

まとめ

今回のテスト実装を通して、アクセシビリティロールやTestingLibraryの各クエリについて理解を深めることができました。上の参考資料にあるとおり、ByTestIdを多用せず、なるべくアクセシブルなクエリを使ったテストを優先できるよう引き続き考えていきたいと思います。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0