Plan: Production Smoke Tests with Seed Data
Overview
A Playwright smoke test suite that runs against the live site (https://ai-mee.uk) after deploys. Each run seeds its own ephemeral test user and full data set (client, campaign, posts) via the Supabase service-role key, runs 9 critical page checks, then deletes everything — leaving zero test artifacts in production.
Reuses the tracked-record cleanup pattern from tests/bot-e2e/helpers/supabase.ts.
Data Lifecycle
Setup: seedAuthUser → seedCustomer → seedCampaign → 3x seedPost → write state JSON → login via real form
Tests: 9 page checks using seeded IDs from state JSON
Teardown: cleanupTestData() → deletes platform_posts → posts → campaign → customer → auth user
Auth is handled via the actual login form (email + password). Playwright's storageState setup-project pattern saves the session once and all tests reuse it.
Steps
Phase 1 — Extend Existing Seed Helpers
File: tests/bot-e2e/helpers/supabase.ts
- Add
seedCampaign(customerId, name, description)— inserts intocustomer_campaign, tracks the ID for cleanup, exports the function. - Add optional
campaign_idparam toseedPost()so posts can be linked to a campaign.
Phase 2 — Playwright Smoke Config
Create: front-end/playwright.smoke.config.ts
testDir: './tests/smoke'baseURLfromSMOKE_BASE_URLenv var (defaulthttps://ai-mee.uk)- No
webServerblock — tests run against the live site globalTeardown: './tests/smoke/global-teardown.ts'- Two Playwright projects:
setup— runsauth.setup.ts, nostorageStatedependencysmoke— runssmoke.spec.ts, depends onsetup, usesstorageState: './tests/smoke/.auth/user.json'
- Chromium only,
expect.timeout: 20_000, retries: 2 on CI,screenshot: 'on'
Modify: front-end/package.json
"test:smoke": "playwright test --config playwright.smoke.config.ts"
Phase 3 — Seed Module + Auth Setup
Create: front-end/tests/smoke/seed.ts
Self-contained seed/cleanup module (cannot cross-import from tests/bot-e2e/ due to separate workspace packages — the bot-e2e helpers serve as the reference implementation).
Functions:
initSeedClient()— creates Supabase admin client using service-role keyseedAuthUser(email, password)— creates auth user via admin REST API, emails confirmed automaticallyseedCustomer(name, userId)— inserts intocustomer_customer, resolves user UUIDseedCampaign(customerId, name)— inserts intocustomer_campaignseedPost(customerId, options)— inserts intocustomer_posts+customer_platform_postcleanupTestData()— deletes all tracked records in reverse FK order, then clears the auth user via admin APIwriteState(state)/readState()— persists seeded IDs totests/smoke/.smoke-state.jsonfor sharing between setup and spec
Cleanup deletion order (respects FK constraints):
customer_platform_postcustomer_postscustomer_campaigncustomer_customer- Auth user (via
DELETE /auth/v1/admin/users/:id)
Create: front-end/tests/smoke/auth.setup.ts (Playwright setup project)
- Fail fast with a clear error if
SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY, orSMOKE_TEST_PASSWORDare missing - Generate a unique email:
smoke-{Date.now()}@test.invalid - Seed data:
seedAuthUser(email, password)→userIdseedCustomer('Smoke Test Co', userId)→customerIdseedCampaign(customerId, 'Smoke Campaign')→campaignIdseedPost(customerId, { status: 'pending_review', campaignId })→postIdseedPost(customerId, { status: 'approved' })seedPost(customerId, { status: 'sent' })
writeState({ customerId, campaignId, postId })- Navigate to
/auth, click "Use password instead" (.forgot-link-btn), fill email + password, submit - Wait for redirect to
/app(confirms login succeeded) page.context().storageState({ path: 'tests/smoke/.auth/user.json' })
Create: front-end/tests/smoke/global-teardown.ts
import { cleanupTestData } from './seed'
import { unlink } from 'node:fs/promises'
export default async function globalTeardown() {
try {
await cleanupTestData()
await unlink('tests/smoke/.smoke-state.json').catch(() => {})
await unlink('tests/smoke/.auth/user.json').catch(() => {})
} catch (err) {
console.error('[smoke teardown] cleanup error (non-fatal):', err)
}
}
Phase 4 — Smoke Test Spec
Create: front-end/tests/smoke/smoke.spec.ts
Uses test.describe.serial() — tests run in order and share the saved auth state.
Reads .smoke-state.json in test.beforeAll() to get seeded customerId, campaignId, postId.
| # | Test | Route | Key Assertion |
|---|---|---|---|
| 1 | Dashboard loads | /app | .company-dashboard visible |
| 2 | Clients list | /app/clients | Search input visible + ≥1 client row |
| 3 | Client detail | /app/client/{customerId} | Text "Smoke Test Co" visible |
| 4 | Client integrations | /app/client/{customerId}/integrations | Integration cards present |
| 5 | Client settings | /app/client/{customerId}/settings | Settings form visible |
| 6 | Posts list | /app/posts | ≥1 post row visible |
| 7 | Post editor | Click first post in posts list | Editor container loads |
| 8 | Campaigns | /app/campaigns | ≥1 campaign row visible |
| 9 | Settings | /app/settings | Settings page loads |
Every test also asserts: no Vue error overlay, no 404 catch-all page ("Page not found" text absent).
Phase 5 — GitHub Actions Workflow
Create: .github/workflows/smoke.yml
Triggers:
deployment_status— fires automatically when Cloudflare Pages GitHub app reports a successful deployworkflow_dispatch— manual trigger with an optionalbase_urlinput for stagingschedule: '0 */6 * * *'— fallback cron every 6 hours (catches manualwranglerdeploys)
Condition: skip if deployment_status event and .state != 'success'.
Job steps (ubuntu-latest):
- Checkout
- pnpm 10.10.0 + Node 20 (cache on
front-end/pnpm-lock.yaml) cd front-end && pnpm install --frozen-lockfilepnpm exec playwright install chromium --with-deps- Health check:
curl --retry 10 --retry-delay 5 -sf $SMOKE_BASE_URL pnpm test:smoke- Upload
playwright-report/artifact on failure (7-day retention)
Required GitHub secrets:
| Secret | Purpose |
|---|---|
SUPABASE_URL | Production Supabase project URL |
SUPABASE_SERVICE_ROLE_KEY | Bypasses RLS for seeding + cleanup |
SMOKE_TEST_PASSWORD | Password set for each ephemeral test user |
Phase 6 — Gitignore
Modify: front-end/.gitignore
tests/smoke/.auth/
tests/smoke/.smoke-state.json
Files Summary
| Action | File |
|---|---|
| Modify | tests/bot-e2e/helpers/supabase.ts — add seedCampaign(), add campaign_id param to seedPost() |
| Modify | front-end/package.json — add test:smoke script |
| Modify | front-end/.gitignore — add smoke temp paths |
| Create | front-end/playwright.smoke.config.ts |
| Create | front-end/tests/smoke/seed.ts |
| Create | front-end/tests/smoke/auth.setup.ts |
| Create | front-end/tests/smoke/global-teardown.ts |
| Create | front-end/tests/smoke/smoke.spec.ts |
| Create | .github/workflows/smoke.yml |
Verification Steps
- Local dry-run: Set env vars and run
pnpm test:smokeagainst production. All 9 tests pass. Querycustomer_customerwith service-role key afterwards — zero smoke records should remain. - Cleanup resilience: Kill the test mid-run (
Ctrl+C), re-run — unique timestamp email avoids collision;globalTeardownhandles cleanup. - Manual CI trigger: Fire
workflow_dispatchfrom GitHub Actions UI — full pipeline runs end-to-end. - Intentional failure: Break one assertion → verify workflow fails, report artifact uploads, cleanup still runs.
Decisions
Ephemeral user per run, not a shared test account.
No persistent test user to manage. Timestamp in email (smoke-{Date.now()}@test.invalid) prevents collisions between concurrent runs. Full isolation between runs.
Self-contained seed module in front-end/tests/smoke/seed.ts.
Cannot cross-import from tests/bot-e2e/helpers/supabase.ts due to separate workspace packages. The bot-e2e helper is the reference implementation; the smoke module mirrors the same tracked-records + reverse-FK-deletion pattern.
globalTeardown for cleanup, not afterAll.
Playwright's globalTeardown runs even if tests crash or are cancelled. The cleanupTestData() function deletes records in reverse FK insertion order: customer_platform_post → customer_posts → customer_campaign → customer_customer → auth user.
Real login via the actual form.
Tests the auth flow itself (not just page rendering). Playwright's storageState setup-project pattern is the official recommended approach. .forgot-link-btn toggles email+password mode on /auth.
Service-role key as a CI secret.
Required to bypass RLS for seeding and to delete the auth user at teardown. Same pattern used by the bot-e2e test suite. The key is never logged.
Further Considerations
Cloudflare Pages trigger:
If CF Pages isn't connected via the GitHub app (manual wrangler deploy), deployment_status events won't fire. The 6-hour cron catches drift. Alternatively, append the following to your local deploy script:
gh workflow run smoke.yml --ref master
Orphaned data safety net (future enhancement):
If teardown fails (network error), orphaned smoke data accumulates in production. A future sweeper could delete customer_customer rows where:
email LIKE 'smoke-%@test.invalid' AND created_at < now() - interval '1 hour'
Not needed for v1 — unique timestamps mean orphans don't interfere with future runs.