playwright
End-to-end testing for modern web
$ npx docs2skills add playwright-testPlaywright
End-to-end testing for modern web applications
What this skill does
Playwright Test is a comprehensive end-to-end testing framework that bundles test runner, assertions, isolation, parallelization and rich tooling into a single solution. It enables testing of modern web applications across Chromium, WebKit and Firefox browsers on Windows, Linux and macOS, locally or in CI environments.
Unlike traditional testing tools, Playwright provides native cross-browser support with consistent APIs, automatic waiting for elements, network interception, and powerful debugging tools including trace viewer and UI mode. It solves the reliability issues common in web testing through web-first assertions and smart element selection strategies.
The framework includes mobile emulation for Chrome (Android) and Mobile Safari, making it suitable for testing responsive applications across desktop and mobile viewports without separate tooling.
Prerequisites
- Node.js 20.x, 22.x, or 24.x
- Operating system: Windows 11+, macOS 14+, or Ubuntu 22.04+/Debian 12+
- Package manager: npm, yarn, or pnpm
- At least 2GB disk space for browser binaries
Quick start
# Initialize new Playwright project
npm init playwright@latest
# Run tests
npx playwright test
# Open HTML report
npx playwright show-report
Basic test structure:
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://playwright.dev/');
await expect(page).toHaveTitle(/Playwright/);
await page.getByRole('link', { name: 'Get started' }).click();
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
Core concepts
Test fixtures: Built-in dependency injection system providing page, context, browser objects with automatic cleanup.
Locators: Lazy selectors that auto-wait and retry. Created with page.locator(), page.getByRole(), page.getByText() etc.
Web-first assertions: Async assertions like expect(locator).toBeVisible() that automatically wait and retry until conditions are met.
Browser contexts: Isolated browser sessions with separate cookies, storage, and cache. Multiple contexts can share a browser instance.
Projects: Configuration units in playwright.config.ts that define different browser/environment combinations for test execution.
Key API surface
Page navigation:
await page.goto(url, { waitUntil: 'networkidle' });
await page.goBack();
await page.reload();
Element interaction:
await page.click(selector);
await page.fill(selector, text);
await page.selectOption(selector, value);
await page.check(selector);
Locator strategies:
page.locator(selector)
page.getByRole(role, options)
page.getByText(text)
page.getByLabel(text)
page.getByPlaceholder(text)
page.getByTestId(testId)
Assertions:
await expect(page).toHaveTitle(pattern);
await expect(locator).toBeVisible();
await expect(locator).toHaveText(text);
await expect(locator).toHaveCount(count);
Common patterns
Page Object Model:
export class LoginPage {
constructor(private page: Page) {}
async login(username: string, password: string) {
await this.page.getByLabel('Username').fill(username);
await this.page.getByLabel('Password').fill(password);
await this.page.getByRole('button', { name: 'Sign in' }).click();
}
}
API testing with request fixture:
test('api and ui', async ({ request, page }) => {
const response = await request.post('/api/login', {
data: { username: 'user', password: 'pass' }
});
const token = await response.json();
await page.goto('/dashboard');
await page.evaluate(token => {
localStorage.setItem('auth_token', token);
}, token.access_token);
});
Custom fixtures:
export const test = base.extend<{ todoPage: TodoPage }>({
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await use(todoPage);
},
});
Mobile testing:
test('mobile viewport', async ({ browser }) => {
const context = await browser.newContext({
...devices['iPhone 13'],
});
const page = await context.newPage();
await page.goto('/');
});
Network interception:
await page.route('/api/**', async route => {
const response = await route.fetch();
const json = await response.json();
json.modified = true;
await route.fulfill({ response, json });
});
Configuration
Essential playwright.config.ts options:
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://127.0.0.1:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
Environment variables:
PWDEBUG=1- Debug mode with browser openHEADED=1- Run in headed modeCI=true- Enables CI-optimized settings
Best practices
Use data-testid for stable selectors: Prefer getByTestId('submit-button') over CSS selectors that may change.
Leverage auto-waiting: Don't use setTimeout() or manual waits. Web-first assertions handle timing automatically.
Isolate test data: Use test.beforeEach() to set up clean state or generate unique test data per test.
Use Page Object Models: Encapsulate page interactions to reduce duplication and improve maintainability.
Test API and UI together: Use request fixture to set up data via API, then test UI interactions.
Enable trace on retry: Set trace: 'on-first-retry' to capture traces only when tests fail and retry.
Run tests in parallel: Enable fullyParallel: true for faster execution with proper test isolation.
Use soft assertions: expect.soft() continues test execution after assertion failures to gather more information.
Gotchas and common mistakes
Auto-waiting doesn't apply to all actions: page.evaluate() and direct DOM manipulation bypass Playwright's waiting mechanisms.
Locators are lazy: const button = page.locator('button') doesn't search immediately. The search happens when you interact or assert.
Context isolation: Cookies, localStorage, and sessions don't persist between test files unless explicitly configured.
Network idle timing: waitUntil: 'networkidle' waits for 500ms of no network requests, which may be too aggressive for dynamic sites.
Mobile emulation limitations: Device emulation simulates viewport and user agent but doesn't run actual mobile browsers.
Trace files grow large: Disable traces in production CI to avoid storage issues. Use trace: 'retain-on-failure' instead.
Screenshot timing: Screenshots are taken immediately, not after animations complete. Use await expect(locator).toBeVisible() first.
Headless vs headed differences: Some behaviors (like file downloads) work differently between modes.
Browser context cleanup: Custom contexts created with browser.newContext() must be manually closed to prevent resource leaks.
Selector specificity: getByRole('button') may match multiple elements. Use additional options like { name: 'Submit' } to narrow selection.