Environment Drift: Staging vs Local E2E
Short answer
Specs pass on localhost:3000 but fail on preview or staging because BASE_URL, API hosts, and feature flags differ—not because Playwright is flaky. Fix with explicit env posture per CI job, seed routes that match the target deployment, and multi-environment execution instead of hardcoded URLs.
Part of Common E2E testing gotchas.
Symptom
- Green locally, red in GitHub Actions against preview URL
page.goto('/checkout')lands on wrong tenant or 404 SPA shell- API calls hit
localhostwhile UI loads*.vercel.app - Feature branch preview missing routes the spec expects
- Intermittent failures when
% rolloutflags differ between preview and staging
Root cause
E2E assumes one implicit environment while CI runs against another:
playwright.config.tshardcodeshttp://localhost:3000but CI setsBASE_URLonly in shell—not in config- Client bundle baked with
NEXT_PUBLIC_API_URLat build time; server reads runtime env on preview - Preview deploy uses different
FLAG_*or LaunchDarkly project than staging - Relative
page.goto('/dashboard')resolves against wrong origin whenbaseURLis unset - Webhook or OAuth redirect URLs registered for staging, not ephemeral preview hostnames
This overlaps missing feature flag seeds and hardcoded test data—environment is the Arrange layer those fixes assume.
Fix: explicit environment posture
1. Single source of truth for baseURL
// playwright.config.ts
const baseURL =
process.env.BASE_URL ??
process.env.PLAYWRIGHT_BASE_URL ??
'http://localhost:3000';
export default defineConfig({
use: { baseURL },
projects: [
{ name: 'local', use: { baseURL: 'http://localhost:3000' } },
{ name: 'preview', use: { baseURL: process.env.PREVIEW_URL } },
],
});
In CI, export BASE_URL before npx playwright test—never rely on developers remembering to match preview manually. Document the variable in your test plan and PR template.
2. Preview URL from deploy output
# GitHub Actions — after Vercel/Netlify deploy
- name: E2E against preview
env:
BASE_URL: ${{ steps.deploy.outputs.url }}
run: npx playwright test --project=preview
Preview URLs change every PR. Pin the URL from the deploy step; do not commit https://my-app-git-feature-abc.vercel.app into specs. See multi-environment execution for SmartTests targeting staging, preview, and prod with the same spec files.
3. Seed flags and API hosts for the target env
test.beforeEach(async ({ request }) => {
const runId = `env-${test.info().workerIndex}-${Date.now()}`;
await request.post('/api/test/set-flags', {
data: {
runId,
flags: { NEW_CHECKOUT: true },
// optional: assert server reports same env CI thinks it hit
expectedHost: new URL(process.env.BASE_URL!).host,
},
});
});
Probe that the app reports the environment you intend:
await expect.poll(async () => {
const res = await request.get('/api/test/probe-env');
return (await res.json()).deployment;
}).toBe('preview');
4. Local vs CI parity checklist
| Check | Local | CI preview |
|---|---|---|
BASE_URL / baseURL | localhost | Deploy output URL |
| Feature flags | .env.local | Seed route or test LD project |
| Auth redirect URIs | http://localhost:3000/callback | Preview hostname registered |
| API + UI same origin | Usually yes | Verify split CDN vs API gateway |
| Seed routes enabled | NODE_ENV=test | Same guard on preview/staging |
Anti-patterns
| Anti-pattern | Why it fails | Better approach |
|---|---|---|
Hardcoded https://staging.example.com in specs | Breaks preview; forks env per branch | baseURL from env; relative navigation |
BASE_URL in shell only, not Playwright config | Config still points at localhost | Read env in playwright.config.ts |
| Assume staging flags = preview flags | Branch deploys differ | Seed flag posture per run |
| Skip E2E on preview; only run on staging | Drift ships until merge | PR job against preview URL |
Different .env files undocumented | "Works on my machine" | Env matrix in markdown test plan |
TestChimp workflow
/testchimp init scaffolds env-aware fixtures—BASE_URL from CI, seed routes, and probe-env stubs so SmartTests run against preview without editing URLs per PR. /testchimp test on PRs reads markdown scenarios that name target environment (local, preview, staging); agents repair specs when deploy output or flag posture changes—not orphan chat guesses.
Related
- Multi-environment execution
- Missing feature flag seed
- Hardcoded test data
- GitHub Actions parallel
- Seed routes and probe Assert
- Playwright test configuration
Frequently asked questions
Why do Playwright tests pass locally but fail in CI?
Usually BASE_URL, API host, or feature flags differ between localhost and the deployment CI targets. Set baseURL from env in playwright.config.ts and seed flag posture per run—do not assume staging matches your laptop.
Should E2E run against preview or staging?
Preview on every PR catches branch-specific drift before merge. Staging nightly catches integration with shared services. Use the same specs with different BASE_URL—see multi-environment execution.
How do I pass preview URL from Vercel to Playwright?
Capture the deploy output URL in your CI step and export it as BASE_URL before npx playwright test. Never hardcode ephemeral preview hostnames in spec files.
Our API is on a different domain than the UI—what breaks?
Cookies, CORS, and relative page.goto paths. Use full URLs only when necessary; register OAuth redirect URIs for each preview host; probe that API calls hit the intended backend.
Feature flags differ per environment—how do E2E stay stable?
Seed explicit flag state in Arrange via a test-only route or LD test project keyed to runId. Never assume preview has the same rollout as staging—see the missing feature flag gotcha.
Is environment drift the same as flaky tests?
Symptoms overlap—timeouts on wrong host look like flake. Log resolved baseURL and probe-env at test start; if host or deployment label is wrong, fix posture before raising timeouts.
Does TestChimp support multi-environment SmartTests?
Yes—/testchimp init wires BASE_URL from CI and documents env targets in markdown plans. /testchimp test on PRs keeps specs aligned when preview URLs or flag matrices change.
Should NEXT_PUBLIC_* vars match between local and CI?
Client bundle vars are build-time. If preview builds with different NEXT_PUBLIC_API_URL than local, UI may call the wrong API even when BASE_URL is correct. Align build args per environment or probe API host in Arrange.
Apply these patterns in your repo
Run `/testchimp init` to connect TestChimp to your repo, then `/testchimp test` on PRs to turn these patterns into maintained SmartTests. Use `/testchimp evolve` when you want to expand coverage as your app grows.