概要
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 でも別途サーバー起動が不要になる