概要
今回はPlaywrightでdesktop / tablet / mobileの3つのビューポートを一発でテストして、各テストの操作フローをスクリーンショット連番として自動収集する方法と構成について紹介していきます。
個人開発でレスポンシブ対応をすると、「PCでは問題ないがスマホだけレイアウトが崩れる」というバグが定期的に発生します。
毎回手で確認するのは、さすがに今どきでは無く現実的でないので、E2Eテストで3画面サイズを自動で回し、操作フローのエビデンスをまとめて取得する仕組みを入れています。
自分が作ったcopype.shinpinoshi.comこぴぺったりでは、6 spec × 3ビューポート = テスト1回で189枚のスクリーンショットが取れる構成にしています。
個人開発の方や業務の自動化をガンガン回したい方は是非見ていってください!
それではやっていきましょう(^^!
目次
なぜ3ビューポートで回したいのか
レスポンシブWebアプリでは、「desktop」「tablet」「mobile」で異なるレイアウト・操作系を持つことが普通です。
- desktop: サイドバー常時表示・マウスホバー操作前提
- tablet: 中間幅でレイアウト崩れが起きやすい
- mobile: ドロワーUI・タップ操作・SafeArea
E2Eを1ビューポートだけで回しても、tablet/mobile固有のレイアウトバグが見逃されるので、テストとしてはナンセンスだと思ってます。
しかも3回コマンド叩くのも面倒…
なので「同じspecを3ビューポートそれぞれで回す」設定が欲しくなります。
projectsで3ビューポートを定義する
Playwrightではprojects配列で複数の実行プロファイルを持てます。
playwright.config.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import { defineConfig, devices } from "@playwright/test";
export default defineConfig({ testDir: "./e2e", outputDir: "./e2e/.artifacts", fullyParallel: false, workers: 1, reporter: [["list"], ["html", { outputFolder: "evidence/_report", open: "never" }]], globalTeardown: "./scripts/build-review.mjs", timeout: 30_000, expect: { timeout: 5_000 },
use: { baseURL: "http://localhost:5173", locale: "ja-JP", timezoneId: "Asia/Tokyo", permissions: ["clipboard-read", "clipboard-write"], trace: "retain-on-failure", video: "off", },
projects: [ { name: "desktop", use: { ...devices["Desktop Chrome"], viewport: { width: 1440, height: 900 } }, }, { name: "tablet", use: { ...devices["Desktop Chrome"], viewport: { width: 820, height: 1180 } }, }, { name: "mobile", use: { ...devices["Desktop Chrome"], viewport: { width: 390, height: 844 } }, }, ],
webServer: { command: "npm run dev", url: "http://localhost:5173", reuseExistingServer: true, timeout: 60_000, }, });
|
projects[].nameがそのままevidenceの親ディレクトリ名になる
viewportでdesktop=1440x900, tablet=820x1180, mobile=390x844を指定
permissions: ["clipboard-read", "clipboard-write"]でクリップボード操作を許可(コピー機能テスト用)
webServerでVite devサーバを自動起動。reuseExistingServer: trueなので開発中のサーバも再利用
npx playwright test一発で、同じspecを3プロジェクト分実行します。
カスタムフィクスチャでスクショ命名を自動化
各テスト中の操作ごとにスクショを撮ると、命名がブレてレビューしづらくなります。
そこでtest.extendでスクショ用のカスタムフィクスチャを作り、app.shot('label')で連番付きで撮れるようにします。
e2e/helpers.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| import { test as base, expect } from "@playwright/test"; import path from "node:path"; import fs from "node:fs";
export { expect };
const EVIDENCE_ROOT = path.resolve("evidence");
function slug(s) { return s.replace(/[^\w-]+/g, "_").replace(/^_+|_+$/g, ""); }
export const test = base.extend({ app: async ({ page }, use, testInfo) => { const projectName = testInfo.project.name; const file = slug(testInfo.titlePath[0] || "spec"); const title = slug(testInfo.title); const evDir = path.join(EVIDENCE_ROOT, projectName, file, title); fs.mkdirSync(evDir, { recursive: true });
let counter = 0;
await page.goto("/", { waitUntil: "domcontentloaded" }); await page.evaluate(() => localStorage.clear()); await page.reload({ waitUntil: "domcontentloaded" }); await page.waitForSelector(".app");
await page.addStyleTag({ content: `*,*::before,*::after{ animation-duration:0s !important; transition-duration:0s !important; }`, });
const helper = { page, evDir, async shot(label, description) { counter += 1; const seq = String(counter).padStart(2, "0"); const base = `${seq}_${slug(label)}`; await page.screenshot({ path: path.join(evDir, `${base}.png`), fullPage: false, }); fs.writeFileSync( path.join(evDir, `${base}.meta.json`), JSON.stringify({ seq: counter, label, description: description || "", testTitle: testInfo.title, viewport: projectName, }, null, 2), "utf-8" ); return `${base}.png`; }, }; await use(helper); }, });
|
- 各テスト開始時に
localStorage.clear()とreloadでクリーンな状態で開始
addStyleTagでアニメ・トランジションをゼロにしてスクショを安定化
app.shot('label')で連番付きPNG+meta.jsonをペアで書き出し
- evidenceパスは
evidence/<viewport>/<spec>/<test>/01_label.pngの階層構造
evidenceディレクトリの構造
このフィクスチャを使うと、テスト結果が以下のように自動構造化されます。
evidence/1 2 3 4 5 6 7 8 9 10 11 12 13 14
| evidence/ ├── desktop/ │ ├── 01-empty-and-folder_spec_js/ │ │ ├── 01_empty_no_folders.png │ │ ├── 01_empty_no_folders.meta.json │ │ ├── 02_new_folder_modal.png │ │ └── ... │ ├── 02-item-crud_spec_js/ │ └── ... ├── tablet/ │ └── (同じ構造) ├── mobile/ │ └── (同じ構造) └── _report/ (Playwright HTMLレポート)
|
同じ操作の同じステップがviewport別のディレクトリに並ぶので、横並び比較がしやすくなります。
189枚(6 spec × 3 viewport × 平均10ステップ)あっても、ディレクトリで整理されているので迷子になりません。
specの書き方
カスタムフィクスチャを使ったspecはこうなります。
e2e/02-item-crud.spec.js1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import { test, expect } from "./helpers.js";
test("アイテムを作成・編集・削除する", async ({ app }) => { const { page } = app;
await page.click("button:has-text('+ フォルダ')"); await page.fill("input[placeholder='フォルダ名']", "開発"); await page.click("button:has-text('作成')");
await page.click("button:has-text('+ 新規')"); await page.fill("input[name='title']", "useDebounce"); await page.fill("textarea[name='body']", "function useDebounce(v, d=300){ /* ... */ }"); await page.click("button:has-text('保存')"); await app.shot("item_added", "useDebounceスニペットを追加");
await page.click("[data-testid='copy-button']"); await app.shot("after_copy", "コピー成功時のチェックアイコン表示");
await page.click("[data-testid='edit-button']"); await page.fill("input[name='title']", "useDebounce (updated)"); await page.click("button:has-text('保存')"); await app.shot("item_updated"); });
|
app.shot() の呼び出し順がそのまま操作フローのドキュメントになるので、後から見返すときの可読性が高いです。
globalTeardownでレビュー画面を自動生成
189枚のPNGをdesktop/tablet/mobileを横並びで比較できるHTMLレビュー画面に自動変換すると、レスポンシブ確認が一気に楽になります。
globalTeardownで全テスト完了後にスクリプトを走らせます。
scripts/build-review.mjs1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import fs from "node:fs"; import path from "node:path";
const EVIDENCE_ROOT = path.resolve("evidence"); const VIEWPORTS = ["desktop", "tablet", "mobile"]; const OUTPUT = path.join(EVIDENCE_ROOT, "review.html");
function walk(dir, list = []) { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { if (entry.isDirectory()) { if (entry.name === "_report") continue; walk(path.join(dir, entry.name), list); } else if (entry.name.endsWith(".png")) { list.push(path.join(dir, entry.name)); } } return list; }
|
これでevidence/review.htmlを開くと、同じステップのdesktop / tablet / mobileが横3列で並ぶレビュー画面が出来上がります。
レスポンシブの崩れを目視チェックするときに圧倒的に楽です。
実行コマンド
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| npx playwright test
npx playwright test --project=mobile
npx playwright test e2e/02-item-crud.spec.js
npx playwright test --ui
npx playwright show-report evidence/_report
|
--project=mobileで問題のあるviewportだけ素早く再テスト
--uiモードはステップ実行できるので、デバッグ時に便利
evidence/_reportにはPlaywright標準のレポートも残る
個人的には、npx playwright testだけでもいいと思ってますが、一応紹介します!
沼りポイント
fullyParallel: trueだとlocalStorageが衝突しごちゃごちゃになる
並列実行を有効にすると、同一baseURLに複数のブラウザが同時アクセスしてlocalStorageが混線することがあります。
workers: 1, fullyParallel: falseにして直列実行にする方が安定します。
3 viewport × 6 spec × 数十ステップでも、自分の環境だと2〜3分で終わるので問題ありません。
Clipboard APIがブラウザ起動オプションで必要
navigator.clipboard.writeText()をテストするにはpermissions: ["clipboard-read", "clipboard-write"]をuseに渡す必要があります。
これが無いとNotAllowedErrorで落ちます。
地味に沼るので忘れずに!
スクショが毎回微妙に違ってDiffが取りにくい
addStyleTagでアニメーション・transitionを無効化(必須)
- フォーカスインジケータの点滅もスクショに混じることがあるので、
document.activeElement.blur()で消す
- 日付・時刻表示があるなら
Dateのモック化を検討
tabletビューポートの定義が悩ましい
iPad縦 (820x1180) を取りましたが、Androidタブレットや横向きまでカバーしたい場合はさらに分岐が必要です。
最初は「desktop / mobileの2つだけ」でも十分価値があります。3
tablet必要に応じて足せばOKです。
まとめ
いかがでしたか?
今回はplaywrightで個人開発のレスポンシブサイトのテストを全自動化してみました!
今回紹介した点をまとめと以下の通り(^^
projects配列でdesktop / tablet / mobileを定義
- カスタムフィクスチャ
app.shot()でスクショ命名とmeta書き出しを自動化
- evidenceディレクトリは
viewport / spec / test / step.pngの階層
globalTeardownで横並びレビューHTMLを自動生成
fullyParallel: falseでlocalStorage衝突を回避
個人開発でレスポンシブの崩れを毎回目視で追うのが辛くなったら、ぜひ試してみてください。
自分は仕事でも使っている民なので、色々な書き方を試して、もっとマスターしていきたいですね!
ちなみにこのE2Eテストを使用して完成したツールである「こぴぺったり」は、コピー&ペーストを簡単に行えるツールになっております。
コピーしたいデータを登録し、登録したデータを押すと文字列がコピーされる仕組みです!
メールアドレスやメールの定型文、PGのよくある書き方集など使い道は様々、ディレクトリ階層がアプリ内にあり、分けやすさも兼ね備えております!
そしてなんと、「完全無料」「登録不要」「コピペデータのデータ送信なし」というセキュリティ的にも安心の一品です。
copype.shinpinoshi.comこぴぺったり
ここから使用できるので、是非使用してみてください!
それでは、よきE2Eライフを(^^b