Initial commit
This commit is contained in:
470
agents/qa-engineer/templates/playwright-e2e-test.ts
Normal file
470
agents/qa-engineer/templates/playwright-e2e-test.ts
Normal 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)
|
||||
*/
|
||||
Reference in New Issue
Block a user