
## 概要
ローカル E2E のたびに「Supabase 起動 → DB リセット → 認証モック起動 →
本記事は Cloudflare Workers + Supabase(Postgres)+ Playwright の構成を前提に、ローカル E2E を「自己完結スクリプト」に組み立てる実装パターンです。Auth モック、一時環境ファイルの安全なクリーンアップ、状態解決のフォールバック、の3点を扱います。
## 全体構成
``
ON CONFLICT (id) DO NOTHING
## 概要
ローカル 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 env が DB_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 を立ち上げる版)

