Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:57:09 +08:00
commit 205830d396
16 changed files with 8845 additions and 0 deletions

View File

@@ -0,0 +1,470 @@
/**
* Playwright E2E Test Template
*
* This template demonstrates best practices for writing end-to-end tests
* with Playwright for web applications.
*
* Features:
* - Page Object Model (POM)
* - Test fixtures
* - Cross-browser testing
* - Mobile emulation
* - API mocking
* - Visual regression
* - Accessibility testing
*/
import { test, expect, Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
// ============================================================================
// PAGE OBJECTS
// ============================================================================
/**
* Login Page Object
*
* Encapsulates all interactions with the login page
*/
class LoginPage {
constructor(private page: Page) {}
// Locators
get emailInput() {
return this.page.getByLabel('Email');
}
get passwordInput() {
return this.page.getByLabel('Password');
}
get loginButton() {
return this.page.getByRole('button', { name: 'Login' });
}
get errorMessage() {
return this.page.getByRole('alert');
}
// Actions
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
/**
* Dashboard Page Object
*/
class DashboardPage {
constructor(private page: Page) {}
get welcomeMessage() {
return this.page.getByText(/Welcome,/);
}
get logoutButton() {
return this.page.getByRole('button', { name: 'Logout' });
}
async expectLoggedIn(username: string) {
await expect(this.welcomeMessage).toContainText(username);
}
async logout() {
await this.logoutButton.click();
}
}
// ============================================================================
// TEST FIXTURES
// ============================================================================
/**
* Custom fixture that provides authenticated page
*/
const test = base.extend<{ authenticatedPage: Page }>({
authenticatedPage: async ({ page }, use) => {
// Setup: Login before test
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
// Wait for navigation to dashboard
await page.waitForURL('/dashboard');
// Provide authenticated page to test
await use(page);
// Teardown: Logout after test
const dashboardPage = new DashboardPage(page);
await dashboardPage.logout();
},
});
// ============================================================================
// BASIC E2E TESTS
// ============================================================================
test.describe('Authentication Flow', () => {
test('should login successfully with valid credentials', async ({ page }) => {
// ARRANGE
const loginPage = new LoginPage(page);
await loginPage.goto();
// ACT
await loginPage.login('user@example.com', 'password123');
// ASSERT
await expect(page).toHaveURL('/dashboard');
const dashboardPage = new DashboardPage(page);
await dashboardPage.expectLoggedIn('User');
});
test('should show error for invalid credentials', async ({ page }) => {
// ARRANGE
const loginPage = new LoginPage(page);
await loginPage.goto();
// ACT
await loginPage.login('invalid@example.com', 'wrongpassword');
// ASSERT
await loginPage.expectError('Invalid email or password');
await expect(page).toHaveURL('/login'); // Still on login page
});
test('should show validation errors for empty fields', async ({ page }) => {
// ARRANGE
const loginPage = new LoginPage(page);
await loginPage.goto();
// ACT
await loginPage.loginButton.click();
// ASSERT
await expect(page.getByText('Email is required')).toBeVisible();
await expect(page.getByText('Password is required')).toBeVisible();
});
});
// ============================================================================
// AUTHENTICATED TESTS (Using Fixture)
// ============================================================================
test.describe('Dashboard Features', () => {
test('should display user profile', async ({ authenticatedPage }) => {
// Navigate to profile
await authenticatedPage.goto('/profile');
// Verify profile data
await expect(
authenticatedPage.getByText('user@example.com')
).toBeVisible();
await expect(authenticatedPage.getByText('Member since')).toBeVisible();
});
test('should allow editing profile', async ({ authenticatedPage }) => {
// Navigate to profile
await authenticatedPage.goto('/profile');
// Edit name
const nameInput = authenticatedPage.getByLabel('Name');
await nameInput.clear();
await nameInput.fill('New Name');
// Save changes
await authenticatedPage
.getByRole('button', { name: 'Save Changes' })
.click();
// Verify success
await expect(
authenticatedPage.getByText('Profile updated successfully')
).toBeVisible();
});
});
// ============================================================================
// API MOCKING
// ============================================================================
test.describe('API Mocking', () => {
test('should handle API errors gracefully', async ({ page }) => {
// Mock API to return error
await page.route('**/api/users', (route) => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
// Navigate to users page
await page.goto('/users');
// Verify error message
await expect(
page.getByText('Failed to load users. Please try again.')
).toBeVisible();
});
test('should display mocked user data', async ({ page }) => {
// Mock API to return test data
await page.route('**/api/users', (route) => {
route.fulfill({
status: 200,
body: JSON.stringify([
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' },
]),
});
});
// Navigate to users page
await page.goto('/users');
// Verify mocked data is displayed
await expect(page.getByText('John Doe')).toBeVisible();
await expect(page.getByText('Jane Smith')).toBeVisible();
});
});
// ============================================================================
// VISUAL REGRESSION TESTING
// ============================================================================
test.describe('Visual Regression', () => {
test('homepage matches baseline', async ({ page }) => {
await page.goto('/');
// Capture screenshot and compare to baseline
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
animations: 'disabled',
});
});
test('button states match baseline', async ({ page }) => {
await page.goto('/components');
const button = page.getByRole('button', { name: 'Submit' });
// Default state
await expect(button).toHaveScreenshot('button-default.png');
// Hover state
await button.hover();
await expect(button).toHaveScreenshot('button-hover.png');
// Focus state
await button.focus();
await expect(button).toHaveScreenshot('button-focus.png');
});
});
// ============================================================================
// ACCESSIBILITY TESTING
// ============================================================================
test.describe('Accessibility', () => {
test('should not have accessibility violations', async ({ page }) => {
await page.goto('/');
// Run axe accessibility scan
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze();
// Assert no violations
expect(accessibilityScanResults.violations).toEqual([]);
});
test('should support keyboard navigation', async ({ page }) => {
await page.goto('/form');
// Tab through form fields
await page.keyboard.press('Tab');
await expect(page.getByLabel('Email')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByLabel('Password')).toBeFocused();
await page.keyboard.press('Tab');
await expect(
page.getByRole('button', { name: 'Submit' })
).toBeFocused();
// Submit with Enter
await page.keyboard.press('Enter');
});
test('should have proper ARIA labels', async ({ page }) => {
await page.goto('/');
// Check navigation has aria-label
await expect(
page.getByRole('navigation', { name: 'Main navigation' })
).toBeVisible();
// Check main content has aria-label
await expect(page.getByRole('main')).toHaveAttribute(
'aria-label',
'Main content'
);
// Check all images have alt text
const images = await page.getByRole('img').all();
for (const img of images) {
await expect(img).toHaveAttribute('alt');
}
});
});
// ============================================================================
// MOBILE TESTING
// ============================================================================
test.describe('Mobile Experience', () => {
test.use({ viewport: { width: 375, height: 667 } }); // iPhone SE
test('should render mobile navigation', async ({ page }) => {
await page.goto('/');
// Mobile menu button should be visible
await expect(
page.getByRole('button', { name: 'Menu' })
).toBeVisible();
// Desktop navigation should be hidden
const desktopNav = page.getByRole('navigation').first();
await expect(desktopNav).toBeHidden();
});
test('should handle touch gestures', async ({ page }) => {
await page.goto('/gallery');
// Get image element
const image = page.getByRole('img').first();
// Swipe left
await image.dispatchEvent('touchstart', {
touches: [{ clientX: 300, clientY: 200 }],
});
await image.dispatchEvent('touchmove', {
touches: [{ clientX: 100, clientY: 200 }],
});
await image.dispatchEvent('touchend');
// Verify navigation to next image
await expect(page.getByText('Image 2 of 10')).toBeVisible();
});
});
// ============================================================================
// PERFORMANCE TESTING
// ============================================================================
test.describe('Performance', () => {
test('page load performance', async ({ page }) => {
await page.goto('/');
// Get performance metrics
const performanceMetrics = await page.evaluate(() => {
const perfData = window.performance.timing;
return {
loadTime: perfData.loadEventEnd - perfData.navigationStart,
domContentLoaded:
perfData.domContentLoadedEventEnd - perfData.navigationStart,
};
});
// Assert performance targets
expect(performanceMetrics.loadTime).toBeLessThan(3000); // 3s max
expect(performanceMetrics.domContentLoaded).toBeLessThan(2000); // 2s max
});
});
// ============================================================================
// FILE UPLOAD/DOWNLOAD
// ============================================================================
test.describe('File Operations', () => {
test('should upload file', async ({ page }) => {
await page.goto('/upload');
// Set up file chooser
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByRole('button', { name: 'Upload File' }).click();
const fileChooser = await fileChooserPromise;
// Upload file
await fileChooser.setFiles('tests/fixtures/test-file.pdf');
// Verify upload success
await expect(page.getByText('File uploaded successfully')).toBeVisible();
});
test('should download file', async ({ page }) => {
await page.goto('/downloads');
// Set up download
const downloadPromise = page.waitForEvent('download');
await page.getByRole('link', { name: 'Download Report' }).click();
const download = await downloadPromise;
// Verify download
expect(download.suggestedFilename()).toBe('report.pdf');
await download.saveAs(`/tmp/${download.suggestedFilename()}`);
});
});
// ============================================================================
// MULTI-TAB/WINDOW TESTING
// ============================================================================
test.describe('Multi-Window', () => {
test('should handle popup windows', async ({ context, page }) => {
await page.goto('/');
// Wait for popup
const [popup] = await Promise.all([
context.waitForEvent('page'),
page.getByRole('button', { name: 'Open Help' }).click(),
]);
// Interact with popup
await expect(popup.getByText('Help Center')).toBeVisible();
await popup.close();
});
});
// ============================================================================
// BEST PRACTICES CHECKLIST
// ============================================================================
/*
✅ Page Object Model (POM)
✅ Test Fixtures for Setup/Teardown
✅ Descriptive Test Names
✅ Auto-Waiting (Playwright built-in)
✅ User-Centric Selectors (getByRole, getByLabel)
✅ API Mocking for Reliability
✅ Visual Regression Testing
✅ Accessibility Testing (axe-core)
✅ Mobile/Responsive Testing
✅ Performance Assertions
✅ File Upload/Download
✅ Multi-Tab/Window Handling
✅ Screenshot/Video on Failure (configured in playwright.config.ts)
✅ Parallel Execution (configured in playwright.config.ts)
✅ Cross-Browser Testing (configured in playwright.config.ts)
*/

View File

@@ -0,0 +1,507 @@
/**
* Test Data Factory Template
*
* This template demonstrates best practices for creating reusable
* test data factories using the Factory pattern.
*
* Benefits:
* - Consistent test data generation
* - Easy to customize with overrides
* - Reduces test setup boilerplate
* - Type-safe with TypeScript
*/
import { faker } from '@faker-js/faker';
// ============================================================================
// TYPES
// ============================================================================
export interface User {
id: string;
email: string;
username: string;
firstName: string;
lastName: string;
role: 'admin' | 'user' | 'guest';
isActive: boolean;
createdAt: Date;
updatedAt: Date;
profile?: UserProfile;
}
export interface UserProfile {
bio: string;
avatar: string;
phoneNumber: string;
address: Address;
}
export interface Address {
street: string;
city: string;
state: string;
zipCode: string;
country: string;
}
export interface Product {
id: string;
name: string;
description: string;
price: number;
category: string;
inStock: boolean;
quantity: number;
imageUrl: string;
createdAt: Date;
}
export interface Order {
id: string;
userId: string;
items: OrderItem[];
subtotal: number;
tax: number;
total: number;
status: 'pending' | 'processing' | 'shipped' | 'delivered' | 'cancelled';
shippingAddress: Address;
createdAt: Date;
updatedAt: Date;
}
export interface OrderItem {
productId: string;
quantity: number;
price: number;
}
// ============================================================================
// FACTORY FUNCTIONS
// ============================================================================
/**
* User Factory
*
* Creates realistic user test data with sensible defaults
*/
export class UserFactory {
/**
* Create a single user
*
* @param overrides - Partial user object to override defaults
* @returns Complete user object
*
* @example
* ```ts
* const admin = UserFactory.create({ role: 'admin' });
* const inactiveUser = UserFactory.create({ isActive: false });
* ```
*/
static create(overrides: Partial<User> = {}): User {
const firstName = faker.person.firstName();
const lastName = faker.person.lastName();
const email =
overrides.email || faker.internet.email({ firstName, lastName });
return {
id: faker.string.uuid(),
email,
username: faker.internet.userName({ firstName, lastName }),
firstName,
lastName,
role: 'user',
isActive: true,
createdAt: faker.date.past(),
updatedAt: faker.date.recent(),
...overrides,
};
}
/**
* Create multiple users
*
* @param count - Number of users to create
* @param overrides - Partial user object to override defaults for all users
* @returns Array of user objects
*
* @example
* ```ts
* const users = UserFactory.createMany(5);
* const admins = UserFactory.createMany(3, { role: 'admin' });
* ```
*/
static createMany(count: number, overrides: Partial<User> = {}): User[] {
return Array.from({ length: count }, () => this.create(overrides));
}
/**
* Create admin user
*
* @param overrides - Partial user object to override defaults
* @returns Admin user object
*/
static createAdmin(overrides: Partial<User> = {}): User {
return this.create({
role: 'admin',
...overrides,
});
}
/**
* Create user with complete profile
*
* @param overrides - Partial user object to override defaults
* @returns User with profile object
*/
static createWithProfile(overrides: Partial<User> = {}): User {
return this.create({
profile: {
bio: faker.person.bio(),
avatar: faker.image.avatar(),
phoneNumber: faker.phone.number(),
address: AddressFactory.create(),
},
...overrides,
});
}
/**
* Create inactive user
*
* @param overrides - Partial user object to override defaults
* @returns Inactive user object
*/
static createInactive(overrides: Partial<User> = {}): User {
return this.create({
isActive: false,
...overrides,
});
}
}
/**
* Address Factory
*
* Creates realistic address test data
*/
export class AddressFactory {
static create(overrides: Partial<Address> = {}): Address {
return {
street: faker.location.streetAddress(),
city: faker.location.city(),
state: faker.location.state(),
zipCode: faker.location.zipCode(),
country: faker.location.country(),
...overrides,
};
}
static createUS(overrides: Partial<Address> = {}): Address {
return this.create({
country: 'United States',
zipCode: faker.location.zipCode('#####'),
state: faker.location.state({ abbreviated: true }),
...overrides,
});
}
}
/**
* Product Factory
*
* Creates realistic product test data
*/
export class ProductFactory {
static create(overrides: Partial<Product> = {}): Product {
return {
id: faker.string.uuid(),
name: faker.commerce.productName(),
description: faker.commerce.productDescription(),
price: parseFloat(faker.commerce.price()),
category: faker.commerce.department(),
inStock: true,
quantity: faker.number.int({ min: 0, max: 100 }),
imageUrl: faker.image.url(),
createdAt: faker.date.past(),
...overrides,
};
}
static createMany(count: number, overrides: Partial<Product> = {}): Product[] {
return Array.from({ length: count }, () => this.create(overrides));
}
static createOutOfStock(overrides: Partial<Product> = {}): Product {
return this.create({
inStock: false,
quantity: 0,
...overrides,
});
}
static createExpensive(overrides: Partial<Product> = {}): Product {
return this.create({
price: faker.number.int({ min: 1000, max: 10000 }),
...overrides,
});
}
}
/**
* Order Factory
*
* Creates realistic order test data with items
*/
export class OrderFactory {
static create(overrides: Partial<Order> = {}): Order {
const items = overrides.items || [
{
productId: faker.string.uuid(),
quantity: faker.number.int({ min: 1, max: 5 }),
price: parseFloat(faker.commerce.price()),
},
];
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
const tax = subtotal * 0.08; // 8% tax
const total = subtotal + tax;
return {
id: faker.string.uuid(),
userId: faker.string.uuid(),
items,
subtotal,
tax,
total,
status: 'pending',
shippingAddress: AddressFactory.createUS(),
createdAt: faker.date.recent(),
updatedAt: faker.date.recent(),
...overrides,
};
}
static createMany(count: number, overrides: Partial<Order> = {}): Order[] {
return Array.from({ length: count }, () => this.create(overrides));
}
static createWithItems(items: OrderItem[], overrides: Partial<Order> = {}): Order {
return this.create({ items, ...overrides });
}
static createShipped(overrides: Partial<Order> = {}): Order {
return this.create({
status: 'shipped',
...overrides,
});
}
static createCancelled(overrides: Partial<Order> = {}): Order {
return this.create({
status: 'cancelled',
...overrides,
});
}
}
// ============================================================================
// BUILDER PATTERN (Advanced)
// ============================================================================
/**
* User Builder
*
* Provides a fluent interface for building complex user objects
*
* @example
* ```ts
* const user = new UserBuilder()
* .withEmail('admin@example.com')
* .withRole('admin')
* .withProfile()
* .build();
* ```
*/
export class UserBuilder {
private user: Partial<User> = {};
withId(id: string): this {
this.user.id = id;
return this;
}
withEmail(email: string): this {
this.user.email = email;
return this;
}
withUsername(username: string): this {
this.user.username = username;
return this;
}
withName(firstName: string, lastName: string): this {
this.user.firstName = firstName;
this.user.lastName = lastName;
return this;
}
withRole(role: User['role']): this {
this.user.role = role;
return this;
}
withProfile(profile?: UserProfile): this {
this.user.profile = profile || {
bio: faker.person.bio(),
avatar: faker.image.avatar(),
phoneNumber: faker.phone.number(),
address: AddressFactory.create(),
};
return this;
}
inactive(): this {
this.user.isActive = false;
return this;
}
active(): this {
this.user.isActive = true;
return this;
}
build(): User {
return UserFactory.create(this.user);
}
}
// ============================================================================
// USAGE EXAMPLES
// ============================================================================
/**
* Example: Simple user creation
*/
export function exampleSimpleUser() {
const user = UserFactory.create();
const admin = UserFactory.createAdmin();
const users = UserFactory.createMany(5);
return { user, admin, users };
}
/**
* Example: Customized user creation
*/
export function exampleCustomUser() {
const user = UserFactory.create({
email: 'custom@example.com',
role: 'admin',
isActive: false,
});
return user;
}
/**
* Example: Builder pattern
*/
export function exampleBuilder() {
const user = new UserBuilder()
.withEmail('builder@example.com')
.withName('John', 'Doe')
.withRole('admin')
.withProfile()
.active()
.build();
return user;
}
/**
* Example: Order with products
*/
export function exampleOrder() {
// Create products
const products = ProductFactory.createMany(3);
// Create order items from products
const items: OrderItem[] = products.map((product) => ({
productId: product.id,
quantity: faker.number.int({ min: 1, max: 3 }),
price: product.price,
}));
// Create order with items
const order = OrderFactory.createWithItems(items);
return { products, order };
}
// ============================================================================
// TEST USAGE
// ============================================================================
/**
* Example test using factories
*/
import { describe, it, expect } from 'vitest';
describe('UserService (using factories)', () => {
it('should create user', () => {
// ARRANGE
const userData = UserFactory.create();
// ACT
const result = userService.create(userData);
// ASSERT
expect(result.id).toBeDefined();
expect(result.email).toBe(userData.email);
});
it('should only allow admins to delete users', () => {
// ARRANGE
const admin = UserFactory.createAdmin();
const regularUser = UserFactory.create();
// ACT & ASSERT
expect(() => userService.deleteUser(regularUser.id, admin)).not.toThrow();
expect(() => userService.deleteUser(admin.id, regularUser)).toThrow('Unauthorized');
});
it('should calculate order total correctly', () => {
// ARRANGE
const order = OrderFactory.create({
items: [
{ productId: '1', quantity: 2, price: 50 },
{ productId: '2', quantity: 1, price: 30 },
],
});
// ACT
const total = orderService.calculateTotal(order);
// ASSERT
expect(total).toBe(140.4); // (50*2 + 30*1) * 1.08 tax
});
});
// ============================================================================
// BEST PRACTICES
// ============================================================================
/*
✅ Use realistic data (faker.js)
✅ Provide sensible defaults
✅ Allow overrides for customization
✅ Type-safe with TypeScript
✅ Create helper methods (createAdmin, createInactive, etc.)
✅ Builder pattern for complex objects
✅ Consistent naming (create, createMany, createWith...)
✅ Document with JSDoc
✅ Export all factories for reuse
✅ Keep factories simple and focused
*/

View File

@@ -0,0 +1,400 @@
/**
* Vitest Unit Test Template
*
* This template demonstrates best practices for writing unit tests
* with Vitest for TypeScript projects.
*
* Features:
* - AAA pattern (Arrange-Act-Assert)
* - Test isolation
* - Mocking dependencies
* - Parametric testing
* - Error handling tests
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
// Import the module under test
import { YourModule } from './YourModule';
// Import dependencies (to be mocked)
import { ExternalDependency } from './ExternalDependency';
// ============================================================================
// MOCKS
// ============================================================================
// Mock external dependencies
vi.mock('./ExternalDependency', () => ({
ExternalDependency: vi.fn().mockImplementation(() => ({
fetchData: vi.fn().mockResolvedValue({ data: 'mocked' }),
processData: vi.fn(),
})),
}));
// ============================================================================
// TEST SUITE
// ============================================================================
describe('YourModule', () => {
// ========================================================================
// SETUP & TEARDOWN
// ========================================================================
let instance: YourModule;
let mockDependency: any;
beforeEach(() => {
// ARRANGE: Set up fresh instance for each test
mockDependency = new ExternalDependency();
instance = new YourModule(mockDependency);
});
afterEach(() => {
// TEARDOWN: Clean up mocks
vi.clearAllMocks();
});
// ========================================================================
// HAPPY PATH TESTS
// ========================================================================
describe('methodName', () => {
it('should perform expected operation with valid input', () => {
// ARRANGE
const input = 'test-input';
const expectedOutput = 'test-output';
// ACT
const result = instance.methodName(input);
// ASSERT
expect(result).toBe(expectedOutput);
});
it('should call dependency with correct parameters', () => {
// ARRANGE
const input = 'test-input';
// ACT
instance.methodName(input);
// ASSERT
expect(mockDependency.processData).toHaveBeenCalledWith(input);
expect(mockDependency.processData).toHaveBeenCalledTimes(1);
});
});
// ========================================================================
// ASYNC TESTS
// ========================================================================
describe('asyncMethod', () => {
it('should resolve with data on success', async () => {
// ARRANGE
const mockData = { id: 1, value: 'test' };
mockDependency.fetchData.mockResolvedValue(mockData);
// ACT
const result = await instance.asyncMethod();
// ASSERT
expect(result).toEqual(mockData);
expect(mockDependency.fetchData).toHaveBeenCalled();
});
it('should handle async errors gracefully', async () => {
// ARRANGE
const error = new Error('Network error');
mockDependency.fetchData.mockRejectedValue(error);
// ACT & ASSERT
await expect(instance.asyncMethod()).rejects.toThrow('Network error');
});
});
// ========================================================================
// EDGE CASES
// ========================================================================
describe('edge cases', () => {
it('should handle empty input', () => {
// ACT
const result = instance.methodName('');
// ASSERT
expect(result).toBe('');
});
it('should handle null input', () => {
// ACT
const result = instance.methodName(null as any);
// ASSERT
expect(result).toBeNull();
});
it('should handle undefined input', () => {
// ACT
const result = instance.methodName(undefined as any);
// ASSERT
expect(result).toBeUndefined();
});
it('should handle very large input', () => {
// ARRANGE
const largeInput = 'x'.repeat(1000000);
// ACT
const result = instance.methodName(largeInput);
// ASSERT
expect(result).toBeDefined();
});
});
// ========================================================================
// ERROR HANDLING
// ========================================================================
describe('error handling', () => {
it('should throw error for invalid input', () => {
// ARRANGE
const invalidInput = -1;
// ACT & ASSERT
expect(() => instance.methodName(invalidInput)).toThrow(
'Input must be positive'
);
});
it('should throw specific error type', () => {
// ARRANGE
const invalidInput = 'invalid';
// ACT & ASSERT
expect(() => instance.methodName(invalidInput)).toThrow(ValidationError);
});
it('should include error details', () => {
// ARRANGE
const invalidInput = 'invalid';
// ACT
try {
instance.methodName(invalidInput);
fail('Expected error to be thrown');
} catch (error) {
// ASSERT
expect(error).toBeInstanceOf(ValidationError);
expect(error.message).toContain('invalid');
expect(error.code).toBe('INVALID_INPUT');
}
});
});
// ========================================================================
// PARAMETRIC TESTS (Table-Driven)
// ========================================================================
describe.each([
{ input: 1, expected: 2 },
{ input: 2, expected: 4 },
{ input: 3, expected: 6 },
{ input: 4, expected: 8 },
])('methodName($input)', ({ input, expected }) => {
it(`should return ${expected}`, () => {
// ACT
const result = instance.methodName(input);
// ASSERT
expect(result).toBe(expected);
});
});
// ========================================================================
// STATE MANAGEMENT TESTS
// ========================================================================
describe('state management', () => {
it('should update internal state correctly', () => {
// ARRANGE
const initialState = instance.getState();
// ACT
instance.updateState('new-value');
// ASSERT
const newState = instance.getState();
expect(newState).not.toBe(initialState);
expect(newState).toBe('new-value');
});
it('should emit events on state change', () => {
// ARRANGE
const eventHandler = vi.fn();
instance.on('stateChanged', eventHandler);
// ACT
instance.updateState('new-value');
// ASSERT
expect(eventHandler).toHaveBeenCalledWith('new-value');
});
});
// ========================================================================
// PERFORMANCE TESTS
// ========================================================================
describe('performance', () => {
it('should complete within reasonable time', () => {
// ARRANGE
const startTime = Date.now();
// ACT
instance.methodName('test');
// ASSERT
const endTime = Date.now();
const executionTime = endTime - startTime;
expect(executionTime).toBeLessThan(100); // 100ms threshold
});
it('should handle large datasets efficiently', () => {
// ARRANGE
const largeDataset = Array.from({ length: 10000 }, (_, i) => i);
// ACT
const startTime = Date.now();
const result = instance.processBatch(largeDataset);
const endTime = Date.now();
// ASSERT
expect(result).toHaveLength(10000);
expect(endTime - startTime).toBeLessThan(1000); // 1s threshold
});
});
// ========================================================================
// SNAPSHOT TESTS
// ========================================================================
describe('snapshots', () => {
it('should match snapshot for complex output', () => {
// ARRANGE
const input = { id: 1, name: 'Test', nested: { value: 42 } };
// ACT
const result = instance.transform(input);
// ASSERT
expect(result).toMatchSnapshot();
});
it('should match inline snapshot for simple output', () => {
// ARRANGE
const input = 'test';
// ACT
const result = instance.methodName(input);
// ASSERT
expect(result).toMatchInlineSnapshot('"test-output"');
});
});
// ========================================================================
// TIMER/DEBOUNCE TESTS
// ========================================================================
describe('timers', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.restoreAllTimers();
});
it('should debounce function calls', () => {
// ARRANGE
const callback = vi.fn();
const debounced = instance.debounce(callback, 1000);
// ACT
debounced();
debounced();
debounced();
// ASSERT (not called yet)
expect(callback).not.toHaveBeenCalled();
// Fast-forward time
vi.advanceTimersByTime(1000);
// ASSERT (called once)
expect(callback).toHaveBeenCalledTimes(1);
});
it('should throttle function calls', () => {
// ARRANGE
const callback = vi.fn();
const throttled = instance.throttle(callback, 1000);
// ACT
throttled(); // Called immediately
throttled(); // Ignored (within throttle window)
throttled(); // Ignored (within throttle window)
// ASSERT
expect(callback).toHaveBeenCalledTimes(1);
// Fast-forward time
vi.advanceTimersByTime(1000);
// ACT
throttled(); // Called after throttle window
// ASSERT
expect(callback).toHaveBeenCalledTimes(2);
});
});
});
// ============================================================================
// HELPER TYPES & CLASSES (For Examples Above)
// ============================================================================
class ValidationError extends Error {
constructor(
message: string,
public code: string
) {
super(message);
this.name = 'ValidationError';
}
}
// ============================================================================
// BEST PRACTICES CHECKLIST
// ============================================================================
/*
✅ AAA Pattern (Arrange-Act-Assert)
✅ Test Isolation (beforeEach creates fresh instance)
✅ Descriptive Test Names (should do X when Y)
✅ One Assertion Per Test (when possible)
✅ Mock External Dependencies
✅ Test Happy Path
✅ Test Edge Cases (null, undefined, empty, large)
✅ Test Error Handling
✅ Parametric Tests (test.each)
✅ Async Testing (async/await, rejects, resolves)
✅ Timer Testing (useFakeTimers)
✅ Performance Testing (execution time)
✅ Snapshot Testing (complex outputs)
✅ No Shared State Between Tests
✅ Fast Execution (< 1s per test)
*/