IKivan-kozenko -aqa
Try yourself as a QA tester
All topics·Intermediate·10 / 27

Web server

The webServer option in playwright.config.ts starts your dev server before any test runs and kills it after. The setting I always set: reuseExistingServer: !process.env.CI — locally it reuses my already-running 'npm run dev' so tests start instantly, on CI it always starts fresh.

Basic setup — start a dev server for all tests

Playwright starts the server and polls the url until it responds — tests only run after the server is ready

I add webServer when writing tests against a local app that isn't deployed anywhere yet. Playwright waits for the url to return a 2xx/3xx response before running the first test. If the server doesn't respond within timeout milliseconds, tests fail with a clear error.

reuseExistingServer: !process.env.CI is the key setting. Locally: if my dev server is already running (which it usually is), Playwright just uses it without starting a new one — tests start in seconds. On CI: CI is set, so !process.env.CI is false, Playwright always starts fresh.

ts
// playwright.config.ts
export default defineConfig({
  webServer: {
    command: 'npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    stdout: 'ignore',
    stderr: 'pipe',
  },
  use: {
    baseURL: 'http://localhost:3000',
  },
})

Slow server — increase timeout

The default timeout is 60 seconds. For Next.js or other frameworks that do a full build on first start, I bump this to 120 seconds. On CI with a slow runner it can take even longer.

ts
// playwright.config.ts
export default defineConfig({
  webServer: {
    command: 'npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120 * 1000,  // 2 хвилини замість 1
  },
})

Wait for server output instead of URL

Sometimes the URL check isn't the right signal — the server responds 200 before the database connection is ready. The wait option waits for a specific string in stdout or stderr instead. Named capture groups in the regex get stored as environment variables.

ts
// playwright.config.ts
export default defineConfig({
  webServer: {
    command: 'npm run start',
    wait: {
      // Чекати поки сервер виведе цей рядок в stdout
      stdout: /Server ready on port (?<port>\d+)/,
    },
    // Якщо задані і url і wait — сервер вважається готовим
    // коли виконується хоча б одна умова
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
})

Multiple servers — frontend + backend

For projects where the frontend and backend run on different ports, I pass an array. Each server gets a name that prefixes its log output — makes it easy to see which server printed what in CI logs.

ts
// playwright.config.ts — frontend + backend разом
export default defineConfig({
  webServer: [
    {
      command: 'npm run start',
      url: 'http://localhost:3000',
      name: 'Frontend',
      timeout: 120 * 1000,
      reuseExistingServer: !process.env.CI,
    },
    {
      command: 'npm run backend',
      url: 'http://localhost:3333',
      name: 'Backend',
      timeout: 120 * 1000,
      reuseExistingServer: !process.env.CI,
    },
  ],
  use: {
    baseURL: 'http://localhost:3000',
  },
})

Pairing webServer with baseURL

I always pair webServer with baseURL in the use section. Without baseURL, every page.goto() needs the full URL. With it, I write page.goto('/orders') and Playwright prepends http://localhost:3000 automatically.

ts
// У тестах після налаштування webServer + baseURL
test('orders page loads', async ({ page }) => {
  await page.goto('/orders')  // → http://localhost:3000/orders
  await expect(page.getByRole('heading', { name: 'Orders' })).toBeVisible()
})

test('dashboard shows metrics', async ({ page }) => {
  await page.goto('/dashboard')  // → http://localhost:3000/dashboard
})