Playwright tests follow a consistent, easy-to-understand structure:
const { test, expect } = require('@playwright/test');
test('description of test scenario', async ({ page }) => {
// Test implementation
});
// Group related tests
test.describe('Feature Group', () => {
test('specific test case', async ({ page }) => {
// Test implementation
});
});
Playwright provides multiple strategies for element selection:
// By CSS Selector
await page.locator('.login-button');
// By Text
await page.getByText('Submit');
// By Role
await page.getByRole('button', { name: 'Login' });
// By Test ID (Recommended for stability)
await page.getByTestId('login-submit');
{% hint style="info" %}
- Prefer data-testid attributes for most reliable selection
- Avoid using complex CSS selectors or brittle locators {% endhint %}
Common user interactions:
// Clicking
await page.click('.submit-button');
// Typing
await page.fill('#username', 'testuser');
// Handling dropdowns
await page.selectOption('select#role', 'admin');
// Checkbox and radio buttons
await page.check('#agree-terms');
// Hover and complex interactions
await page.hover('.dropdown-menu');
Comprehensive assertion capabilities:
// Basic existence
await expect(page.locator('.error-message')).toBeVisible();
// Text content
await expect(page.locator('.welcome-message'))
.toHaveText('Welcome, John Doe');
// Attribute checks
await expect(page.locator('button'))
.toHaveAttribute('disabled');
// Multiple assertions
test('login workflow', async ({ page }) => {
await expect(page).toHaveTitle('Login Page');
await expect(page.locator('.login-form')).toBeVisible();
});
Recommended pattern for organizing tests:
// page-objects/login.page.js
class LoginPage {
constructor(page) {
this.page = page;
this.usernameInput = page.locator('#username');
this.passwordInput = page.locator('#password');
this.loginButton = page.locator('.login-button');
}
async login(username, password) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
await this.page.waitForNavigation();
}
async isLoggedIn() {
return await this.page.locator('.user-dashboard').isVisible();
}
}
// test implementation
test('successful login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.login('validuser', 'validpassword');
expect(await loginPage.isLoggedIn()).toBeTruthy();
});
test('async data loading', async ({ page }) => {
// Wait for network request
await page.waitForResponse('**/api/data');
// Wait for element to appear
await page.waitForSelector('.data-loaded');
// Custom wait with timeout
await page.waitForFunction(() =>
document.querySelector('.loading-indicator') === null
);
});
test.describe('Authentication Workflows', () => {
// Login scenarios
test('successful login', async ({ page }) => { /* ... */ });
test('failed login - invalid credentials', async ({ page }) => { /* ... */ });
// Form interactions
test('form validation', async ({ page }) => { /* ... */ });
// Navigation
test('navigation between pages', async ({ page }) => { /* ... */ });
});
{% hint style="warning" %} Important: Always handle potential async operations and unexpected behaviors in your tests. {% endhint %}