Skip to content

Playwright Browser Testing

Set up Playwright with Chromium for end-to-end browser testing in local development and GitHub Actions CI.

When to use

Use Playwright when tests need a real browser — navigation, clicks, form fills, network interception, screenshots. It runs headless by default, making it suitable for both local npm test and CI pipelines.

The pattern

Install

Two options depending on context:

Global install via Volta — best for ad-hoc browser automation, quick scripting, and one-off testing outside a project:

sh
volta install playwright
playwright install chromium

This puts playwright on your PATH everywhere without touching any project's package.json.

Local install per project — best when the project has Playwright tests, especially if they run in CI. CI environments don't have Volta, so the project needs Playwright in its own dependencies:

sh
npm install --save-dev playwright
npx playwright install chromium

Install only Chromium unless you need cross-browser coverage — this keeps browser downloads small.

Write a test

Use node:test with Playwright's chromium launcher. Each describe block manages its own browser lifecycle through setup and cleanup tests:

javascript
import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { chromium } from "playwright";

describe("home page", () => {
  let browser;
  let page;
  
  test("setup", async () => {
    browser = await chromium.launch({
      headless: true,
    });
    page = await browser.newPage();
  });
  
  test("loads and shows heading", async () => {
    await page.goto("http://localhost:3000");
    const heading = await page.textContent("h1");
    assert.strictEqual(heading, "Welcome");
  });
  
  test("navigates to about page", async () => {
    await page.click('a[href="/about"]');
    await page.waitForURL("**/about");
    const title = await page.title();
    assert.ok(title.includes("About"));
  });
  
  test("cleanup", async () => {
    await browser?.close();
  });
});

Run with node --test test/browser.test.js.

Console message capture

Intercept browser console output to verify client-side logging or detect errors:

javascript
const messages = [];
page.on("console", (msg) => {
  messages.push(msg.text());
});
page.on("pageerror", (error) => {
  console.error("Page error:", error.message);
});

await page.goto("http://localhost:3000");
await page.waitForTimeout(1000);

assert.ok(
  messages.some((m) => m.includes("ready")),
  "Expected 'ready' in console output",
);

GitHub Actions CI

Add a workflow step that installs only Chromium before running tests:

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci
  
      - name: Install Playwright Browsers
        run: npx playwright install chromium
  
      - run: npm test

Serving files during tests

When testing static HTML, spin up a temporary server with automatic port selection so tests don't collide:

javascript
import { createServer } from "node:http";
import { readFileSync } from "node:fs";
import { join, extname } from "node:path";

function startServer(root) {
  const types = {
    ".js": "application/javascript",
    ".html": "text/html",
    ".css": "text/css",
  };
  const server = createServer((req, res) => {
    const file = req.url === "/"
      ? "index.html"
      : req.url.slice(1).split("?")[0];
    try {
      const body = readFileSync(
        join(root, file),
      );
      res.writeHead(200, {
        "Content-Type":
          types[extname(file)] || "text/plain",
      });
      res.end(body);
    } catch {
      res.writeHead(404);
      res.end("Not found");
    }
  });
  return new Promise((resolve) => {
    server.listen(0, "127.0.0.1", () => {
      const port = server.address().port;
      resolve({ server, port });
    });
  });
}

Trade-offs

ChoiceProCon
Chromium onlyFast install, small CI cacheNo Firefox/WebKit coverage
All browsersCross-browser confidenceSlower CI, larger downloads
HeadlessCI-friendly, fasterCan't visually debug
HeadedSee what's happeningCan't run in CI

Start with Chromium-only headless. Add Firefox or WebKit only after you've hit a browser-specific bug that warrants the extra CI time.