| name | playwright |
| description | [Applies to: **/*.{js,ts}] Definitive guidelines for writing robust, maintainable, and high-quality end-to-end tests with Playwright in TypeScript. |
| source | cursor_mdc |
Playwright Best Practices
Playwright is the gold standard for reliable E2E testing. These rules ensure your tests are fast, stable, and easy to maintain, aligning with modern 2025 development standards for reliability, quality, and structure.
1. Always Use @playwright/test
Leverage the official test runner for built-in fixtures, isolation, and web-first assertions. Avoid the low-level playwright library for E2E tests.
โ BAD: Using playwright directly
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage();
await browser.close();
โ
GOOD: Using @playwright/test
import { test, expect } from '@playwright/test';
test('should navigate to home', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/Home/);
});
2. Prioritize Robust Locators
Use Playwright's built-in Locators API, favoring user-facing attributes over brittle CSS selectors. This drastically improves test stability.
โ BAD: Fragile, implementation-dependent selectors
await page.locator('div.container > ul > li:nth-child(2) > button').click();
โ
GOOD: Semantic, user-facing locators
await page.getByRole('button', { name: 'Add to Cart' }).click();
await page.getByLabel('Username').fill('testuser');
await page.getByTestId('product-item-123').click();
3. Embrace Web-First Assertions
Playwright's expect assertions automatically retry until conditions are met, eliminating manual waits and flakiness. Never use page.waitForTimeout().
โ BAD: Manual, flaky waits and generic assertions
await page.waitForTimeout(2000);
const title = await page.title();
assert.equal(title, 'My Page');
โ
GOOD: Reliable, auto-retrying assertions
await expect(page).toHaveTitle(/My Page/);
await expect(page.getByText('Welcome')).toBeVisible();
await expect(page.getByRole('checkbox')).toBeChecked();
4. Implement the Page Object Model (POM)
Encapsulate selectors and actions within dedicated classes. This improves readability, reusability, and maintainability.
โ BAD: Repeated selectors and logic across tests
await page.getByLabel('Username').fill('user');
await page.getByLabel('Password').fill('pass');
await page.getByRole('button', { name: 'Login' }).click();
โ
GOOD: Centralized Page Object
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.getByLabel('Username');
this.passwordInput = page.getByLabel('Password');
this.loginButton = page.getByRole('button', { name: 'Login' });
}
async navigate() {
await this.page.goto('/login');
}
async login(username: , : ) {
..(username);
..(password);
..();
}
}
{ test, expect } ;
{ } ;
(, ({ page }) => {
loginPage = (page);
loginPage.();
loginPage.(, );
(page).();
});
5. Optimize Performance with Auth State & Route Blocking
Reduce test execution time by reusing authenticated sessions and blocking unnecessary network requests.
โ BAD: Logging in for every test and loading all assets
test('view profile', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Username').fill('user');
await page.getByLabel('Password').fill('pass');
await page.getByRole('button', { name: 'Login' }).click();
await page.goto('/profile');
});
โ
GOOD: Reusing auth state and blocking requests
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
storageState: 'playwright-auth.json',
},
});
import { chromium, expect } from '@playwright/test';
export default async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto('/login');
await page.getByLabel('Username').fill('testuser');
await page.getByLabel('Password').fill('password');
await page.getByRole('button', { name: 'Login' }).click();
await expect(page).toHaveURL(/dashboard/);
page.().({ : });
browser.();
}
{ test, expect } ;
(, ({ page, context }) => {
context.(, route.());
page.();
(page.()).();
});
6. Mock APIs for Deterministic Tests
Isolate your UI tests from backend flakiness by intercepting and mocking API responses.
โ BAD: Relying on a live, potentially unstable backend
test('display products', async ({ page }) => {
await page.goto('/products');
await expect(page.getByText('Product A')).toBeVisible();
});
โ
GOOD: Mocking API responses
test('display mocked products', async ({ page }) => {
await page.route('**/api/products', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Mock Product' }]),
});
});
await page.goto('/products');
await expect(page.getByText('Mock Product')).toBeVisible();
});
7. Leverage CI/CD Features for Debugging
Configure tracing, screenshots, and video recording in your playwright.config.ts to instantly diagnose failures in CI.
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [['html'], ['list']],
use: {
trace: 'on-first-retry',
screenshot: 'on',
video: 'on-first-retry',
},
});
8. Maintain Code Quality with Linters & Formatters
Integrate ESLint (with Playwright plugin) and Prettier into your workflow via pre-commit hooks to enforce consistent code style and catch errors early.
{
"scripts": {
"lint": "eslint . --ext .ts",
"format": "prettier --write .",
"test:e2e": "playwright test"
},
"devDependencies": {
"@playwright/test": "^1.x.x",
"@typescript-eslint/eslint-plugin": "^7.x.x",
"@typescript-eslint/parser": "^7.x.x",
"eslint": "^8.x.x",
"eslint-plugin-playwright": "^1.x.x",
"prettier": "^3.x.x",
"husky": "^9.x.x",
"lint-staged": "^15.x.x"
}
}
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint', 'playwright'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:playwright/recommended',
],
rules: {
},
env: {
node: true,
browser: true,
},
};
{
"singleQuote": true,
"semi": true,
"trailingComma": "all"
}
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
module.exports = {
'*.{ts,js}': ['eslint --fix', 'prettier --write'],
};