Playwright tests, when run in Docker, can become surprisingly sluggish if you don’t account for the I/O performance characteristics of the containerized filesystem.
Let’s see Playwright in action within a Docker container. Imagine a simple test suite that navigates to a webpage, interacts with an element, and asserts some text.
# Dockerfile
FROM mcr.microsoft.com/playwright:v1.41.2-jammy
WORKDIR /app
COPY . .
RUN npm install
CMD ["npx", "playwright", "test"]
And a basic test file (example.spec.ts):
// example.spec.ts
import { test, expect } from '@playwright/test';
test('should navigate and find text', async ({ page }) => {
await page.goto('https://playwright.dev/');
const heading = page.locator('h1');
await expect(heading).toContainText('Playwright');
});
When you build and run this, you’re essentially spinning up a Linux environment with Playwright pre-installed. The npm install step brings in your project dependencies, and the CMD executes the Playwright test runner. This setup is great for CI/CD pipelines, ensuring consistent test environments and avoiding "it works on my machine" issues.
The core problem Playwright solves is providing a unified API for automating Chromium, Firefox, and WebKit. This means you can write tests once and run them against all major browsers. Internally, Playwright communicates with browser instances via the WebSocket protocol. For each browser, it launches a separate process and establishes a WebSocket connection. The Playwright test runner then orchestrates these browser instances, sending commands (like page.goto, element.click) and receiving results.
The levers you control are primarily within your Dockerfile and your Playwright configuration (playwright.config.ts).
In the Dockerfile:
- Base Image: You choose the Playwright Docker image. These are pre-configured with Node.js and Playwright, along with necessary system dependencies. Different tags offer different base OS versions (e.g.,
jammy,focal) and Playwright versions. - Dependencies: You install your project’s Node.js dependencies (
npm installoryarn install). - Code Copying: You copy your test files and application code into the container.
- Execution Command: You define how to run the tests (
npx playwright test).
In playwright.config.ts:
projects: Define different browser configurations (e.g., a project for Chromium, another for Firefox).use: Configure browser-specific settings like viewport size, timezone, or device emulation.reporter: Specify how test results are reported (e.g.,list,html).outputDir: Where screenshots and traces are saved.
Consider this configuration snippet for playwright.config.ts:
// playwright.config.ts
import type { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
testDir: './tests',
reporter: 'list',
projects: [
{
name: 'chromium',
use: {
browserName: 'chromium',
viewport: { width: 1280, height: 720 },
launchOptions: {
// Example: headless vs. headed mode
headless: true,
},
},
},
// Add other projects for Firefox, WebKit if needed
],
use: {
// Global configuration
baseURL: 'http://localhost:3000', // If your app runs on a specific port
trace: 'on-first-retry', // Capture traces for retried tests
screenshot: 'only-on-failure', // Capture screenshots only when tests fail
},
outputDir: 'test-results/',
timeout: 60 * 1000, // 60 seconds timeout per test
expect: {
timeout: 5000, // 5 seconds timeout for Playwright assertions
},
};
export default config;
This configuration tells Playwright to run tests in the tests directory using the list reporter, specifically targeting the chromium browser with a defined viewport. It also sets a global base URL, enables trace capturing on retries, and takes screenshots only on failure.
The most surprising thing about Playwright’s internal architecture is how it manages browser context isolation. When you create a new browserContext, Playwright doesn’t just start a new browser process; it often reuses an existing browser instance and creates a new context within it. This is analogous to opening new private browsing windows in your desktop browser. Each context has its own cookies, local storage, and session. This allows for efficient reuse of browser processes while maintaining strong isolation between test runs or different scenarios within a single test file, preventing interference.
If you’re dealing with very slow file operations within your containerized Playwright tests, especially during dependency installation or asset loading, consider mounting your node_modules directory as a volume from the host machine if running locally for development, or using a Docker image with a more performant filesystem for your CI environment.