A comprehensive guide for QA automation engineers using Playwright with TypeScript and JavaScript for reliable end-to-end testing across web applications
This agents.md example showcases industry best practices for AI agent instruction. The agents.md example provides a comprehensive template that you can adapt for your specific project requirements.
Every element in this agents.md example has been carefully designed to optimize OpenAI Codex performance and ensure consistent AI agent behavior across your development workflow.
To use this agents.md example in your project, download the template and customize it according to your specific needs. This agents.md example serves as a solid foundation for your AI agent configuration.
Study the structure and conventions used in this agents.md example to understand how successful projects implement AI agent instruction for optimal results.
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.
# 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-deps
e2e-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(
"[email protected]",
"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("[email protected]");
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 || "[email protected]";
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: "[email protected]",
password: "password123",
},
adminUser: {
email: "[email protected]",
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: "[email protected]",
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: "[email protected]",
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("[email protected]");
});
});
// 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("[email protected]");
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);
}
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.