Skip to main content
playwright-best-practices Core patterns for writing maintainable, reliable Playwright tests with TypeScript. Use when writing new tests, refactoring existing ones, or reviewing test quality. Covers test structure, selectors, waits, assertions, page objects, fixtures, and common anti-patterns.
Zur Installation springen Skills Marktplatz Entdecken und erkunden Sie KI-Skills, die von der Community erstellt wurden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Prompt kopierenPrompt-Details anzeigen Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
npx skills add https://github.com/AbhiRKeesara/test-automation-skills --skill playwright-best-practicesDer Befehl bleibt in einer Zeile. Scrollen Sie horizontal, um ihn vor dem Kopieren vollständig zu prüfen.
Sie bevorzugen eine lokale Kopie? Laden Sie die Dateien herunter, die SkillsMP derzeit vorliegen.
ZIP herunterladen Herunterladen... Mehr aus diesem Repository Accessibility (a11y) testing patterns with Playwright and axe-core. Use when adding WCAG 2.1 compliance checks, keyboard navigation testing, screen reader compatibility, or color contrast validation to your test suite.
Storage state reuse, 2FA/TOTP testing, multi-role auth, session management, OAuth flows, and secure credential handling in Playwright
GitHub Actions setup, parallel execution, sharding, test filtering, artifact management, and pipeline optimization for Playwright tests
Verwandte Berufe SOC
Basierend auf der SOC-Berufsklassifikation
name playwright-best-practices description Core patterns for writing maintainable, reliable Playwright tests with TypeScript. Use when writing new tests, refactoring existing ones, or reviewing test quality. Covers test structure, selectors, waits, assertions, page objects, fixtures, and common anti-patterns.
Playwright Best Practices Skill
This skill contains comprehensive best practices for writing maintainable, reliable Playwright tests with TypeScript.
Core Principles
Tests should be reliable - No flaky tests
Tests should be fast - Parallel execution, efficient waits
Tests should be maintainable - Clear structure, reusable code
Tests should be readable - Self-documenting, clear intent
Tests should be isolated - No dependencies between tests
Table of Contents
Test Structure
✅ Good Practice
import { test, expect } from '@playwright/test' ;
import { LoginPage } from '../page-objects/LoginPage' ;
test. ( , {
( , ({ page }) => {
loginPage = (page);
loginPage. ();
loginPage. ( , );
(page). ( );
(page. ( , { : })). ();
});
( , ({ page }) => {
loginPage = (page);
loginPage. ();
loginPage. ( , );
(loginPage. ). ();
(loginPage. ). ( );
});
});
describe
'User Authentication'
() =>
test
'should login successfully with valid credentials'
async
const
new
LoginPage
await
navigate
await
login
'user@example.com'
'SecurePass123'
await
expect
toHaveURL
/.*dashboard/
await
expect
getByRole
'heading'
name
'Welcome'
toBeVisible
test
'should show error message with invalid credentials'
async
const
new
LoginPage
await
navigate
await
login
'user@example.com'
'WrongPassword'
await
expect
errorMessage
toBeVisible
await
expect
errorMessage
toHaveText
'Invalid credentials'
❌ Bad Practice
test ('test 1' , async ({ page }) => {
await page.goto ('https://example.com/login' );
await page.waitForTimeout (2000 );
await page.locator ('#user' ).fill ('user@example.com' );
await page.locator ('#pass' ).type ('password' );
await page.locator ('button' ).click ();
await page.waitForTimeout (3000 );
});
Why the good practice is better:
✅ Descriptive test names explain what's being tested
✅ Uses page objects for reusability
✅ Proper assertions verify expected outcomes
✅ No hard-coded waits
✅ Tests are grouped logically in describe blocks
Selectors
Priority Order (Use in this order)
getByRole - Accessibility-first, semantic
getByLabel - Form elements with labels
getByPlaceholder - Form elements with placeholders
getByText - Text content (use sparingly)
getByTestId - Last resort for complex elements
✅ Good Practice
await page.getByRole ('button' , { name : 'Submit' }).click ();
await page.getByRole ('textbox' , { name : 'Email' }).fill ('user@example.com' );
await page.getByRole ('heading' , { name : 'Dashboard' }).waitFor ();
await page.getByLabel ('Password' ).fill ('SecurePass123' );
await page.getByLabel ('Remember me' ).check ();
await page.getByPlaceholder ('Enter your email' ).fill ('user@example.com' );
await page.getByTestId ('prescription-card-123' ).click ();
❌ Bad Practice
await page.locator ('#submit-btn' ).click ();
await page.locator ('.form-input[name="email"]' ).fill ('user@example.com' );
await page.locator ('div > div > button:nth-child(2)' ).click ();
await page.locator ('//div[@class="container"]/button[1]' ).click ();
await page.locator ('button' ).click ();
await page.locator ('input' ).fill ('text' );
Why the good practice is better:
✅ Accessible selectors work with screen readers
✅ Resilient to CSS/structure changes
✅ Self-documenting (you know what element you're targeting)
✅ Follows web accessibility standards
Selector Patterns
await page
.getByRole ('main' )
.getByRole ('button' , { name : 'Add to Cart' })
.click ();
await page
.getByRole ('listitem' )
.filter ({ hasText : 'Prescription #123' })
.getByRole ('button' , { name : 'Refill' })
.click ();
const firstPrescription = page.getByRole ('article' ).first ();
const lastPrescription = page.getByRole ('article' ).last ();
const thirdPrescription = page.getByRole ('article' ).nth (2 );
Waits and Timing
✅ Good Practice
await page.getByRole ('button' , { name : 'Submit' }).click ();
await expect (page.getByText ('Success' )).toBeVisible ();
await page.getByRole ('progressbar' ).waitFor ({ state : 'hidden' });
await page.getByRole ('alert' ).waitFor ({ state : 'visible' , timeout : 10000 });
await page.waitForLoadState ('networkidle' );
await page.waitForResponse (resp =>
resp.url ().includes ('/api/prescriptions' ) && resp.status () === 200
);
❌ Bad Practice
await page.waitForTimeout (2000 );
await page.waitForTimeout (5000 );
await page.click ('button' );
await new Promise (resolve => setTimeout (resolve, 3000 ));
Why the good practice is better:
✅ Playwright has built-in auto-waiting
✅ Tests run as fast as possible
✅ No unnecessary delays
✅ Clear intent (waiting for what?)
Assertions
✅ Good Practice
await expect (page.getByRole ('heading' , { name : 'Dashboard' })).toBeVisible ();
await expect (page.getByRole ('alert' )).toBeHidden ();
await expect (page.getByRole ('status' )).toHaveText ('Order confirmed' );
await expect (page.getByRole ('heading' )).toContainText ('Welcome' );
await expect (page).toHaveURL (/.*dashboard/ );
await expect (page).toHaveURL ('https://example.com/prescriptions' );
await expect (page.getByRole ('button' , { name : 'Submit' })).toBeEnabled ();
await expect (page.getByRole ('button' , { name : 'Delete' })).toBeDisabled ();
await expect (page.getByRole ('checkbox' )).toBeChecked ();
await expect (page.getByRole ('listitem' )).toHaveCount (5 );
await expect (page.getByRole ('img' , { name : 'Logo' })).toHaveAttribute ('alt' , 'Company Logo' );
❌ Bad Practice
await page.click ('button' );
const text = await page.locator ('.message' ).textContent ();
expect (text).toBe ('Success' );
const button = await page.$('button' );
expect (button).not .toBeNull ();
Why the good practice is better:
✅ Auto-retrying assertions (handles timing issues)
✅ Clear, readable intent
✅ Better error messages
✅ Verifies actual user-visible behavior
Page Objects
✅ Good Practice
import { Page , Locator } from '@playwright/test' ;
export class LoginPage {
readonly page : Page ;
readonly emailInput : Locator ;
readonly passwordInput : Locator ;
readonly submitButton : Locator ;
readonly errorMessage : Locator ;
constructor (page : Page ) {
this .page = page;
this .emailInput = page.getByLabel ('Email' );
this .passwordInput = page.getByLabel ('Password' );
this .submitButton = page.getByRole ('button' , { name : 'Sign In' });
this .errorMessage = page.getByRole ('alert' );
}
async navigate ( ) {
await this .page .goto ('/login' );
}
async login (email : string , password : string ) {
await this .emailInput .fill (email);
await this .passwordInput .fill (password);
await this .submitButton .click ();
}
async loginWithEnter (email : string , password : string ) {
await this .emailInput .fill (email);
await this .passwordInput .fill (password);
await this .passwordInput .press ('Enter' );
}
async getErrorText (): Promise <string > {
return await this .errorMessage .textContent () || '' ;
}
}
Using the Page Object
import { test, expect } from '@playwright/test' ;
import { LoginPage } from '../../page-objects/LoginPage' ;
test ('successful login' , async ({ page }) => {
const loginPage = new LoginPage (page);
await loginPage.navigate ();
await loginPage.login ('user@example.com' , 'SecurePass123' );
await expect (page).toHaveURL (/.*dashboard/ );
});
❌ Bad Practice
test ('login test 1' , async ({ page }) => {
await page.goto ('/login' );
await page.locator ('#email' ).fill ('user@example.com' );
await page.locator ('#password' ).fill ('pass' );
await page.locator ('button' ).click ();
});
test ('login test 2' , async ({ page }) => {
await page.goto ('/login' );
await page.locator ('#email' ).fill ('other@example.com' );
await page.locator ('#password' ).fill ('pass' );
await page.locator ('button' ).click ();
});
Why the good practice is better:
✅ Single source of truth for selectors
✅ Reusable across tests
✅ Easy to maintain (change in one place)
✅ Type-safe with TypeScript
✅ Encapsulates page-specific logic
Fixtures
✅ Good Practice
import { test as base } from '@playwright/test' ;
import { LoginPage } from '../page-objects/LoginPage' ;
type AuthFixtures = {
loginPage : LoginPage ;
authenticatedPage : Page ;
};
export const test = base.extend <AuthFixtures >({
loginPage : async ({ page }, use) => {
const loginPage = new LoginPage (page);
await use (loginPage);
},
authenticatedPage : async ({ page }, use) => {
const loginPage = new LoginPage (page);
await loginPage.navigate ();
await loginPage.login ('user@example.com' , 'SecurePass123' );
await page.waitForURL (/.*dashboard/ );
await use (page);
},
});
export { expect } from '@playwright/test' ;
Using Fixtures import { test, expect } from '../fixtures/auth.fixture' ;
test ('user can view prescriptions' , async ({ authenticatedPage }) => {
await authenticatedPage.getByRole ('link' , { name : 'Prescriptions' }).click ();
await expect (authenticatedPage).toHaveURL (/.*prescriptions/ );
});
✅ Shared setup/teardown logic
✅ Better test isolation
✅ Composable and reusable
✅ Automatic cleanup
Test Organization
✅ Good Practice tests/
├── auth/
│ ├── login.spec.ts
│ ├── logout.spec.ts
│ ├── password-reset.spec.ts
│ └── registration.spec.ts
├── pharmacy/
│ ├── prescription-flow/
│ │ ├── create-prescription.spec.ts
│ │ ├── refill-prescription.spec.ts
│ │ └── cancel-prescription.spec.ts
│ └── medication-search.spec.ts
├── patient-portal/
│ ├── appointments.spec.ts
│ └── medical-records.spec.ts
└── billing/
├── payment-methods.spec.ts
└── invoices.spec.ts
File Naming
✅ feature-name.spec.ts - Descriptive, lowercase with dashes
❌ test1.ts - Not descriptive
❌ TestLoginFeature.spec.ts - Not using kebab-case
Test Naming
test ('should display error when email is invalid' , async ({ page }) => {});
test ('should allow user to refill prescription with one click' , async ({ page }) => {});
test ('test1' , async ({ page }) => {});
test ('it works' , async ({ page }) => {});
Error Handling
✅ Good Practice test ('should handle network errors gracefully' , async ({ page }) => {
await page.route ('**/api/prescriptions' , route => route.abort ());
await page.getByRole ('button' , { name : 'Load Prescriptions' }).click ();
await expect (page.getByRole ('alert' )).toHaveText (
'Unable to load prescriptions. Please try again.'
);
});
test ('should retry failed API calls' , async ({ page }) => {
let attemptCount = 0 ;
await page.route ('**/api/data' , route => {
attemptCount++;
if (attemptCount < 3 ) {
route.abort ();
} else {
route.continue ();
}
});
await page.goto ('/dashboard' );
await expect (page.getByRole ('main' )).toBeVisible ();
});
Common Anti-Patterns
❌ Anti-Pattern 1: Hard-Coded Waits await page.click ('button' );
await page.waitForTimeout (5000 );
Fix: Use auto-waiting or explicit waits
await page.getByRole ('button' ).click ();
await expect (page.getByText ('Success' )).toBeVisible ();
❌ Anti-Pattern 2: Brittle CSS Selectors await page.locator ('div.container > div:nth-child(3) > button' ).click ();
Fix: Use accessible selectors
await page.getByRole ('button' , { name : 'Submit' }).click ();
❌ Anti-Pattern 3: No Page Objects
test ('test1' , async ({ page }) => {
await page.locator ('#email' ).fill ('test@example.com' );
await page.locator ('#password' ).fill ('pass' );
});
test ('test2' , async ({ page }) => {
await page.locator ('#email' ).fill ('other@example.com' );
await page.locator ('#password' ).fill ('pass' );
});
const loginPage = new LoginPage (page);
await loginPage.login ('test@example.com' , 'pass' );
❌ Anti-Pattern 4: Tests Depend on Each Other let userId : string ;
test ('create user' , async ({ request }) => {
const response = await request.post ('/users' , { data : {...} });
userId = (await response.json ()).id ;
});
test ('update user' , async ({ request }) => {
await request.patch (`/users/${userId} ` , { data : {...} });
});
Fix: Each test should be independent
test ('update user' , async ({ request }) => {
const createResponse = await request.post ('/users' , { data : {...} });
const userId = (await createResponse.json ()).id ;
await request.patch (`/users/${userId} ` , { data : {...} });
});
❌ Anti-Pattern 5: Testing Implementation Details test ('state is set correctly' , async ({ page }) => {
await page.evaluate (() => {
return window .__app__ .state .isLoggedIn === true ;
});
});
Fix: Test user-visible behavior
test ('user sees dashboard after login' , async ({ page }) => {
const loginPage = new LoginPage (page);
await loginPage.login ('user@example.com' , 'pass' );
await expect (page).toHaveURL (/.*dashboard/ );
await expect (page.getByRole ('heading' , { name : 'Dashboard' })).toBeVisible ();
});
Quick Reference Checklist Before committing a test, verify:
Pre-Test Completion Checklist Before considering a test done, verify all of these (inspired by agentskills.io ):
Before Writing
Test Structure
Selectors & Interactions
Assertions & Data
Validation
Related Resources