Playwright Best Practices
This chapter summarizes the most important best practices during Playwright test writing and maintenance, helping you write stable and maintainable tests.
* * *
## Testing Philosophy
### Test User-Visible Behavior
Automated tests should verify what end users can see and interact with, rather than verifying implementation details.
For example, tests should verify that correct text is displayed on the page, buttons are clickable, and forms redirect to the correct page after submission, rather than verifying the return value of a JavaScript function or the internal structure of the DOM.
## Examples
// Not recommended: Testing implementation details
expect(await page.evaluate(()=> window.__store.getState().user.name))
.toBe('tutorial');
// Recommended: Testing what the user sees
await expect(page.getByText('Welcome, tutorial')).toBeVisible();
* * *
## Test Isolation
Each test should be completely independent from other tests and not rely on the state of the previous test.
Playwright provides isolated Contexts by default, but you should also ensure you don't depend on test execution order.
## Examples
// Not recommended: Tests depend on each other
let createdId;
test('Create resource', async ({ request })=>{
const resp = await request.post('/api/items');
createdId =(await resp.json()).id;// Shared state
});
test('Use created resource', async ({ page })=>{
await page.goto(`/items/${createdId}`);// Depends on previous test result
});
// Recommended: Each test is self-sufficient
test('Create and use resource', async ({ request, page })=>{
const resp = await request.post('/api/items');
const id =(await resp.json()).id;
await page.goto(`/items/${id}`);
await expect(page.getByText('Resource Details')).toBeVisible();
});
* * *
## Locator Priority Principles
Using Locators in the recommended order can improve test stability.
| Priority | Method | Use Case |
| --- | --- | --- |
| 1 | `getByRole()` | Element has explicit ARIA role |
| 2 | `getByLabel()` | Form element associated with label |
| 3 | `getByPlaceholder()` | Input has placeholder |
| 4 | `getByText()` | Element has explicit text |
| 5 | `getByAltText()` | Image has alt attribute |
| 6 | `getByTitle()` | Element has title attribute |
| 7 | `getByTestId()` | Fallback solution |
| 8 | `locator()` | CSS/XPath, last resort |
> Try not to use CSS class names as locators.
>
> Class names are prone to change due to style refactoring, causing large-scale test failures.
* * *
## Avoid Unnecessary Waiting
Take advantage of Playwright's auto-waiting mechanism and avoid manually writing `sleep` or `waitForTimeout`.
## Examples
// Not recommended: Hardcoded wait
await page.waitForTimeout(3000);
await page.getByText('Data loaded').click();
// Recommended: Rely on auto-waiting and assertions
await expect(page.getByText('Loading data...')).toBeHidden({ timeout:10000});
await page.getByText('Data loaded').click();
* * *
## Don't Test Third-Party Services
Only test what you can control, and don't rely on the availability of third-party services.
## Examples
// Not recommended: Rely on external CDN or API
await page.goto('https://example.com/');
// Page may load Google Analytics, external fonts, etc.
// Recommended: Intercept third-party requests, mock external services
await page.route('**/*analytics*', route => route.abort());
await page.route('**/external-api/**', route =>{
route.fulfill({ json:{ status:'ok'}});
});
* * *
## Use expect.soft() Soft Assertions
When checking multiple independent conditions, soft assertions can report all failures at once, rather than stopping at the first failure.
## Examples
// Recommended: Soft assertion checks all fields at once
await expect.soft(page.getByLabel('Username')).toBeVisible();
await expect.soft(page.getByLabel('Password')).toBeVisible();
await expect.soft(page.getByLabel('Email')).toBeVisible();
await expect.soft(page.getByRole('button',{ name:'Register'})).toBeEnabled();
// If multiple fields are missing, report all at once
* * *
## Organize Test Files Reasonably
| Practice | Description |
| --- | --- |
| Split files by functional modules | `login.spec.ts`, `checkout.spec.ts`, etc. |
| Use describe for grouping | Put related tests in `test.describe()` |
| Use beforeEach well | Extract repeated navigation and preparation steps to `beforeEach` |
| Descriptive test names | Good names explain "what was done" and "what was expected" |
| Extract common code | Extract helpers used by more than 3 files to shared modules |
| Don't over-DRY | 2-3 lines of duplication is more maintainable than over-abstraction |
* * *
## Troubleshooting Common Issues
### Flaky Tests
Causes and solutions for unstable tests:
| Cause | Solution |
| --- | --- |
| Hardcoded sleep not long enough | Use auto-waiting mechanism instead of fixed waits |
| Rely on third-party services | Use `route()` to mock external requests |
| Animation causes element position changes | Set `animations: 'disabled'` in configuration |
| Data residue between tests | Ensure each test creates and cleans up its own data |
| Time-related logic | Use `page.clock` to fix time |
### Locator Can't Find Element
Troubleshooting steps:
* Confirm if element is in DOM (use `toBeAttached()` instead of `toBeVisible()`)
* Confirm if element is in viewport
* Confirm if it's nested in an iframe
* Use Pick Locator in UI Mode to check the locator
* Check if there are multiple matching elements (Locator requires strict mode by default)
YouTip