Playwright Best Practices
This chapter summarizes the most important best practices in writing and maintaining Playwright tests, 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 code implementation details.
For example, tests should verify that the correct text is displayed on the page, buttons are clickable, and submitting the form navigates to the correct page, rather than verifying the return value of a JavaScript function or the internal structure of the DOM.
Example
expect(await page.evaluate(() => window.__store.getState().user.name))
.toBe('example');
// Recommended: testing what users see
await expect(page.getByText('Welcome, example')).toBeVisible();
Test Isolation
Each test should becompletely independentfrom other tests, and not depend on the state of a previous test.
Playwright provides an independent context by default, but you should also ensure that you don't rely on the execution order of tests.
Example
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 the result of the previous test
});
// 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 | Applicable scenario |
|---|---|---|
| 1 | getByRole() | Element has a clear ARIA role |
| 2 | getByLabel() | Form element associated with a label |
| 3 | getByPlaceholder() | Input has a placeholder |
| 4 | getByText() | Element has explicit text |
| 5 | getByAltText() | Image has alt attribute |
| 6 | getByTitle() | Element has title attribute |
| 7 | getByTestId() | Fallback |
| 8 | locator() | CSS/XPath, last resort |
Try to avoid using CSS class names as locators.
Class names are prone to change due to style refactoring, causing large-scale test failures.
Avoid Unnecessary Waits
Leverage Playwright's auto-wait mechanism, avoid hand-codingsleeporwaitForTimeout。
Example
await page.waitForTimeout(3000);
await page.getByText('Data loading complete').click();
// Recommended: rely on auto-waiting and assertions
await expect(page.getByText('Loading data...')).toBeHidden({ timeout: 10000 });
await page.getByText('Data loading complete').click();
Don't Test Third-Party Services
Only test what you can control; don't depend on the availability of third-party services.
Example
await page.goto('https://www.example.com/');
// The 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 instead of stopping at the first failure.
Example
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 module | login.spec.ts、checkout.spec.tswait |
| Use describe to group | Put related tests intest.describe()Medium |
| Use beforeEach well | Extract repeated navigation and setup steps intobeforeEach |
| Descriptive test names | Good names explain "what was done" and "what is expected" |
| Extract common code | Helpers used in more than 3 files should be extracted into a shared module |
| Don't over-DRY | 2-3 lines of repetition are easier to maintain than over-abstraction |
Common Troubleshooting
Flaky Tests
Causes and solutions for flaky tests:
| Cause | Solution |
|---|---|
| Hardcoded sleep is not long enough | Use auto-wait mechanism instead of fixed waits |
| Depending on third-party services | Useroute()Mock external requests |
| Animations cause element position changes | In the configurationanimations: 'disabled' |
| Data residue between tests | Ensure each test independently creates and cleans up data |
| Time-related logic | Usepage.clockFixed time |
Locator Cannot Find Element
Troubleshooting steps:
- Confirm whether the element is in the DOM (use
toBeAttached()rather thantoBeVisible()) - Confirm whether the element is in the viewport
- Confirm whether it is nested in an iframe
- Use Pick Locator in UI Mode to check the locator
- Check whether there are multiple matching elements (Locator requires strict mode by default)