End-to-end tests are the ones most likely to eventually get pointed at a real environment instead of a local dev server — that's the whole point of E2E testing, confirming the actual deployed thing works, not just a mock. That's also exactly the setup that can go wrong on a resource-constrained server, which shaped a specific, deliberate default in how Tabeer.ai's Playwright suite got configured.
Install
npm install -D @playwright/test
npx playwright install chromium --with-deps
Chromium only, not all three engines — this is a small project's smoke-test suite, not a cross-browser compatibility matrix. Add Firefox/WebKit later specifically if a browser-specific bug ever justifies the extra install size and CI time.
Config, With the Safety Default Built In
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
// Points at localhost by default — never run this against production without knowing why.
const baseURL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: 'list',
use: { baseURL, trace: 'on-first-retry' },
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],
})
The baseURL default is the actual design decision here, not an afterthought: localhost:3000
unless PLAYWRIGHT_BASE_URL is explicitly set. Nobody runs npm run test:e2e and accidentally
hits production — it takes a deliberate environment variable to point anywhere else.
The Test
// tests/e2e/smoke.spec.ts
import { expect, test } from '@playwright/test'
test('homepage loads with the expected title and hero heading', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/Tabeer\.ai/)
await expect(page.getByRole('heading', { level: 1 })).toContainText('Beat the test')
})
test('the exams hub page loads', async ({ page }) => {
await page.goto('/tests')
await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
})
Verified both ways — against a local dev server, and once against the live site to prove the config actually works end-to-end:
PLAYWRIGHT_BASE_URL=https://tabeer.ai npx playwright test
# ✓ the exams hub page loads (1.1s)
# ✓ homepage loads with the expected title and hero heading (1.1s)
# 2 passed (1.6s)
Why This Needed a Written-Down Warning, Not Just a Sensible Default
The production server this site runs on is a memory-constrained EC2 instance that's been OOM-killed by completely normal single-request traffic — not load, just normal usage at the wrong moment. A Playwright suite that grows over time, run frequently (every push, say), even at light concurrency, is real additional request volume on a box that's already shown it can't always absorb what it gets today. Two isolated page loads for a manual smoke check is nothing; the same suite grown to twenty tests, run on every CI push, is a different risk profile entirely — and that shift tends to happen gradually, one added test at a time, without anyone deciding "let's add load to production" on purpose.
So the config default handles the accidental case, and tests/e2e/README.md handles the
deliberate one:
Fine for occasional smoke checks against production. Do not grow this suite into anything that runs frequently or in a loop against the live site, and never run load/stress tooling against it — the production instance is memory-constrained and has OOM-killed under normal traffic before.
The Actual Principle
A safe default (localhost unless told otherwise) stops the accidental mistake. It doesn't
stop the gradual one — a suite that's fine at 2 tests and genuinely risky at 50, with no
single commit that looks like the problem. That needs the reasoning written down somewhere a
future contributor (or future version of the same person) will actually read before making the
suite bigger, not just a config value that happens to be safe today.
If you're setting up E2E testing for an app running on infrastructure that can't absorb unlimited test traffic, and want the safety built into the setup rather than assumed, get in touch.
By Shahid Malik