- How to parameterize base URLs per environment (local / CI / staging)
.env.local/.env.ci/ env-variable layering- The capstone: you write a feature from scratch, solo
The suite you built runs against http://localhost:4200 and http://localhost:3000. In reality you'll eventually want to run the same suite against a deployed staging environment. That's a config change, not a code change.
And by now, you should be able to add a feature without a walkthrough. The capstone proves it.
- Module 14 complete
git checkout 15-start
apps/web-e2e/playwright.config.ts:
const webBaseURL = process.env['E2E_BASE_URL'] ?? 'http://localhost:4200';
const apiBaseURL = process.env['E2E_API_BASE_URL'] ?? 'http://localhost:3000';Environment takes precedence. Set either via shell, .env, or CI secrets.
E2E_BASE_URL="http://localhost:4200"
E2E_API_BASE_URL="http://localhost:3000"
JWT_SECRET="dev-not-secret-pin-for-local-do-not-use-in-production"
Commit .env.example. Git-ignore .env / .env.local. Participants copy the example and edit locally.
For CI, set these via repository secrets / env vars rather than a committed file.
Assume staging is at https://api.staging.yourproduct.com:
E2E_API_BASE_URL=https://api.staging.yourproduct.com \
E2E_BASE_URL=https://staging.yourproduct.com \
npm run e2eWhat needs to be true for this to work:
- The staging api has the
/test/*seam enabled (dangerous — only do this for sealed staging envs, never prod) - The staging web points at the staging api
- Your network allows Playwright to reach both
If any of those isn't true, you run against local and mock external dependencies (back to Module 10).
Remember webServer in playwright.config.ts:
webServer: [
{
command: 'npx nx run api:serve',
url: `${apiBaseURL}/auth/me`,
reuseExistingServer: !process.env['CI'],
// ...
},
{
command: 'npx nx run web:preview',
url: webBaseURL,
reuseExistingServer: !process.env['CI'],
// ...
},
],- Locally —
reuseExistingServer: truemeans if you're already runningnpm start, Playwright uses it. Otherwise it starts the servers for you. - CI —
reuseExistingServer: false, always starts fresh.
If you're running against a deployed environment, omit webServer entirely — Playwright expects the URL to already be reachable.
Write a feature from scratch covering a user flow we haven't tested yet. No walkthrough. Pick one:
Add a UI affordance for editing a project's description (if the current app doesn't have one, add it). Write a feature that:
- Seeds a project via API
- Opens the project
- Edits the description through the UI
- Asserts via both UI and an API round-trip
Write a feature covering:
- Tasks with past-due dates displaying differently (you may need to add visual indicators in the web app first)
- Creating a task with no due date
- Editing a task to clear its due date
Add a UI for selecting multiple tasks and updating their status. Write a feature that seeds 5 tasks, selects 3, changes their status, verifies the other 2 are untouched.
Anything that:
- Has a clear user-facing behavior
- Exercises both UI and API paths
- Has at least one edge case / error path
- Takes less than a day
For any option, your deliverable is:
- Feature file (
apps/web-e2e/src/features/<area>/<name>.feature) — tagged appropriately, 3+ scenarios - Step defs you reuse or add
- Any POM additions (maintain the "no DOM in steps" rule)
- All scenarios green twice in a row under
--workers 4
# Your new scenarios
npm run e2e -- --grep @capstone
# Whole suite — should still be green
npm run e2egit diff 15-complete -- apps/web-e2eNote: 15-complete in this repo contains one possible capstone solution. Yours will look different — that's the point.
Env-variable layering (leftmost wins):
CLI env (E2E_BASE_URL=... npm run e2e)
> .env.local
> .env
> playwright.config.ts defaults
Running against staging:
E2E_BASE_URL=https://staging.example.com \
E2E_API_BASE_URL=https://api.staging.example.com \
npm run e2eAssumptions: the staging api has the /test/* seam (or your suite has been adapted to not need it), and your user has creds that work there.
Capstone checklist:
- Feature file in the right
features/<area>/folder - Tagged (
@capstone, area tag, module tag) - At least one happy-path scenario
- At least one error/edge scenario
- No
page.getByXinside step defs (POM discipline from Module 02) - Names scoped with
scenarioIdvia seed fixtures (Module 07) - Green twice in a row
Seriously — if you finished the capstone, you've built real-world E2E testing skills. The patterns here (POM discipline, API-seeded isolation, fixtures, storageState, network interception) are what professional QA engineers use on production codebases.
Next stops:
- Apply these patterns to your own app. Start with the isolation discipline (the one lesson that saves the most time).
- Read the
docs/concepts.mdreference when you need to jog your memory. - Share the workshop with a teammate.
If you spot bugs or want to improve a module, open a PR — this is meant to stay alive.