PlaywrightTestingBest Practices

Playwright Best Practices for Reliable Tests

Learn the essential Playwright patterns and practices that make tests more reliable, maintainable, and faster to run.

4 min read

Playwright is a powerful testing framework, but without proper patterns, tests can become brittle and hard to maintain. Here are the best practices we've learned from running thousands of AI-generated tests.

1. Selector Strategy

The order of selector priority matters:

// Best: Semantic, accessible selectors await page.getByRole('button', { name: 'Submit' }) await page.getByLabel('Email address') await page.getByPlaceholder('Enter your email') // Okay: Text content (can change with copy updates) await page.getByText('Submit') // Avoid: CSS selectors (brittle) await page.locator('.btn-primary') await page.locator('#submit-button')

Why? Role-based selectors mirror how users interact with your app and are more resilient to UI changes.

2. Proper Waiting Patterns

Never use arbitrary timeouts:

// Bad: Arbitrary wait await page.waitForTimeout(3000) // Good: Wait for specific conditions await page.waitForLoadState('networkidle') await page.getByRole('button').waitFor({ state: 'visible' }) await expect(page.getByText('Success')).toBeVisible()

Pro tip: Most Playwright actions auto-wait, so explicit waits are often unnecessary.

3. Page Object Pattern

Organize tests with page objects:

class LoginPage { constructor(private page: Page) {} async login(email: string, password: string) { await this.page.getByLabel('Email').fill(email) await this.page.getByLabel('Password').fill(password) await this.page.getByRole('button', { name: 'Sign In' }).click() } async expectLoggedIn() { await expect(this.page.getByText('Welcome back')).toBeVisible() } }

This keeps tests DRY and easier to maintain.

4. Test Isolation

Each test should be independent:

// Good: Fresh state per test test('user can add item to cart', async ({ page }) => { await loginAsUser(page) // Fresh login await addItemToCart(page) await expect(page.getByText('1 item')).toBeVisible() }) // Bad: Tests depend on each other test.describe.serial('checkout flow', () => { test('login', async ({ page }) => { /* ... */ }) test('add to cart', async ({ page }) => { /* ... */ }) // Assumes logged in })

5. Assertions Best Practices

Make assertions specific and meaningful:

// Weak assertion await expect(page.locator('div')).toBeVisible() // Strong assertion await expect(page.getByRole('alert')).toContainText('Order confirmed') await expect(page.getByRole('status')).toHaveAttribute('aria-live', 'polite')

6. Error Handling

Handle expected errors gracefully:

// Test error states explicitly test('shows error on invalid email', async ({ page }) => { await page.getByLabel('Email').fill('invalid-email') await page.getByRole('button', { name: 'Submit' }).click() await expect(page.getByRole('alert')).toContainText('Invalid email') })

7. Network Mocking

Mock external dependencies:

test('loads user data', async ({ page }) => { // Mock API response await page.route('**/api/user', async (route) => { await route.fulfill({ status: 200, body: JSON.stringify({ name: 'Test User' }), }) }) await page.goto('/dashboard') await expect(page.getByText('Test User')).toBeVisible() })

8. Mobile Testing

Test responsive designs:

test('mobile menu works', async ({ page }) => { // Set mobile viewport await page.setViewportSize({ width: 375, height: 667 }) await page.goto('/') await page.getByRole('button', { name: 'Menu' }).click() await expect(page.getByRole('navigation')).toBeVisible() })

9. Screenshot and Video on Failure

Configure automatic debugging aids:

// playwright.config.ts export default defineConfig({ use: { screenshot: 'only-on-failure', video: 'retain-on-failure', trace: 'retain-on-failure', }, })

10. Parallel Execution

Run tests faster with parallelization:

// playwright.config.ts export default defineConfig({ workers: process.env.CI ? 2 : undefined, fullyParallel: true, })

How Noet Helps

Noet automatically implements these best practices:

  • Smart selectors: AI chooses the most resilient selector strategy
  • Auto-waits: Proper waiting logic is built into generated code
  • Self-healing: When UI changes, tests adapt automatically
  • Isolation: Each generated test runs independently

Instead of memorizing all these patterns, just describe your test in plain English and let Noet generate code that follows best practices automatically.

Conclusion

Playwright is powerful, but following these patterns makes tests:

  • More reliable - fewer flaky failures
  • Easier to maintain - changes don't break everything
  • Faster to run - proper waits and parallelization
  • Better documented - clear, semantic selectors

Start applying these practices today, or let Noet handle them for you automatically.

Create tests from user stories in minutes

Transform your Jira stories into comprehensive test suites in without manual setup or coding today.