| name | Playwright End-to-End Testing Automation Guide | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| description | A comprehensive guide for QA automation engineers using Playwright with TypeScript and JavaScript for reliable end-to-end testing across web applications | ||||||||
| category | Testing Framework | ||||||||
| author | Agents.md Collection | ||||||||
| authorUrl | https://github.com/gakeez/agents_md_collection | ||||||||
| tags |
|
||||||||
| lastUpdated | 2025-06-16 |
This comprehensive guide outlines best practices for QA automation engineers using Playwright with TypeScript and JavaScript for end-to-end testing. The guide emphasizes writing reliable, maintainable tests that reflect real user behavior, utilizing Playwright's modern testing capabilities including fixtures, web-first assertions, and cross-browser compatibility.
- Testing Framework: Playwright 1.40+
- Languages: TypeScript 5.0+, JavaScript ES2022+
- Test Runner: Playwright Test Runner
- Browsers: Chromium, Firefox, WebKit (Safari)
- Devices: Desktop, Mobile, Tablet configurations
- CI/CD: GitHub Actions, Jenkins, Azure DevOps
- Reporting: HTML Reporter, Allure, JUnit XML
- Visual Testing: Playwright Screenshots, Percy integration
- Node.js 18+
- npm/yarn/pnpm
- Playwright browsers
- VS Code with Playwright extension (recommended)
# Initialize new project
npm init playwright@latest
# Or add to existing project
npm install -D @playwright/test
npx playwright install
# Install specific browsers
npx playwright install chromium firefox webkit
# Install system dependencies (Linux)
npx playwright install-depse2e-tests/
├── tests/ # Test files
│ ├── auth/
│ │ ├── login.spec.ts
│ │ └── registration.spec.ts
│ ├── user-management/
│ │ ├── profile.spec.ts
│ │ └── settings.spec.ts
│ └── api/
│ └── user-api.spec.ts
├── fixtures/ # Custom fixtures
│ ├── auth-fixture.ts
│ └── database-fixture.ts
├── page-objects/ # Page Object Models
│ ├── login-page.ts
│ ├── dashboard-page.ts
│ └── base-page.ts
├── utils/ # Helper utilities
│ ├── test-data.ts
│ ├── api-helpers.ts
│ └── database-helpers.ts
├── config/ # Configuration files
│ ├── environments.ts
│ └── test-data.json
├── reports/ # Test reports
├── screenshots/ # Visual comparisons
├── playwright.config.ts # Main configuration
├── global-setup.ts # Global setup
├── global-teardown.ts # Global teardown
└── package.json
// tests/user-management/profile.spec.ts
import { test, expect } from "@playwright/test";
import { LoginPage } from "../page-objects/login-page";
import { ProfilePage } from "../page-objects/profile-page";
test.describe("User Profile Management", () => {
test.beforeEach(async ({ page }) => {
// Setup: Navigate to application and login
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.loginWithValidCredentials(
"user@example.com",
"password123"
);
});
test("should display user profile information correctly", async ({
page,
}) => {
const profilePage = new ProfilePage(page);
await profilePage.goto();
// Verify profile information is displayed
await expect(profilePage.userNameField).toBeVisible();
await expect(profilePage.emailField).toHaveValue("user@example.com");
await expect(profilePage.profileImage).toBeVisible();
});
test("should update user profile successfully", async ({ page }) => {
const profilePage = new ProfilePage(page);
await profilePage.goto();
const newName = "Updated User Name";
await profilePage.updateUserName(newName);
await profilePage.saveChanges();
// Verify success message and updated information
await expect(profilePage.successMessage).toBeVisible();
await expect(profilePage.successMessage).toHaveText(
"Profile updated successfully"
);
await expect(profilePage.userNameField).toHaveValue(newName);
});
test("should validate required fields when updating profile", async ({
page,
}) => {
const profilePage = new ProfilePage(page);
await profilePage.goto();
// Clear required field and attempt to save
await profilePage.clearUserName();
await profilePage.saveChanges();
// Verify validation error
await expect(profilePage.nameValidationError).toBeVisible();
await expect(profilePage.nameValidationError).toHaveText(
"Name is required"
);
});
});// page-objects/base-page.ts
import { Page, Locator } from "@playwright/test";
export abstract class BasePage {
protected page: Page;
constructor(page: Page) {
this.page = page;
}
/**
* Wait for the page to be fully loaded
*/
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState("networkidle");
}
/**
* Take a screenshot of the current page
*/
async takeScreenshot(name: string): Promise<void> {
await this.page.screenshot({
path: `screenshots/${name}.png`,
fullPage: true,
});
}
}
// page-objects/login-page.ts
import { Page, Locator, expect } from "@playwright/test";
import { BasePage } from "./base-page";
export class LoginPage extends BasePage {
// Use role-based and semantic locators
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
readonly forgotPasswordLink: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.getByLabel("Email");
this.passwordInput = page.getByLabel("Password");
this.loginButton = page.getByRole("button", { name: "Sign In" });
this.errorMessage = page.getByTestId("login-error");
this.forgotPasswordLink = page.getByRole("link", {
name: "Forgot Password?",
});
}
async goto(): Promise<void> {
await this.page.goto("/login");
await this.waitForPageLoad();
}
/**
* Login with provided credentials
*/
async loginWithCredentials(email: string, password: string): Promise<void> {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
/**
* Login with valid test credentials
*/
async loginWithValidCredentials(
email?: string,
password?: string
): Promise<void> {
const testEmail =
email || process.env.TEST_USER_EMAIL || "test@example.com";
const testPassword =
password || process.env.TEST_USER_PASSWORD || "password123";
await this.loginWithCredentials(testEmail, testPassword);
await this.page.waitForURL("/dashboard");
}
/**
* Verify login error is displayed
*/
async verifyLoginError(expectedMessage: string): Promise<void> {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toHaveText(expectedMessage);
}
}
// page-objects/profile-page.ts
import { Page, Locator } from "@playwright/test";
import { BasePage } from "./base-page";
export class ProfilePage extends BasePage {
readonly userNameField: Locator;
readonly emailField: Locator;
readonly profileImage: Locator;
readonly saveButton: Locator;
readonly successMessage: Locator;
readonly nameValidationError: Locator;
constructor(page: Page) {
super(page);
this.userNameField = page.getByLabel("Full Name");
this.emailField = page.getByLabel("Email Address");
this.profileImage = page.getByRole("img", { name: "Profile Picture" });
this.saveButton = page.getByRole("button", { name: "Save Changes" });
this.successMessage = page.getByTestId("success-message");
this.nameValidationError = page.getByTestId("name-validation-error");
}
async goto(): Promise<void> {
await this.page.goto("/profile");
await this.waitForPageLoad();
}
/**
* Update user name field
*/
async updateUserName(name: string): Promise<void> {
await this.userNameField.clear();
await this.userNameField.fill(name);
}
/**
* Clear user name field
*/
async clearUserName(): Promise<void> {
await this.userNameField.clear();
}
/**
* Save profile changes
*/
async saveChanges(): Promise<void> {
await this.saveButton.click();
}
}// playwright.config.ts
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"],
["junit", { outputFile: "reports/junit-results.xml" }],
["json", { outputFile: "reports/test-results.json" }],
],
use: {
baseURL: process.env.BASE_URL || "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
actionTimeout: 10000,
navigationTimeout: 30000,
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
{
name: "Mobile Chrome",
use: { ...devices["Pixel 5"] },
},
{
name: "Mobile Safari",
use: { ...devices["iPhone 12"] },
},
{
name: "Microsoft Edge",
use: { ...devices["Desktop Edge"], channel: "msedge" },
},
],
webServer: {
command: "npm run start",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});// fixtures/auth-fixture.ts
import { test as base, expect } from "@playwright/test";
import { LoginPage } from "../page-objects/login-page";
type AuthFixtures = {
authenticatedPage: any;
loginPage: LoginPage;
};
export const test = base.extend<AuthFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
authenticatedPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.loginWithValidCredentials();
await use(page);
},
});
export { expect };// config/environments.ts
export interface Environment {
baseUrl: string;
apiUrl: string;
testUser: {
email: string;
password: string;
};
adminUser: {
email: string;
password: string;
};
}
export const environments: Record<string, Environment> = {
development: {
baseUrl: "http://localhost:3000",
apiUrl: "http://localhost:3001/api",
testUser: {
email: "test@example.com",
password: "password123",
},
adminUser: {
email: "admin@example.com",
password: "admin123",
},
},
staging: {
baseUrl: "https://staging.example.com",
apiUrl: "https://api-staging.example.com",
testUser: {
email: process.env.STAGING_TEST_EMAIL!,
password: process.env.STAGING_TEST_PASSWORD!,
},
adminUser: {
email: process.env.STAGING_ADMIN_EMAIL!,
password: process.env.STAGING_ADMIN_PASSWORD!,
},
},
production: {
baseUrl: "https://example.com",
apiUrl: "https://api.example.com",
testUser: {
email: process.env.PROD_TEST_EMAIL!,
password: process.env.PROD_TEST_PASSWORD!,
},
adminUser: {
email: process.env.PROD_ADMIN_EMAIL!,
password: process.env.PROD_ADMIN_PASSWORD!,
},
},
};
export function getEnvironment(): Environment {
const env = process.env.NODE_ENV || "development";
return environments[env];
}// tests/api/user-api.spec.ts
import { test, expect } from "@playwright/test";
test.describe("User API Tests", () => {
let apiContext: any;
test.beforeAll(async ({ playwright }) => {
apiContext = await playwright.request.newContext({
baseURL: "https://api.example.com",
extraHTTPHeaders: {
Authorization: `Bearer ${process.env.API_TOKEN}`,
"Content-Type": "application/json",
},
});
});
test.afterAll(async () => {
await apiContext.dispose();
});
test("should create user via API", async () => {
const userData = {
name: "Test User",
email: "testuser@example.com",
password: "securePassword123",
};
const response = await apiContext.post("/users", {
data: userData,
});
expect(response.status()).toBe(201);
const responseBody = await response.json();
expect(responseBody).toHaveProperty("id");
expect(responseBody.email).toBe(userData.email);
expect(responseBody.name).toBe(userData.name);
});
test("should retrieve user by ID", async () => {
// First create a user
const createResponse = await apiContext.post("/users", {
data: {
name: "Retrieve Test User",
email: "retrieve@example.com",
password: "password123",
},
});
const createdUser = await createResponse.json();
// Then retrieve the user
const getResponse = await apiContext.get(`/users/${createdUser.id}`);
expect(getResponse.status()).toBe(200);
const retrievedUser = await getResponse.json();
expect(retrievedUser.id).toBe(createdUser.id);
expect(retrievedUser.email).toBe("retrieve@example.com");
});
});// tests/visual/homepage.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Visual Regression Tests", () => {
test("homepage should match visual baseline", async ({ page }) => {
await page.goto("/");
await page.waitForLoadState("networkidle");
// Hide dynamic content that changes between runs
await page.addStyleTag({
content: `
.timestamp, .live-chat, .ads {
visibility: hidden !important;
}
`,
});
await expect(page).toHaveScreenshot("homepage.png");
});
test("user profile page should match visual baseline", async ({ page }) => {
// Login first
await page.goto("/login");
await page.getByLabel("Email").fill("test@example.com");
await page.getByLabel("Password").fill("password123");
await page.getByRole("button", { name: "Sign In" }).click();
// Navigate to profile
await page.goto("/profile");
await page.waitForLoadState("networkidle");
// Mask dynamic content
await expect(page).toHaveScreenshot("profile-page.png", {
mask: [page.getByTestId("last-login-time")],
});
});
});// utils/test-helpers.ts
/**
* Generate random test data for user creation
*/
export function generateTestUser() {
const timestamp = Date.now();
return {
name: `Test User ${timestamp}`,
email: `test${timestamp}@example.com`,
password: "TestPassword123!",
};
}
/**
* Wait for element to be stable (not moving/changing)
*/
export async function waitForElementStable(locator: any, timeout = 5000) {
let previousBoundingBox = await locator.boundingBox();
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
await new Promise((resolve) => setTimeout(resolve, 100));
const currentBoundingBox = await locator.boundingBox();
if (
JSON.stringify(previousBoundingBox) === JSON.stringify(currentBoundingBox)
) {
return;
}
previousBoundingBox = currentBoundingBox;
}
throw new Error(`Element did not stabilize within ${timeout}ms`);
}
/**
* Fill form with retry mechanism
*/
export async function fillFormWithRetry(
locator: any,
value: string,
maxRetries = 3
) {
for (let i = 0; i < maxRetries; i++) {
try {
await locator.clear();
await locator.fill(value);
// Verify the value was set correctly
const actualValue = await locator.inputValue();
if (actualValue === value) {
return;
}
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
}
/**
* Handle file upload with validation
*/
export async function uploadFile(
page: any,
fileInputSelector: string,
filePath: string
) {
const fileInput = page.locator(fileInputSelector);
await fileInput.setInputFiles(filePath);
// Wait for upload to complete
await page.waitForFunction((selector) => {
const input = document.querySelector(selector);
return input && input.files && input.files.length > 0;
}, fileInputSelector);
}- Use descriptive and meaningful test names that clearly describe the expected behavior
- Utilize Playwright fixtures (test, page, expect) to maintain test isolation and consistency
- Use test.beforeEach and test.afterEach for setup and teardown to ensure clean state
- Keep tests DRY by extracting reusable logic into helper functions
- Focus on critical user paths with tests that are stable, maintainable, and reflect real user behavior
- Avoid page.locator and always use recommended built-in and role-based locators
- Prefer semantic locators: page.getByRole, page.getByLabel, page.getByText, page.getByTitle
- Use page.getByTestId whenever data-testid is defined on elements
- Reuse Playwright locators by using variables or constants for commonly used elements
- Use web-first assertions (toBeVisible, toHaveText) whenever possible
- Use playwright.config.ts for global configuration and environment setup
- Implement proper error handling and logging in tests for clear failure messages
- Use projects for multiple browsers and devices to ensure cross-browser compatibility
- Use built-in config objects like devices whenever possible
- Avoid hardcoded timeouts and use page.waitFor with specific conditions
- Ensure tests run reliably in parallel without shared state conflicts
- Use expect matchers for assertions (toEqual, toContain, toBeTruthy, toHaveLength)
- Avoid assert statements in favor of Playwright's expect matchers
- Implement retry mechanisms for flaky operations
- Use proper wait strategies for dynamic content and async operations
- Add JSDoc comments to describe the purpose of helper functions and reusable logic
- Avoid commenting on the resulting code unless necessary for complex logic
- Extract common patterns into reusable page objects and utilities
- Maintain consistent code style and formatting across test files
- Follow official Playwright documentation and best practices
- Configure appropriate retry strategies for CI environments
- Use proper reporting formats (HTML, JUnit, JSON) for different stakeholders
- Implement visual regression testing for UI consistency
- Set up proper artifact collection for screenshots, videos, and traces
- Use environment-specific configurations for different deployment stages
- Use environment variables for sensitive data like credentials
- Implement proper test data management with cleanup procedures
- Avoid hardcoded credentials in test files
- Use API setup for test data creation when possible
- Implement proper isolation between test runs and environments
This comprehensive guide provides a solid foundation for building reliable, maintainable end-to-end tests using Playwright with TypeScript and JavaScript, ensuring high-quality QA automation that reflects real user behavior and maintains stability across different browsers and devices.