Verification', () => {
test('validate header and dynamic status message', async ({ page }) => {
await page.goto('/app/dashboard');
// Semantic locator for the main heading
const header = page.getByRole('heading', { name: 'System Overview' });
// Locator for a status banner that appears after load
const statusBanner = page.getByTestId('system-status');
// Verify exact text match with auto-retry
await expect(header).toHaveText('System Overview');
// Verify partial text for dynamic content
await expect(statusBanner).toContainText('All systems operational');
});
});
**Rationale:**
- `getByRole` targets accessibility attributes, making tests resilient to style changes.
- `toHaveText` ensures the entire content matches, preventing false positives from substring matches.
- `toContainText` is ideal for messages where timestamps or IDs might vary.
#### 2. Secure Login Flow with Error Handling
Login tests must cover both success and failure paths. This includes filling credentials, handling validation errors, and verifying navigation upon success.
**Implementation Pattern:**
Use `fill` for inputs, `click` for submission, and verify error states or URL changes.
```typescript
import { test, expect } from '@playwright/test';
test.describe('Authentication Module', () => {
test('handle invalid credentials and verify error state', async ({ page }) => {
await page.goto('/auth/login');
const userField = page.getByLabel('Username');
const passField = page.getByLabel('Password');
const submitBtn = page.getByRole('button', { name: 'Sign In' });
const errorAlert = page.getByRole('alert');
// Fill and verify input value
await userField.fill('test_user');
await expect(userField).toHaveValue('test_user');
// Clear and refill to test field behavior
await userField.clear();
await expect(userField).toHaveValue('');
await userField.fill('admin');
await passField.fill('wrong_password');
await submitBtn.click();
// Verify error message appears
await expect(errorAlert).toHaveText('Invalid username or password.');
});
test('successful login redirects to workspace', async ({ page }) => {
await page.goto('/auth/login');
await page.getByLabel('Username').fill('admin');
await page.getByLabel('Password').fill('secure_pass_123');
await page.getByRole('button', { name: 'Sign In' }).click();
// Verify navigation and workspace content
await expect(page).toHaveURL('/app/workspace');
await expect(page.getByText('Welcome back, Admin')).toBeVisible();
});
});
Rationale:
- Testing
clear and fill ensures input fields behave correctly under user manipulation.
- Verifying the URL and post-login content confirms the full authentication cycle.
- Using
getByLabel links inputs to their accessible labels, improving test stability.
3. Checkbox and Radio Button Interactions
Toggle controls require precise state management. Checkboxes allow multiple selections, while radio buttons enforce mutual exclusion. Tests must verify checked states, disabled states, and batch operations.
Implementation Pattern:
Use check and uncheck for toggles. Validate states with toBeChecked and toBeDisabled. Use loops for dynamic lists.
import { test, expect } from '@playwright/test';
test.describe('User Preferences', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/settings/preferences');
});
test('toggle notification settings and verify state', async ({ page }) => {
const emailNotif = page.getByLabel('Email Notifications');
const smsNotif = page.getByLabel('SMS Alerts');
const disabledOption = page.getByLabel('Legacy Alerts');
// Check and verify
await emailNotif.check();
await expect(emailNotif).toBeChecked();
// Uncheck and verify
await emailNotif.uncheck();
await expect(emailNotif).not.toBeChecked();
// Verify disabled state
await expect(disabledOption).toBeDisabled();
// Multi-select validation
await smsNotif.check();
await expect(smsNotif).toBeChecked();
});
test('validate subscription tier selection', async ({ page }) => {
const basicRadio = page.getByRole('radio', { name: 'Basic Plan' });
const proRadio = page.getByRole('radio', { name: 'Pro Plan' });
const resultText = page.getByTestId('selection-result');
// Select Pro plan
await proRadio.check();
await expect(proRadio).toBeChecked();
await expect(basicRadio).not.toBeChecked();
// Verify result text updates
await expect(resultText).toHaveText('Selected: Pro Plan');
});
test('batch enable all active features', async ({ page }) => {
const enableAllBtn = page.getByRole('button', { name: 'Enable All' });
const activeCheckboxes = page.locator('input[type="checkbox"]:not(:disabled)');
await enableAllBtn.click();
const count = await activeCheckboxes.count();
for (let i = 0; i < count; i++) {
await expect(activeCheckboxes.nth(i)).toBeChecked();
}
});
});
Rationale:
check and uncheck handle the interaction logic, including waiting for the element to be actionable.
toBeChecked and not.toBeChecked provide clear state verification.
toBeDisabled ensures UI constraints are enforced.
- Looping through locators allows validation of dynamic lists without hardcoding indices.
Pitfall Guide
Avoid these common mistakes to maintain a healthy test suite.
| Pitfall | Explanation | Fix |
|---|
| Brittle CSS Selectors | Using IDs or classes that change during development causes frequent test breaks. | Use semantic locators like getByRole, getByLabel, or getByTestId. |
| Manual Sleeps | Adding await page.waitForTimeout() slows tests and doesn't solve race conditions. | Rely on Playwright's auto-waiting and retrying assertions. Remove all sleeps. |
| Ignoring Disabled States | Failing to test disabled controls misses critical UI validation logic. | Use toBeDisabled to verify controls are correctly restricted. |
| Checkbox/Radio Confusion | Treating checkboxes and radios identically ignores mutual exclusion in radios. | Use check for both, but verify that selecting one radio unchecks others. |
| Missing Negative Paths | Only testing happy paths leaves error handling unverified. | Test invalid inputs, empty fields, and error messages explicitly. |
| Hardcoded Credentials | Embedding secrets in tests creates security risks and environment issues. | Use environment variables or secure fixtures for credentials. |
| Over-Asserting | Asserting every minor detail increases maintenance without adding value. | Focus assertions on critical business outcomes and state changes. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Static Form Fields | getByLabel + fill | Links inputs to accessible labels; stable across UI changes. | Low maintenance |
| Dynamic Checkbox List | Locator loop + toBeChecked | Handles variable counts without hardcoding indices. | Medium setup, low maintenance |
| Cross-Browser Validation | Playwright projects config | Runs tests on Chromium, Firefox, and WebKit automatically. | Higher execution cost, higher coverage |
| Login with SSO | Mock authentication or API setup | Avoids UI flakiness from third-party redirects. | Reduces flakiness, faster tests |
| Critical Business Flow | Full E2E with assertions | Validates end-to-end user journey with state checks. | Higher initial effort, high ROI |
Configuration Template
Use this playwright.config.ts to enable parallel execution, retries, and multi-browser support.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'https://app.example.com',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});
Quick Start Guide
- Initialize Project: Run
npm init playwright@latest to scaffold the test environment and configuration.
- Write Test: Create a test file in the
tests directory using the patterns above. Use semantic locators and assertions.
- Run Tests: Execute
npx playwright test to run the suite. Use --headed for visual debugging.
- View Report: Open the HTML report with
npx playwright show-report to analyze results and traces.
- Integrate CI: Add the Playwright action to your CI pipeline to run tests on every pull request.