![「npm run e2e」を中央に据え、Supabase起動、DBリセット、認証モック、Cloudflareを経由し、テスト実行とクリーンアップまでを網羅するローカルE2E環境の自動化ワークフロー図。チェッカーフラッグを持つキャラクターがDXの向上を示唆します。](https://wakatchi.dev/wp-content/uploads/2026/05/self-contained-e2e-setup-eyecatch.webp)

## 概要

ローカル E2E のたびに「Supabase 起動 → DB リセット → 認証モック起動 → .dev.vars 編集 → ようやく npm run e2e」を踏んでいると、心理的なハードルで E2E 自体が回らなくなります。npm run e2e を打つだけで前処理が完結し、終了時にきれいに片付く構成にすると、PR を出すたびにふつうに E2E が走るようになります。

本記事は Cloudflare Workers + Supabase(Postgres)+ Playwright の構成を前提に、ローカル E2E を「自己完結スクリプト」に組み立てる実装パターンです。Auth モック、一時環境ファイルの安全なクリーンアップ、状態解決のフォールバック、の3点を扱います。

## 全体構成

``
npm run e2e
└─> scripts/e2e.mjs
├─> prepareLocalE2EEnvironment() ← 前処理
│ ├─ ensureLocalDatabaseUrl (Supabase の DB_URL を解決、未起動なら start)
│ ├─ resetLocalDatabase (supabase db reset で初期化)
│ ├─ startSupabaseAuthMock (小さな HTTP サーバで認証API代替)
│ └─ stageEnvironmentFiles (一時 .dev.vars / .env.local を生成)
├─> wrangler dev + Playwright run
└─> finally cleanup() ← 失敗しても必ず後始末
`

## エントリの責任ある cleanup パターン

非同期リソースを確保する関数は、途中で失敗したら確保済みのリソースを必ず解放します。確保順の逆順で
cleanup を呼びます。

`js
export async function prepareLocalE2EEnvironment(repoRoot) {
const dbUrl = await ensureLocalDatabaseUrl(repoRoot)
await resetLocalDatabase(repoRoot)
const authMock = await startSupabaseAuthMock(dbUrl)

const runtimeEnv = {
CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE: dbUrl,
SUPABASE_URL: authMock.baseUrl,
SUPABASE_ANON_KEY: 'e2e-anon-key',
// ...
}

let envFiles
try {
envFiles = await stageEnvironmentFiles(repoRoot, runtimeEnv)
} catch (error) {
await authMock.cleanup() // ★ envFiles 失敗時は authMock を片付ける
throw error
}

return {
runtimeEnv,
cleanup: async () => {
await envFiles.cleanup() // ★ 確保順の逆で片付け
await authMock.cleanup()
},
}
}
`

ポイント:

- 各
cleanup は 1 つのリソース専用にして、再入可能(idempotent)にしておきます
- 呼び出し側は
try { ... } finally { await env.cleanup() } で必ず後片付けをします

## 状態解決のフォールバック

「Supabase が起動済みなら DB_URL を取得、未起動なら起動してから取得」をリトライ無しで素直に書きます。

`js
async function ensureLocalDatabaseUrl(repoRoot) {
// 環境変数で明示されていれば最優先
const explicit = process.env.CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE?.trim()
if (explicit) return explicit

// 1回目: 既に起動していれば取れる
const first = await readSupabaseStatusEnv(repoRoot)
if (first.DB_URL) return first.DB_URL

// 起動して 2回目: 起動成功後に再取得
await runSupabaseCommand(repoRoot, ['start', '--ignore-health-check'])
const second = await readSupabaseStatusEnv(repoRoot)
if (second.DB_URL) return second.DB_URL

throw new Error('Unable to resolve a local DB_URL.')
}
`

supabase status -o envDB_URL=... 形式の env 出力をくれるのを利用します。リトライループを書くのではなく「未起動なら start して再取得」の2段で十分です。

## 認証 API モック: pg 直接アクセスの小さな HTTP サーバ

Supabase Auth API の本物を再現する必要はありません。テストで使う最小操作(signUp / signIn / getUser)だけ、Postgres に直接書く HTTP サーバで代替します。

`js
async function startSupabaseAuthMock(dbUrl) {
const pool = new Pool({ connectionString: dbUrl, max: 1 })
const usersByEmail = new Map()
const usersById = new Map()

// demo ユーザを seed
await pool.query(
INSERT INTO public.users (id, email) VALUES ($1, $2)
ON CONFLICT (id) DO NOTHING, [DEMO_USER_ID, DEMO_EMAIL])

const server = createServer((req, res) => {
// /auth/v1/token, /auth/v1/user, /auth/v1/signup の最小実装
// ...
})
await new Promise((resolve) => server.listen(0, resolve))
const baseUrl =
http://127.0.0.1:${server.address().port}

return {
baseUrl,
cleanup: async () => {
await new Promise((resolve) => server.close(resolve))
await pool.end()
},
}
}
`

利点:

- 認証ロジックの揺れに振り回されません(トークン検証等は不要、E2E 用の固定値で OK)
- 起動が速いです(コンテナ起動を待ちません)
- ポートは
listen(0) でOSに割り当てさせ、衝突を避けられます

## 「特定操作を1回だけ許可する」をDB制約で表現する

E2E に絡む副次的な学びです。「祝福通知は同じ目標で1回だけ」のような操作の唯一性は、アプリ層の
isAlreadyCelebrated フラグより、DB の PRIMARY KEY 制約で表現する方が堅いです。

`sql
CREATE TABLE IF NOT EXISTS goal_completion_events (
goal_id INTEGER PRIMARY KEY REFERENCES goals(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
completed_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`

goal_id を PRIMARY KEY にすることで、同じ goal を何度達成しても 2 回目以降は INSERT が失敗します。アプリは INSERT ... ON CONFLICT DO NOTHING で受け止めればよく、競合状態の心配がなくなります。

## まとめ

- ローカル E2E は **
npm run e2e 一発で完結** を目指します。前処理ドキュメントを書くより遥かに実用的です
- 確保したリソースは **逆順 cleanup**、途中失敗時も既確保ぶんを片付けます
- 状態解決は「既に整っているなら取得、無ければ作る」の2段フォールバックで素直に実現できます
- 認証モックは pg 直接アクセスの小さな HTTP サーバで充分です
- 「特定操作を1回だけ」は DB の PRIMARY KEY で表現するのが堅いです

## 次にやりたいこと

- E2E のフレーク率モニタリング(テスト名 × 失敗率の推移)
- Auth モックを共通パッケージ化して別プロジェクトで再利用
-
e2e-local-environment.mjs` を CI でも使える形に拡張(GitHub Actions 上で Supabase を立ち上げる版)