
Test Automation Framework
- 449 installs
- 305 repo stars
- Updated March 4, 2026
- aj-geddes/useful-ai-prompts
test-automation-framework is an AI prompt skill that designs layered test automation with fixtures, CI hooks, and reporting for developers who need reliable regression coverage instead of ad-hoc manual checks.
About
test-automation-framework is a skill from aj-geddes/useful-ai-prompts that guides design of a full test automation stack before new features land. It structures test layers, shared fixtures, CI pipeline hooks, and reporting so unit, integration, and end-to-end suites run on every change rather than relying on manual spot checks. The skill emphasizes repeatable patterns—where tests live, how data is seeded, how failures surface in CI, and how teams onboard new cases—so coverage scales with the product. Reach for test-automation-framework when bootstrapping QA for a greenfield service, refactoring a flaky suite, or formalizing regression gates ahead of continuous delivery. It produces an architecture blueprint and CI integration plan developers can implement with their chosen runner such as Jest, Pytest, or Playwright.
- Layered test pyramid guidance
- Fixture and mock patterns
- CI integration checklist
- Flake reduction strategies
- Coverage and reporting templates
Test Automation Framework by the numbers
- 449 all-time installs (skills.sh)
- Ranked #628 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/aj-geddes/useful-ai-prompts --skill test-automation-frameworkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 449 |
|---|---|
| repo stars | ★ 305 |
| Last updated | March 4, 2026 |
| Repository | aj-geddes/useful-ai-prompts ↗ |
How do you design a test automation framework with CI?
Design a test automation framework—layers, fixtures, CI hooks, and reporting—so new features ship with reliable regression coverage instead of ad-hoc manual checks.
Who is it for?
Developers establishing or refactoring automated regression coverage who need structured layers, fixtures, and CI integration instead of one-off test scripts.
Skip if: Teams that only need a single unit test file written without broader framework or pipeline design.
When should I use this skill?
A project lacks reliable regression automation or CI test hooks and needs a formal framework design before feature velocity outpaces manual QA.
What you get
Test layer architecture, fixture strategy, CI workflow hooks, and reporting plan for regression suites.
- Test automation architecture blueprint
- CI integration and reporting plan
Files
Test Automation Framework
Table of Contents
Overview
A test automation framework provides structure, reusability, and maintainability for automated tests. It defines patterns for organizing tests, managing test data, handling dependencies, and generating reports. A well-designed framework reduces duplication, improves reliability, and accelerates test development.
When to Use
- Setting up new test automation
- Scaling existing test suites
- Standardizing test practices across teams
- Reducing test maintenance burden
- Improving test reliability and speed
- Organizing large test codebases
- Implementing reusable test utilities
- Creating consistent reporting
Quick Start
Minimal working example:
// 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(),
]);
}
}
// ... (see reference guides for full implementation)Reference Guides
Detailed implementations in the references/ directory:
| Guide | Contents |
|---|---|
| Page Object Model (Playwright/TypeScript) | Page Object Model (Playwright/TypeScript) |
| Test Fixtures and Factories | Test Fixtures and Factories |
| Custom Test Utilities | Custom Test Utilities |
| Configuration Management | Configuration Management |
| Custom Reporter | Custom Reporter |
| pytest Framework (Python) | pytest Framework (Python) |
| Test Organization | Test Organization |
Best Practices
✅ DO
- Use Page Object Model for UI tests
- Create reusable test utilities
- Implement proper wait strategies
- Use fixtures for test data
- Configure for multiple environments
- Generate readable test reports
- Organize tests by feature/type
- Version control test framework
❌ DON'T
- Put test logic in page objects
- Use hard-coded waits (sleep)
- Duplicate test setup code
- Mix test data with test logic
- Skip error handling
- Ignore test flakiness
- Create overly complex abstractions
- Hardcode environment URLs
Configuration Management
Configuration Management
// 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"];Custom Reporter
Custom Reporter
// 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;Custom Test Utilities
Custom Test Utilities
// 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");
});Page Object Model (Playwright/TypeScript)
Page Object Model (Playwright/TypeScript)
// 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);
});
});pytest Framework (Python)
pytest Framework (Python)
# 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 Fixtures and Factories
Test Fixtures and Factories
// 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);
});Test Organization
Test Organization
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.json#!/bin/bash
# scaffold-tests.sh - Generate test file scaffolding
# Usage: ./scaffold-tests.sh <source_file> [--framework jest|pytest|mocha]
set -euo pipefail
SOURCE_FILE="${{1:?Usage: $0 <source_file> [--framework jest|pytest|mocha]}}"
FRAMEWORK="${{2:-jest}}"
echo "Scaffolding tests for: $SOURCE_FILE (framework: $FRAMEWORK)"
# TODO: Implement test scaffolding logic
# - Parse source file for exported functions/classes
# - Generate test stubs for each export
# - Include setup/teardown boilerplate
# - Add common assertion patterns
echo "Test scaffolding complete."
// Test Template
// TODO: Customize for your testing framework and project
describe('ModuleName', () => {
// Setup
beforeEach(() => {
// TODO: Add test setup
});
afterEach(() => {
// TODO: Add cleanup
});
describe('functionName', () => {
it('should handle the happy path', () => {
// TODO: Add assertion
});
it('should handle edge cases', () => {
// TODO: Add edge case tests
});
it('should handle errors gracefully', () => {
// TODO: Add error handling tests
});
});
});
Related skills
How it compares
Pick test-automation-framework over test-data-generation when the goal is suite architecture and CI wiring rather than synthetic dataset creation.
FAQ
What does test-automation-framework deliver?
test-automation-framework delivers a structured automation architecture: test layers, shared fixtures, CI hooks, and reporting conventions. Developers use the blueprint to implement suites that run on every push instead of relying on manual regression checks.
When should developers use test-automation-framework?
test-automation-framework fits greenfield services, flaky suite refactors, or pre-CD hardening when regression coverage is thin. The skill focuses on framework design—layers, fixtures, and CI integration—not writing individual assertions in isolation.