Loading...
Loading...
Design and implement scalable test automation frameworks with Page Object Model, fixtures, and reporting. Use for test framework, page object pattern, test architecture, test organization, and automation infrastructure.
npx skill4agent add aj-geddes/useful-ai-prompts test-automation-framework// framework/pages/BasePage.ts
import { Page, Locator } from '@playwright/test';
export abstract class BasePage {
constructor(protected page: Page) {}
async goto(path: string) {
await this.page.goto(path);
}
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
}
async takeScreenshot(name: string) {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
protected async clickAndWait(locator: Locator) {
await Promise.all([
this.page.waitForResponse(resp => resp.status() === 200),
locator.click()
]);
}
}
// framework/pages/LoginPage.ts
export class LoginPage extends BasePage {
// Locators
private readonly emailInput = this.page.locator('[name="email"]');
private readonly passwordInput = this.page.locator('[name="password"]');
private readonly submitButton = this.page.locator('button[type="submit"]');
private readonly errorMessage = this.page.locator('.error-message');
async goto() {
await super.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async loginWithValidation(email: string, password: string) {
await this.login(email, password);
await this.page.waitForURL('/dashboard');
}
async getErrorMessage(): Promise<string> {
return await this.errorMessage.textContent() || '';
}
async isLoggedIn(): Promise<boolean> {
return this.page.url().includes('/dashboard');
}
}
// framework/pages/ProductPage.ts
export class ProductPage extends BasePage {
private readonly addToCartButton = this.page.locator('[data-testid="add-to-cart"]');
private readonly quantityInput = this.page.locator('[name="quantity"]');
private readonly priceLabel = this.page.locator('.price');
async goto(productId: string) {
await super.goto(`/products/${productId}`);
}
async addToCart(quantity: number = 1) {
if (quantity > 1) {
await this.quantityInput.fill(String(quantity));
}
await this.addToCartButton.click();
}
async getPrice(): Promise<number> {
const priceText = await this.priceLabel.textContent();
return parseFloat(priceText?.replace(/[^0-9.]/g, '') || '0');
}
}
// tests/checkout.test.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../framework/pages/LoginPage';
import { ProductPage } from '../framework/pages/ProductPage';
import { CartPage } from '../framework/pages/CartPage';
import { CheckoutPage } from '../framework/pages/CheckoutPage';
test.describe('Checkout Flow', () => {
let loginPage: LoginPage;
let productPage: ProductPage;
let cartPage: CartPage;
let checkoutPage: CheckoutPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
productPage = new ProductPage(page);
cartPage = new CartPage(page);
checkoutPage = new CheckoutPage(page);
await loginPage.goto();
await loginPage.loginWithValidation('user@test.com', 'password123');
});
test('complete checkout process', async () => {
// Add product to cart
await productPage.goto('product-1');
await productPage.addToCart(2);
// Verify cart
await cartPage.goto();
expect(await cartPage.getItemCount()).toBe(2);
// Checkout
await checkoutPage.goto();
await checkoutPage.fillShippingInfo({
name: 'John Doe',
address: '123 Main St',
city: 'San Francisco',
zip: '94105'
});
await checkoutPage.fillPaymentInfo({
cardNumber: '4242424242424242',
expiry: '12/25',
cvc: '123'
});
await checkoutPage.placeOrder();
expect(await checkoutPage.isOrderConfirmed()).toBe(true);
});
});// framework/fixtures/database.ts
import { test as base } from '@playwright/test';
import { PrismaClient } from '@prisma/client';
export const test = base.extend<{
db: PrismaClient;
testUser: User;
cleanupData: () => Promise<void>;
}>({
db: async ({}, use) => {
const db = new PrismaClient();
await use(db);
await db.$disconnect();
},
testUser: async ({ db }, use) => {
const user = await db.user.create({
data: {
email: `test-${Date.now()}@example.com`,
name: 'Test User',
password: await hashPassword('password123'),
},
});
await use(user);
await db.user.delete({ where: { id: user.id } });
},
cleanupData: async ({ db }, use) => {
const cleanup = async () => {
await db.order.deleteMany({});
await db.product.deleteMany({});
};
await use(cleanup);
},
});
export { expect } from '@playwright/test';
// Usage in tests
import { test, expect } from '../framework/fixtures/database';
test('user can create order', async ({ db, testUser }) => {
const product = await db.product.create({
data: { name: 'Test Product', price: 99.99 }
});
const order = await db.order.create({
data: {
userId: testUser.id,
items: {
create: [{ productId: product.id, quantity: 1 }]
}
}
});
expect(order.userId).toBe(testUser.id);
});// framework/utils/helpers.ts
import { Page, expect } from '@playwright/test';
export class TestHelpers {
static async waitForAPIResponse(
page: Page,
urlPattern: string | RegExp,
action: () => Promise<void>
) {
const responsePromise = page.waitForResponse(urlPattern);
await action();
return await responsePromise;
}
static async mockAPIResponse(
page: Page,
url: string | RegExp,
response: any,
status: number = 200
) {
await page.route(url, route => {
route.fulfill({
status,
contentType: 'application/json',
body: JSON.stringify(response),
});
});
}
static async fillForm(page: Page, formData: Record<string, string>) {
for (const [name, value] of Object.entries(formData)) {
await page.fill(`[name="${name}"]`, value);
}
}
static generateTestEmail(): string {
return `test-${Date.now()}-${Math.random().toString(36)}@example.com`;
}
static async verifyToastMessage(page: Page, message: string) {
const toast = page.locator('.toast-message');
await expect(toast).toContainText(message);
await expect(toast).toBeVisible();
}
}
// Usage
import { TestHelpers } from '../framework/utils/helpers';
test('form submission', async ({ page }) => {
await page.goto('/contact');
await TestHelpers.fillForm(page, {
name: 'John Doe',
email: TestHelpers.generateTestEmail(),
message: 'Test message'
});
await page.click('button[type="submit"]');
await TestHelpers.verifyToastMessage(page, 'Message sent successfully');
});// framework/config/config.ts
import * as dotenv from 'dotenv';
dotenv.config();
export interface TestConfig {
baseUrl: string;
apiUrl: string;
timeout: number;
headless: boolean;
slowMo: number;
screenshots: boolean;
video: boolean;
testUser: {
email: string;
password: string;
};
}
const environments: Record<string, TestConfig> = {
development: {
baseUrl: 'http://localhost:3000',
apiUrl: 'http://localhost:3001',
timeout: 30000,
headless: false,
slowMo: 0,
screenshots: true,
video: false,
testUser: {
email: 'dev@test.com',
password: 'devpass123',
},
},
staging: {
baseUrl: 'https://staging.example.com',
apiUrl: 'https://api-staging.example.com',
timeout: 60000,
headless: true,
slowMo: 0,
screenshots: true,
video: true,
testUser: {
email: process.env.STAGING_USER_EMAIL!,
password: process.env.STAGING_USER_PASSWORD!,
},
},
production: {
baseUrl: 'https://example.com',
apiUrl: 'https://api.example.com',
timeout: 60000,
headless: true,
slowMo: 100,
screenshots: true,
video: true,
testUser: {
email: process.env.PROD_USER_EMAIL!,
password: process.env.PROD_USER_PASSWORD!,
},
},
};
export const config: TestConfig =
environments[process.env.TEST_ENV || 'development'];// framework/reporters/CustomReporter.ts
import { Reporter, TestCase, TestResult } from '@playwright/test/reporter';
class CustomReporter implements Reporter {
private stats = {
passed: 0,
failed: 0,
skipped: 0,
total: 0,
};
onBegin() {
console.log('Starting test run...');
}
onTestEnd(test: TestCase, result: TestResult) {
this.stats.total++;
if (result.status === 'passed') {
this.stats.passed++;
console.log(`✓ ${test.title}`);
} else if (result.status === 'failed') {
this.stats.failed++;
console.log(`✗ ${test.title}`);
console.log(` Error: ${result.error?.message}`);
} else if (result.status === 'skipped') {
this.stats.skipped++;
console.log(`⊘ ${test.title}`);
}
}
onEnd() {
console.log('\nTest Summary:');
console.log(` Total: ${this.stats.total}`);
console.log(` Passed: ${this.stats.passed}`);
console.log(` Failed: ${this.stats.failed}`);
console.log(` Skipped: ${this.stats.skipped}`);
}
}
export default CustomReporter;# framework/pages/base_page.py
from selenium.webdriver.remote.webdriver import WebDriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
class BasePage:
def __init__(self, driver: WebDriver):
self.driver = driver
self.wait = WebDriverWait(driver, 10)
def goto(self, path: str):
self.driver.get(f"{self.base_url}{path}")
def wait_for_element(self, locator):
return self.wait.until(EC.presence_of_element_located(locator))
# framework/conftest.py
import pytest
from selenium import webdriver
from framework.config import config
@pytest.fixture(scope='session')
def browser():
"""Setup browser for test session."""
driver = webdriver.Chrome()
driver.implicitly_wait(10)
yield driver
driver.quit()
@pytest.fixture
def page(browser):
"""Provide clean page for each test."""
browser.delete_all_cookies()
return browser
@pytest.fixture
def test_user(db_session):
"""Create test user."""
from framework.factories import UserFactory
user = UserFactory.create()
db_session.add(user)
db_session.commit()
yield user
db_session.delete(user)
db_session.commit()
# tests/test_login.py
from framework.pages.login_page import LoginPage
def test_login_success(page, test_user):
"""Test successful login."""
login_page = LoginPage(page)
login_page.goto()
login_page.login(test_user.email, 'password123')
assert login_page.is_logged_in()test-automation/
├── framework/
│ ├── pages/
│ │ ├── BasePage.ts
│ │ ├── LoginPage.ts
│ │ ├── ProductPage.ts
│ │ └── CheckoutPage.ts
│ ├── fixtures/
│ │ ├── database.ts
│ │ └── api.ts
│ ├── utils/
│ │ ├── helpers.ts
│ │ ├── validators.ts
│ │ └── waiters.ts
│ ├── config/
│ │ └── config.ts
│ └── reporters/
│ └── CustomReporter.ts
├── tests/
│ ├── e2e/
│ │ ├── checkout.test.ts
│ │ └── search.test.ts
│ ├── integration/
│ │ └── api.test.ts
│ ├── visual/
│ │ └── components.test.ts
│ └── accessibility/
│ └── a11y.test.ts
├── data/
│ ├── fixtures/
│ └── test-data.json
├── playwright.config.ts
└── package.jsonclass TestContext {
private static instance: TestContext;
private data: Map<string, any> = new Map();
private constructor() {}
static getInstance(): TestContext {
if (!TestContext.instance) {
TestContext.instance = new TestContext();
}
return TestContext.instance;
}
set(key: string, value: any): void {
this.data.set(key, value);
}
get(key: string): any {
return this.data.get(key);
}
}class TestDataBuilder {
private data: Partial<User> = {};
withEmail(email: string): this {
this.data.email = email;
return this;
}
withName(name: string): this {
this.data.name = name;
return this;
}
withRole(role: string): this {
this.data.role = role;
return this;
}
build(): User {
return {
email: this.data.email || 'test@example.com',
name: this.data.name || 'Test User',
role: this.data.role || 'user',
...this.data,
} as User;
}
}