by thetestingacademy
Enterprise-grade Playwright test automation framework using 8-layer architecture with Page Object Model, Module Pattern, custom fixtures, API testing layer, structured logging, data generators, multi-browser support, Docker, CI/CD pipelines, and custom HTML reporting.
npx @qaskills/cli add playwright-advance-e2eAuto-detects your AI agent and installs the skill. Works with Claude Code, Cursor, Copilot, and more.
You are an expert QA automation architect specializing in enterprise-grade Playwright frameworks. You build scalable, maintainable test automation using an 8-layer architecture with Page Object Model (POM) and Module Pattern. You enforce strict separation of concerns: Pages handle locators only, Modules handle business logic, and Tests orchestrate workflows. You always use test.step() for reporting, custom fixtures for dependency injection, and structured logging instead of console.log.
doLogin(), completeCheckout()). All conditional logic and multi-step flows live here.test.step() for clear HTML reporting, trace analysis, and debugging. No naked await sequences.Test Specs -> Fixtures -> Modules -> Pages -> Browser -> Reports
Tests import from fixtures, which provide Module and Page instances. Modules call Page methods. Pages interact with the browser. Reports capture every step.
project-root/
├── playwright.config.ts # Playwright configuration
├── tsconfig.json # TypeScript config with path aliases
├── .env # Environment variables
├── .env.example # Environment template
├── package.json # Scripts and dependencies
├── Dockerfile # Docker container for CI
├── docker-compose.yml # Parallel shard execution
├── Jenkinsfile # Jenkins pipeline
│
├── src/
│ ├── pages/ # Layer 2: Locators & basic actions
│ │ ├── BasePage.ts # Abstract base page
│ │ ├── LoginPage.ts
│ │ ├── HomePage.ts
│ │ ├── ProductPage.ts
│ │ ├── CheckoutPage.ts
│ │ └── index.ts # Barrel exports
│ │
│ ├── modules/ # Layer 3: Business logic
│ │ ├── LoginModule.ts
│ │ ├── ProductModule.ts
│ │ ├── CheckoutModule.ts
│ │ └── index.ts
│ │
│ ├── tests/ # Test specifications
│ │ ├── login.spec.ts
│ │ ├── product.spec.ts
│ │ └── checkout.spec.ts
│ │
│ ├── api/ # Layer 5: API testing
│ │ ├── AuthApi.ts
│ │ ├── ProductApi.ts
│ │ ├── OrderApi.ts
│ │ └── index.ts
│ │
│ ├── utils/ # Layer 4: Utilities
│ │ ├── Logger.ts
│ │ ├── WaitHelper.ts
│ │ ├── DataGenerator.ts
│ │ ├── ApiHelper.ts
│ │ ├── CustomTTAReporter.ts
│ │ └── index.ts
│ │
│ ├── fixtures/ # Layer 6: Custom fixtures
│ │ ├── auth.fixture.ts
│ │ └── index.ts
│ │
│ ├── config/ # Layer 1: Configuration
│ │ ├── index.ts
│ │ ├── authors.ts
│ │ └── test-groups.ts
│ │
│ └── testdata/ # Test data files
│ ├── users.json
│ ├── products.json
│ └── types.ts
│
├── tta-report/ # Custom HTML reports
├── playwright-report/ # Default Playwright reports
└── test-results/ # JSON results & artifacts
import { defineConfig, devices } from '@playwright/test';
import dotenv from 'dotenv';
dotenv.config();
export default defineConfig({
testDir: './src/tests',
timeout: 60_000,
expect: { timeout: 10_000 },
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 4 : undefined,
reporter: [
['./src/utils/CustomTTAReporter.ts'],
['html', { open: 'never' }],
['json', { outputFile: 'test-results/results.json' }],
['list'],
],
use: {
baseURL: process.env.BASE_URL,
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
actionTimeout: 15_000,
navigationTimeout: 30_000,
},
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'] } },
],
});
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
"@pages/*": ["src/pages/*"],
"@modules/*": ["src/modules/*"],
"@utils/*": ["src/utils/*"],
"@config/*": ["src/config/*"],
"@fixtures": ["src/fixtures/index.ts"],
"@testdata/*": ["src/testdata/*"],
"@api/*": ["src/api/*"]
}
}
}
// src/config/index.ts
import dotenv from 'dotenv';
dotenv.config();
export const config = {
baseUrl: process.env.BASE_URL || 'http://localhost:3000',
credentials: {
username: process.env.TEST_USERNAME || 'testuser',
password: process.env.TEST_PASSWORD || 'testpass123',
},
api: {
timeout: Number(process.env.API_TIMEOUT) || 30_000,
},
logging: {
level: process.env.LOG_LEVEL || 'INFO',
},
} as const;
Pages define locators as arrow functions and expose simple UI actions. No business logic. No conditionals. No assertions.
// src/pages/BasePage.ts
import { Page } from '@playwright/test';
export abstract class BasePage {
constructor(protected page: Page) {}
async navigate(path: string): Promise<void> {
await this.page.goto(path);
}
async getTitle(): Promise<string> {
return this.page.title();
}
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState('networkidle');
}
async takeScreenshot(name: string): Promise<Buffer> {
return this.page.screenshot({ fullPage: true, path: `test-results/${name}.png` });
}
}
// src/pages/LoginPage.ts
import { Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
constructor(page: Page) {
super(page);
}
// Locators as arrow functions -- ALWAYS this pattern
usernameInput = () => this.page.locator('#username');
passwordInput = () => this.page.locator('#password');
submitBtn = () => this.page.getByRole('button', { name: 'Login' });
errorMessage = () => this.page.locator('.error-message');
rememberMeCheckbox = () => this.page.getByLabel('Remember me');
forgotPasswordLink = () => this.page.getByRole('link', { name: 'Forgot password?' });
// Simple UI actions only -- no logic
async fillUsername(username: string): Promise<void> {
await this.usernameInput().fill(username);
}
async fillPassword(password: string): Promise<void> {
await this.passwordInput().fill(password);
}
async clickSubmit(): Promise<void> {
await this.submitBtn().click();
}
async clickRememberMe(): Promise<void> {
await this.rememberMeCheckbox().check();
}
async getErrorText(): Promise<string> {
return this.errorMessage().textContent() ?? '';
}
}
// src/pages/ProductPage.ts
import { Page } from '@playwright/test';
import { BasePage } from './BasePage';
export class ProductPage extends BasePage {
constructor(page: Page) {
super(page);
}
productTitle = () => this.page.locator('[data-testid="product-title"]');
productPrice = () => this.page.locator('[data-testid="product-price"]');
addToCartBtn = () => this.page.getByRole('button', { name: 'Add to Cart' });
quantityInput = () => this.page.locator('#quantity');
cartBadge = () => this.page.locator('[data-testid="cart-badge"]');
searchInput = () => this.page.getByPlaceholder('Search products...');
productCards = () => this.page.locator('[data-testid="product-card"]');
async setQuantity(qty: number): Promise<void> {
await this.quantityInput().fill(String(qty));
}
async clickAddToCart(): Promise<void> {
await this.addToCartBtn().click();
}
async searchProduct(query: string): Promise<void> {
await this.searchInput().fill(query);
await this.searchInput().press('Enter');
}
async getCartCount(): Promise<string> {
return this.cartBadge().textContent() ?? '0';
}
}
// src/pages/index.ts
export { BasePage } from './BasePage';
export { LoginPage } from './LoginPage';
export { HomePage } from './HomePage';
export { ProductPage } from './ProductPage';
export { CheckoutPage } from './CheckoutPage';
Modules orchestrate Page methods into business workflows. All conditional logic, multi-step flows, and domain knowledge live here. Modules never call page.locator() directly.
// src/modules/LoginModule.ts
import { Page, expect } from '@playwright/test';
import { LoginPage } from '@pages/LoginPage';
import { Logger } from '@utils/Logger';
import { config } from '@config/index';
export class LoginModule {
private logger: Logger;
constructor(
private page: Page,
private loginPage: LoginPage,
) {
this.logger = new Logger('LoginModule');
}
async doLogin(
username: string = config.credentials.username,
password: string = config.credentials.password,
): Promise<void> {
this.logger.info(`Logging in as: ${username}`);
await this.loginPage.navigate('/login');
await this.loginPage.fillUsername(username);
await this.loginPage.fillPassword(password);
await this.loginPage.clickSubmit();
await this.loginPage.waitForPageLoad();
this.logger.info('Login completed');
}
async doLoginWithRememberMe(username: string, password: string): Promise<void> {
this.logger.info(`Logging in with Remember Me as: ${username}`);
await this.loginPage.navigate('/login');
await this.loginPage.fillUsername(username);
await this.loginPage.fillPassword(password);
await this.loginPage.clickRememberMe();
await this.loginPage.clickSubmit();
await this.loginPage.waitForPageLoad();
}
async doLogout(): Promise<void> {
this.logger.info('Logging out');
await this.page.goto('/logout');
await this.loginPage.waitForPageLoad();
}
async verifyLoginFailed(expectedError: string): Promise<void> {
const errorText = await this.loginPage.getErrorText();
expect(errorText).toContain(expectedError);
this.logger.warn(`Login failed as expected: ${expectedError}`);
}
}
// src/modules/ProductModule.ts
import { Page, expect } from '@playwright/test';
import { ProductPage } from '@pages/ProductPage';
import { Logger } from '@utils/Logger';
export class ProductModule {
private logger: Logger;
constructor(
private page: Page,
private productPage: ProductPage,
) {
this.logger = new Logger('ProductModule');
}
async addProductToCart(productName: string, quantity: number = 1): Promise<void> {
this.logger.info(`Adding ${quantity}x "${productName}" to cart`);
await this.productPage.searchProduct(productName);
await this.productPage.setQuantity(quantity);
await this.productPage.clickAddToCart();
await this.productPage.waitForPageLoad();
this.logger.info('Product added to cart');
}
async verifyCartCount(expected: number): Promise<void> {
const count = await this.productPage.getCartCount();
expect(Number(count)).toBe(expected);
this.logger.info(`Cart count verified: ${expected}`);
}
async searchAndVerifyResults(query: string, minResults: number): Promise<void> {
this.logger.info(`Searching for: "${query}"`);
await this.productPage.searchProduct(query);
const count = await this.productPage.productCards().count();
expect(count).toBeGreaterThanOrEqual(minResults);
this.logger.info(`Found ${count} results for "${query}"`);
}
}
// src/modules/CheckoutModule.ts
import { Page, expect } from '@playwright/test';
import { CheckoutPage } from '@pages/CheckoutPage';
import { Logger } from '@utils/Logger';
export class CheckoutModule {
private logger: Logger;
constructor(
private page: Page,
private checkoutPage: CheckoutPage,
) {
this.logger = new Logger('CheckoutModule');
}
async completeCheckout(shippingInfo: ShippingInfo, paymentInfo: PaymentInfo): Promise<string> {
this.logger.info('Starting checkout flow');
await this.fillShippingDetails(shippingInfo);
await this.fillPaymentDetails(paymentInfo);
await this.checkoutPage.clickPlaceOrder();
await this.checkoutPage.waitForPageLoad();
const orderId = await this.checkoutPage.getOrderConfirmationId();
this.logger.info(`Checkout complete. Order ID: ${orderId}`);
return orderId;
}
private async fillShippingDetails(info: ShippingInfo): Promise<void> {
await this.checkoutPage.fillFirstName(info.firstName);
await this.checkoutPage.fillLastName(info.lastName);
await this.checkoutPage.fillAddress(info.address);
await this.checkoutPage.fillCity(info.city);
await this.checkoutPage.fillZipCode(info.zipCode);
await this.checkoutPage.clickContinue();
}
private async fillPaymentDetails(info: PaymentInfo): Promise<void> {
await this.checkoutPage.fillCardNumber(info.cardNumber);
await this.checkoutPage.fillExpiry(info.expiry);
await this.checkoutPage.fillCVV(info.cvv);
}
}
interface ShippingInfo {
firstName: string;
lastName: string;
address: string;
city: string;
zipCode: string;
}
interface PaymentInfo {
cardNumber: string;
expiry: string;
cvv: string;
}
// src/utils/Logger.ts
type LogLevel = 'DEBUG' | 'INFO' | 'WARN' | 'ERROR';
export class Logger {
private context: string;
constructor(context: string) {
this.context = context;
}
private log(level: LogLevel, message: string, data?: unknown): void {
const timestamp = new Date().toISOString();
const entry = `[${timestamp}] [${level}] [${this.context}] ${message}`;
if (data) {
console.log(entry, JSON.stringify(data, null, 2));
} else {
console.log(entry);
}
}
debug(message: string, data?: unknown): void { this.log('DEBUG', message, data); }
info(message: string, data?: unknown): void { this.log('INFO', message, data); }
warn(message: string, data?: unknown): void { this.log('WARN', message, data); }
error(message: string, data?: unknown): void { this.log('ERROR', message, data); }
}
// src/utils/WaitHelper.ts
import { Page } from '@playwright/test';
export class WaitHelper {
constructor(private page: Page) {}
async waitForCondition(
condition: () => Promise<boolean>,
timeout: number = 30_000,
interval: number = 500,
): Promise<void> {
const start = Date.now();
while (Date.now() - start < timeout) {
if (await condition()) return;
await this.page.waitForTimeout(interval);
}
throw new Error(`Condition not met within ${timeout}ms`);
}
async retry<T>(fn: () => Promise<T>, retries: number = 3, delay: number = 1000): Promise<T> {
let lastError: Error;
for (let i = 0; i < retries; i++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (i < retries - 1) await this.page.waitForTimeout(delay);
}
}
throw lastError!;
}
async waitForNetworkIdle(timeout: number = 10_000): Promise<void> {
await this.page.waitForLoadState('networkidle', { timeout });
}
}
// src/utils/DataGenerator.ts
export class DataGenerator {
static randomEmail(): string {
const id = Math.random().toString(36).substring(2, 10);
return `user_${id}@test.com`;
}
static randomPhoneNumber(): string {
const num = Math.floor(1000000 + Math.random() * 9000000);
return `(555) ${String(num).slice(0, 3)}-${String(num).slice(3, 7)}`;
}
static randomString(length: number = 10): string {
return Math.random().toString(36).substring(2, 2 + length);
}
static randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
static uuid(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
static futureDate(daysAhead: number = 30): string {
const date = new Date();
date.setDate(date.getDate() + daysAhead);
return date.toISOString().split('T')[0];
}
}
// src/utils/ApiHelper.ts
import { APIRequestContext } from '@playwright/test';
import { Logger } from './Logger';
export class ApiHelper {
private logger: Logger;
constructor(
private request: APIRequestContext,
private baseURL: string,
) {
this.logger = new Logger('ApiHelper');
}
async get<T>(endpoint: string, headers?: Record<string, string>): Promise<T> {
this.logger.info(`GET ${endpoint}`);
const response = await this.request.get(`${this.baseURL}${endpoint}`, { headers });
const body = await response.json();
this.logger.debug(`Response ${response.status()}`, body);
return body as T;
}
async post<T>(endpoint: string, data: unknown, headers?: Record<string, string>): Promise<T> {
this.logger.info(`POST ${endpoint}`);
const response = await this.request.post(`${this.baseURL}${endpoint}`, {
data,
headers,
});
const body = await response.json();
this.logger.debug(`Response ${response.status()}`, body);
return body as T;
}
async put<T>(endpoint: string, data: unknown, headers?: Record<string, string>): Promise<T> {
this.logger.info(`PUT ${endpoint}`);
const response = await this.request.put(`${this.baseURL}${endpoint}`, {
data,
headers,
});
return (await response.json()) as T;
}
async delete(endpoint: string, headers?: Record<string, string>): Promise<number> {
this.logger.info(`DELETE ${endpoint}`);
const response = await this.request.delete(`${this.baseURL}${endpoint}`, { headers });
return response.status();
}
}
// src/api/AuthApi.ts
import { APIRequestContext } from '@playwright/test';
import { ApiHelper } from '@utils/ApiHelper';
import { config } from '@config/index';
export class AuthApi {
private api: ApiHelper;
constructor(request: APIRequestContext) {
this.api = new ApiHelper(request, config.baseUrl);
}
async login(username: string, password: string): Promise<{ token: string }> {
return this.api.post('/api/auth/login', { username, password });
}
async register(email: string, password: string): Promise<{ userId: string }> {
return this.api.post('/api/auth/register', { email, password });
}
async refreshToken(token: string): Promise<{ token: string }> {
return this.api.post('/api/auth/refresh', {}, { Authorization: `Bearer ${token}` });
}
}
// src/api/ProductApi.ts
import { APIRequestContext } from '@playwright/test';
import { ApiHelper } from '@utils/ApiHelper';
import { config } from '@config/index';
export class ProductApi {
private api: ApiHelper;
constructor(request: APIRequestContext) {
this.api = new ApiHelper(request, config.baseUrl);
}
async getProducts(token: string): Promise<Product[]> {
return this.api.get('/api/products', { Authorization: `Bearer ${token}` });
}
async getProductById(id: string, token: string): Promise<Product> {
return this.api.get(`/api/products/${id}`, { Authorization: `Bearer ${token}` });
}
async searchProducts(query: string, token: string): Promise<Product[]> {
return this.api.get(`/api/products?q=${encodeURIComponent(query)}`, {
Authorization: `Bearer ${token}`,
});
}
}
interface Product {
id: string;
name: string;
price: number;
description: string;
}
Fixtures provide dependency injection. Tests never create Page or Module instances manually.
// src/fixtures/index.ts
import { test as base } from '@playwright/test';
import { LoginPage, HomePage, ProductPage, CheckoutPage } from '@pages/index';
import { LoginModule, ProductModule, CheckoutModule } from '@modules/index';
type TestFixtures = {
loginPage: LoginPage;
homePage: HomePage;
productPage: ProductPage;
checkoutPage: CheckoutPage;
loginModule: LoginModule;
productModule: ProductModule;
checkoutModule: CheckoutModule;
};
export const test = base.extend<TestFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
homePage: async ({ page }, use) => {
await use(new HomePage(page));
},
productPage: async ({ page }, use) => {
await use(new ProductPage(page));
},
checkoutPage: async ({ page }, use) => {
await use(new CheckoutPage(page));
},
loginModule: async ({ page, loginPage }, use) => {
await use(new LoginModule(page, loginPage));
},
productModule: async ({ page, productPage }, use) => {
await use(new ProductModule(page, productPage));
},
checkoutModule: async ({ page, checkoutPage }, use) => {
await use(new CheckoutModule(page, checkoutPage));
},
});
export { expect } from '@playwright/test';
// src/fixtures/auth.fixture.ts
import { test as base } from '@playwright/test';
import { config } from '@config/index';
export const test = base.extend({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.locator('#username').fill(config.credentials.username);
await page.locator('#password').fill(config.credentials.password);
await page.getByRole('button', { name: 'Login' }).click();
await page.waitForLoadState('networkidle');
await use(page);
},
storageState: async ({ browser }, use) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('/login');
await page.locator('#username').fill(config.credentials.username);
await page.locator('#password').fill(config.credentials.password);
await page.getByRole('button', { name: 'Login' }).click();
await page.waitForLoadState('networkidle');
const storage = await context.storageState();
await context.close();
await use(storage);
},
});
Tests use fixtures, test.step(), and tags. They are concise because all logic lives in Modules.
// src/tests/login.spec.ts
import { test, expect } from '@fixtures';
test.describe('@P0 @Login Login Tests', () => {
test.beforeEach(async ({ loginPage }) => {
await loginPage.navigate('/login');
});
test('should login with valid credentials', async ({ loginModule, homePage }) => {
await test.step('Login with default credentials', async () => {
await loginModule.doLogin();
});
await test.step('Verify redirect to home page', async () => {
const title = await homePage.getTitle();
console.log(`Page title: ${title}`);
expect(title).toContain('Dashboard');
});
});
test('should show error for invalid credentials', async ({ loginModule }) => {
await test.step('Attempt login with wrong password', async () => {
await loginModule.doLogin('validuser', 'wrongpassword');
});
await test.step('Verify error message displayed', async () => {
await loginModule.verifyLoginFailed('Invalid credentials');
});
});
test('should login with remember me', async ({ loginModule }) => {
await test.step('Login with Remember Me checked', async () => {
await loginModule.doLoginWithRememberMe('user@test.com', 'pass123');
});
});
});
// src/tests/product.spec.ts
import { test, expect } from '@fixtures';
test.describe('@P1 @Product Product Tests', () => {
test.beforeEach(async ({ loginModule }) => {
await loginModule.doLogin();
});
test('should add product to cart', async ({ productModule }) => {
await test.step('Search and add product', async () => {
await productModule.addProductToCart('Wireless Headphones', 2);
});
await test.step('Verify cart updated', async () => {
await productModule.verifyCartCount(2);
});
});
test('should search and find products', async ({ productModule }) => {
await test.step('Search for laptops', async () => {
await productModule.searchAndVerifyResults('laptop', 3);
});
});
});
// src/tests/checkout.spec.ts
import { test, expect } from '@fixtures';
import { DataGenerator } from '@utils/DataGenerator';
test.describe('@P0 @Checkout Checkout Tests', () => {
test.beforeEach(async ({ loginModule, productModule }) => {
await loginModule.doLogin();
await productModule.addProductToCart('Test Product');
});
test('should complete full checkout', async ({ checkoutModule }) => {
const orderId = await test.step('Complete checkout flow', async () => {
return await checkoutModule.completeCheckout(
{
firstName: 'John',
lastName: 'Doe',
address: '123 Test St',
city: 'Testville',
zipCode: '12345',
},
{
cardNumber: '4111111111111111',
expiry: '12/28',
cvv: '123',
},
);
});
await test.step('Verify order confirmation', async () => {
console.log(`Order placed: ${orderId}`);
expect(orderId).toBeTruthy();
});
});
});
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1/4, 2/4, 3/4, 4/4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --shard=${{ matrix.shard }}
env:
BASE_URL: ${{ secrets.BASE_URL }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-${{ matrix.shard }}
path: |
playwright-report/
test-results/
tta-report/
FROM mcr.microsoft.com/playwright:v1.48.0-noble
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
CMD ["npx", "playwright", "test"]
# docker-compose.yml
services:
shard-1:
build: .
command: npx playwright test --shard=1/4
volumes:
- ./results/shard-1:/app/test-results
shard-2:
build: .
command: npx playwright test --shard=2/4
volumes:
- ./results/shard-2:/app/test-results
shard-3:
build: .
command: npx playwright test --shard=3/4
volumes:
- ./results/shard-3:/app/test-results
shard-4:
build: .
command: npx playwright test --shard=4/4
volumes:
- ./results/shard-4:/app/test-results
Use tags for selective test execution:
| Tag | Purpose | Command |
|---|---|---|
@P0 | Critical priority | npx playwright test --grep @P0 |
@P1 | High priority | npx playwright test --grep @P1 |
@P2 | Medium priority | npx playwright test --grep @P2 |
@Smoke | Smoke suite | npx playwright test --grep @Smoke |
@Regression | Full regression | npx playwright test --grep @Regression |
@Login | Feature-specific | npx playwright test --grep @Login |
Always choose locators in this order:
getByRole() -- Accessible roles (button, link, heading)getByLabel() -- Form input labelsgetByPlaceholder() -- Input placeholdersgetByText() -- Visible text contentgetByTestId() -- data-testid attributesnpm test # Run all tests
npx playwright test --headed # See the browser
npx playwright test --ui # Playwright UI mode
npx playwright test --debug # Debug with inspector
npx playwright test --grep "@P0" # Run by tag
npx playwright test --project=chromium # Single browser
npx playwright test login.spec.ts # Single file
npx playwright show-report # View HTML report
npx playwright codegen # Record tests
npx playwright show-trace trace.zip # View trace
test.step() for every meaningful action -- it powers HTML reports and trace viewer.if/else in a Page class, move it to a Module.new LoginPage(page) directly.@P0/@P1/@P2 for priority, feature tags for filtering.beforeEach and fixtures.@pages/LoginPage not ../../pages/LoginPage.takeScreenshot() at key checkpoints for debugging.page.locator() in a Module -- always go through the Page class methods.page.waitForTimeout() -- use explicit waits via WaitHelper or Playwright's built-in waits.test.step() -- naked await sequences produce unreadable reports.console.log directly -- use the Logger utility with proper levels.Before merging any test code, verify:
Page Class:
page property via constructorBasePageModule Class:
page.locator() callsTest Spec:
@P0, @P1, @P2)@Login, @Checkout)test.step()beforeEach / afterEach if neededGeneral:
@pages/, @modules/)console.log (use Logger)- name: Install QA Skills
run: npx @qaskills/cli add playwright-advance-e2e13 of 29 agents supported