All Articles
Testing

Playwright E2E Tests, and the One Line of Config That Keeps Them Safe to Run

Shahid MalikBy Shahid MalikSeptember 4, 20265 min read

Setting up Playwright for Tabeer.ai's Nuxt frontend — real smoke tests, verified against both localhost and live production, plus the deliberate default and written-down warning that stops a test suite from accidentally becoming a stress test.

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.

Related Articles

Testing

Setting Up Vitest on a Nuxt App With Zero Existing Tests

Tabeer.ai's frontend had no unit tests at all before this. Rather than write a sprawling first test suite, here's the actual config and the one real test that proves the setup works — a foundation to build on, not a coverage number to chase.

5 min read
Testing

Adding pytest to a Django Project Without Rewriting a Single Test

Tabeer.ai had 659 tests written the standard Django unittest way. Adding pytest didn't mean migrating them — pytest-django runs unittest-style TestCase classes completely unchanged. Here's the actual config and what 'zero-rewrite' really means in practice.

5 min read
Shahid Malik - AI-First Odoo Consultant

Shahid Malik

AI-First Odoo ERP Specialist

Shahid Malik is an AI-first Odoo consultant helping businesses solve complex ERP and business process challenges. His work combines Odoo consulting, process optimization, automation, integrations, migrations, and practical AI solutions to build scalable and reliable business systems.

Book a consultation for your Odoo project
Discuss Your Odoo Project