レビュー待ち·難易度: 中級·更新: 2026-04-18

Playwright でファイルアップロードの E2E テストを書く

Playwright の setInputFiles を使って file input へのファイル選択をシミュレートし、アップロードの成功・エラー・複数ファイルケースを E2E テストする例。

nextjstestingfile-uploadplaywright

対応バージョン

nextjs 15react 19playwright 1

前提環境

Playwright の基本テスト構成(test / expect / locator)を理解していること

概要

page.setInputFiles()<input type="file"> にファイルをセットし、アップロードフローを E2E テストする。実際のファイルを使った成功ケース・バリデーションエラーケース・複数ファイルケースをカバーする。

インストール

npm install --save-dev @playwright/test
npx playwright install

実装

テスト対象のアップロード UI

// app/upload/page.tsx
"use client";

import { useState } from "react";

export default function UploadPage() {
  const [message, setMessage] = useState<string | null>(null);
  const [error, setError] = useState<string | null>(null);

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setMessage(null);
    setError(null);

    const formData = new FormData(e.currentTarget);
    const res = await fetch("/api/upload", { method: "POST", body: formData });

    if (res.ok) {
      setMessage("アップロード成功");
    } else {
      const data = await res.json() as { error: string };
      setError(data.error);
    }
  }

  return (
    <main className="mx-auto max-w-md p-8">
      <h1 className="mb-6 text-2xl font-bold">ファイルアップロード</h1>
      <form onSubmit={handleSubmit} className="space-y-4">
        <input
          name="file"
          type="file"
          accept="image/*"
          aria-label="ファイルを選択"
          className="block w-full text-sm text-gray-600"
        />
        <button
          type="submit"
          className="rounded bg-blue-600 px-4 py-2 text-sm text-white hover:bg-blue-700"
        >
          アップロード
        </button>
      </form>

      {message && (
        <p role="status" className="mt-4 text-sm text-green-600">{message}</p>
      )}
      {error && (
        <p role="alert" className="mt-4 text-sm text-red-500">{error}</p>
      )}
    </main>
  );
}

アップロード Route Handler

// app/api/upload/route.ts
import { NextResponse } from "next/server";

const MAX_SIZE = 5 * 1024 * 1024; // 5MB

export async function POST(request: Request) {
  const formData = await request.formData();
  const file = formData.get("file");

  if (!file || typeof file === "string") {
    return NextResponse.json({ error: "ファイルが必要です" }, { status: 400 });
  }

  if (file.size > MAX_SIZE) {
    return NextResponse.json(
      { error: "5MB 以下のファイルを選択してください" },
      { status: 400 }
    );
  }

  if (!file.type.startsWith("image/")) {
    return NextResponse.json(
      { error: "画像ファイルのみアップロードできます" },
      { status: 400 }
    );
  }

  return NextResponse.json({ message: "アップロード成功" });
}

Playwright テスト

// e2e/upload.spec.ts
import { test, expect } from "@playwright/test";
import path from "path";

// テスト用のフィクスチャファイルのパス
const FIXTURES_DIR = path.join(__dirname, "fixtures");

test.describe("ファイルアップロード", () => {
  test.beforeEach(async ({ page }) => {
    await page.goto("/upload");
  });

  test("画像ファイルをアップロードすると成功メッセージが表示される", async ({ page }) => {
    // file input にファイルをセット
    await page.getByLabel("ファイルを選択").setInputFiles(
      path.join(FIXTURES_DIR, "sample.png")
    );

    await page.getByRole("button", { name: "アップロード" }).click();

    // 成功メッセージが表示されることを確認
    await expect(page.getByRole("status")).toHaveText("アップロード成功");
  });

  test("ファイル未選択でアップロードするとエラーになる", async ({ page }) => {
    await page.getByRole("button", { name: "アップロード" }).click();

    await expect(page.getByRole("alert")).toHaveText("ファイルが必要です");
  });

  test("5MB を超えるファイルはエラーになる", async ({ page }) => {
    // バッファで大きなファイルを生成(実ファイル不要)
    await page.getByLabel("ファイルを選択").setInputFiles({
      name: "large.png",
      mimeType: "image/png",
      buffer: Buffer.alloc(6 * 1024 * 1024), // 6MB
    });

    await page.getByRole("button", { name: "アップロード" }).click();

    await expect(page.getByRole("alert")).toHaveText(
      "5MB 以下のファイルを選択してください"
    );
  });

  test("画像以外のファイルはエラーになる", async ({ page }) => {
    await page.getByLabel("ファイルを選択").setInputFiles({
      name: "document.pdf",
      mimeType: "application/pdf",
      buffer: Buffer.from("PDF content"),
    });

    await page.getByRole("button", { name: "アップロード" }).click();

    await expect(page.getByRole("alert")).toHaveText(
      "画像ファイルのみアップロードできます"
    );
  });
});

テストフィクスチャの準備

// e2e/fixtures/create-fixtures.ts(初回セットアップ用)
import { writeFileSync, mkdirSync } from "fs";
import path from "path";

// PNG の最小バイナリ(1x1 透明 PNG)
const MINIMAL_PNG = Buffer.from(
  "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000a49444154789c6260000000000200006e0054e70000000049454e44ae426082",
  "hex"
);

mkdirSync(path.join(__dirname), { recursive: true });
writeFileSync(path.join(__dirname, "sample.png"), MINIMAL_PNG);

playwright.config.ts

// playwright.config.ts
import { defineConfig } from "@playwright/test";

export default defineConfig({
  testDir: "./e2e",
  use: {
    baseURL: "http://localhost:3000",
  },
  webServer: {
    command: "npm run dev",
    url: "http://localhost:3000",
    reuseExistingServer: !process.env.CI,
  },
});

ポイント

  • locator.setInputFiles(filePath) で file input にファイルをセットする。実際のクリック操作なしにファイル選択を再現できる
  • 実ファイルを使いたくない場合は { name, mimeType, buffer } 形式でバッファを直接渡せる。大容量ファイルのテストや特定 MIME type のテストに便利
  • ファイル選択後は通常のクリック・待機操作で送信・結果確認を行う。getByRole("alert") / getByRole("status") でアクセシブルな要素を取得する
  • jest-component-test との使い分け: Jest は jsdom で DOM を模倣するためファイル API の実装が制限される。Playwright は実ブラウザで動作するため File / Blob / FormData の挙動を正確に検証できる
  • webServer 設定で npm run dev を自動起動できるので、CI でも別途サーバー起動が不要になる

注意点

jest-component-test / jest-route-handler-test は jsdom 上の単体テスト。storybook-interaction-test は Story 内の UI 操作テスト。これはブラウザを実際に起動し setInputFiles でファイル選択操作を行う Playwright E2E テストに特化。Jest 系とはテストレイヤーが異なる。

関連サンプル