Skip to main content

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 localhost while UI loads *.vercel.app
  • Feature branch preview missing routes the spec expects
  • Intermittent failures when % rollout flags differ between preview and staging

Root cause

E2E assumes one implicit environment while CI runs against another:

  • playwright.config.ts hardcodes http://localhost:3000 but CI sets BASE_URL only in shell—not in config
  • Client bundle baked with NEXT_PUBLIC_API_URL at 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 when baseURL is 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

CheckLocalCI preview
BASE_URL / baseURLlocalhostDeploy output URL
Feature flags.env.localSeed route or test LD project
Auth redirect URIshttp://localhost:3000/callbackPreview hostname registered
API + UI same originUsually yesVerify split CDN vs API gateway
Seed routes enabledNODE_ENV=testSame guard on preview/staging

Anti-patterns

Anti-patternWhy it failsBetter approach
Hardcoded https://staging.example.com in specsBreaks preview; forks env per branchbaseURL from env; relative navigation
BASE_URL in shell only, not Playwright configConfig still points at localhostRead env in playwright.config.ts
Assume staging flags = preview flagsBranch deploys differSeed flag posture per run
Skip E2E on preview; only run on stagingDrift ships until mergePR 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.

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.

Start free on TestChimp · Book a demo