412 lines
12 KiB
TypeScript
412 lines
12 KiB
TypeScript
|
|
/**
|
||
|
|
* EXAMPLE TEST FILE
|
||
|
|
*
|
||
|
|
* This file demonstrates best practices for writing Playwright tests
|
||
|
|
* Use this as a template when creating new tests
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { test, expect } from '@playwright/test';
|
||
|
|
import { login, logout, TEST_USER } from './helpers/auth';
|
||
|
|
import {
|
||
|
|
waitForLoadingToFinish,
|
||
|
|
expectToastMessage,
|
||
|
|
generateTestId,
|
||
|
|
mockApiResponse
|
||
|
|
} from './helpers/utils';
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// BASIC TEST STRUCTURE
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test.describe('Feature Name', () => {
|
||
|
|
// Use authenticated state for tests that require login
|
||
|
|
test.use({ storageState: 'tests/.auth/user.json' });
|
||
|
|
|
||
|
|
// Setup that runs before each test
|
||
|
|
test.beforeEach(async ({ page }) => {
|
||
|
|
await page.goto('/your-page');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Cleanup after each test (if needed)
|
||
|
|
test.afterEach(async ({ page }) => {
|
||
|
|
// Clean up test data if needed
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should display page correctly', async ({ page }) => {
|
||
|
|
// Wait for page to load
|
||
|
|
await waitForLoadingToFinish(page);
|
||
|
|
|
||
|
|
// Verify page elements
|
||
|
|
await expect(page.getByRole('heading', { name: /page title/i })).toBeVisible();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// AUTHENTICATION TESTS
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test.describe('Authentication Example', () => {
|
||
|
|
test('should login manually', async ({ page }) => {
|
||
|
|
// Use helper function
|
||
|
|
await login(page, TEST_USER);
|
||
|
|
|
||
|
|
// Verify login success
|
||
|
|
await expect(page).toHaveURL(/\/app/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should logout', async ({ page }) => {
|
||
|
|
// Login first
|
||
|
|
await login(page, TEST_USER);
|
||
|
|
|
||
|
|
// Logout
|
||
|
|
await logout(page);
|
||
|
|
|
||
|
|
// Verify logged out
|
||
|
|
await expect(page).toHaveURL(/\/login/);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// FORM INTERACTIONS
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test.describe('Form Submission Example', () => {
|
||
|
|
test.use({ storageState: 'tests/.auth/user.json' });
|
||
|
|
|
||
|
|
test('should submit form successfully', async ({ page }) => {
|
||
|
|
await page.goto('/app/form-page');
|
||
|
|
|
||
|
|
// Fill form fields
|
||
|
|
await page.getByLabel(/name/i).fill('Test Name');
|
||
|
|
await page.getByLabel(/email/i).fill('test@example.com');
|
||
|
|
await page.getByLabel(/description/i).fill('Test description');
|
||
|
|
|
||
|
|
// Select from dropdown
|
||
|
|
await page.getByLabel(/category/i).click();
|
||
|
|
await page.getByRole('option', { name: 'Option 1' }).click();
|
||
|
|
|
||
|
|
// Check checkbox
|
||
|
|
await page.getByLabel(/agree to terms/i).check();
|
||
|
|
|
||
|
|
// Submit form
|
||
|
|
await page.getByRole('button', { name: /submit/i }).click();
|
||
|
|
|
||
|
|
// Verify success
|
||
|
|
await expectToastMessage(page, /success/i);
|
||
|
|
await expect(page).toHaveURL(/\/success/);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should show validation errors', async ({ page }) => {
|
||
|
|
await page.goto('/app/form-page');
|
||
|
|
|
||
|
|
// Try to submit empty form
|
||
|
|
await page.getByRole('button', { name: /submit/i }).click();
|
||
|
|
|
||
|
|
// Verify error messages
|
||
|
|
await expect(page.getByText(/name.*required/i)).toBeVisible();
|
||
|
|
await expect(page.getByText(/email.*required/i)).toBeVisible();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// API MOCKING
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test.describe('API Mocking Example', () => {
|
||
|
|
test.use({ storageState: 'tests/.auth/user.json' });
|
||
|
|
|
||
|
|
test('should handle API response', async ({ page }) => {
|
||
|
|
// Mock successful API response
|
||
|
|
await mockApiResponse(
|
||
|
|
page,
|
||
|
|
'**/api/products',
|
||
|
|
{
|
||
|
|
products: [
|
||
|
|
{ id: 1, name: 'Product 1', price: 9.99 },
|
||
|
|
{ id: 2, name: 'Product 2', price: 19.99 },
|
||
|
|
],
|
||
|
|
},
|
||
|
|
200
|
||
|
|
);
|
||
|
|
|
||
|
|
await page.goto('/app/products');
|
||
|
|
|
||
|
|
// Verify mocked data is displayed
|
||
|
|
await expect(page.getByText('Product 1')).toBeVisible();
|
||
|
|
await expect(page.getByText('Product 2')).toBeVisible();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should handle API error', async ({ page }) => {
|
||
|
|
// Mock error response
|
||
|
|
await mockApiResponse(
|
||
|
|
page,
|
||
|
|
'**/api/products',
|
||
|
|
{ error: 'Failed to fetch products' },
|
||
|
|
500
|
||
|
|
);
|
||
|
|
|
||
|
|
await page.goto('/app/products');
|
||
|
|
|
||
|
|
// Verify error message is shown
|
||
|
|
await expect(page.getByText(/error|failed/i)).toBeVisible();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// FILE UPLOAD
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test.describe('File Upload Example', () => {
|
||
|
|
test.use({ storageState: 'tests/.auth/user.json' });
|
||
|
|
|
||
|
|
test('should upload file', async ({ page }) => {
|
||
|
|
await page.goto('/app/upload');
|
||
|
|
|
||
|
|
// Upload file
|
||
|
|
const fileInput = page.locator('input[type="file"]');
|
||
|
|
await fileInput.setInputFiles('tests/fixtures/sample-inventory.csv');
|
||
|
|
|
||
|
|
// Verify file uploaded
|
||
|
|
await expect(page.getByText('sample-inventory.csv')).toBeVisible();
|
||
|
|
|
||
|
|
// Submit
|
||
|
|
await page.getByRole('button', { name: /upload/i }).click();
|
||
|
|
|
||
|
|
// Verify success
|
||
|
|
await expectToastMessage(page, /uploaded successfully/i);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// NAVIGATION
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test.describe('Navigation Example', () => {
|
||
|
|
test.use({ storageState: 'tests/.auth/user.json' });
|
||
|
|
|
||
|
|
test('should navigate between pages', async ({ page }) => {
|
||
|
|
// Start at dashboard
|
||
|
|
await page.goto('/app/dashboard');
|
||
|
|
|
||
|
|
// Click navigation link
|
||
|
|
await page.getByRole('link', { name: /operations/i }).click();
|
||
|
|
|
||
|
|
// Verify navigation
|
||
|
|
await expect(page).toHaveURL(/\/operations/);
|
||
|
|
await expect(page.getByRole('heading', { name: /operations/i })).toBeVisible();
|
||
|
|
|
||
|
|
// Navigate back
|
||
|
|
await page.goBack();
|
||
|
|
await expect(page).toHaveURL(/\/dashboard/);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// MODALS AND DIALOGS
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test.describe('Modal Example', () => {
|
||
|
|
test.use({ storageState: 'tests/.auth/user.json' });
|
||
|
|
|
||
|
|
test('should open and close modal', async ({ page }) => {
|
||
|
|
await page.goto('/app/dashboard');
|
||
|
|
|
||
|
|
// Open modal
|
||
|
|
await page.getByRole('button', { name: /open modal/i }).click();
|
||
|
|
|
||
|
|
// Verify modal is visible
|
||
|
|
await expect(page.getByRole('dialog')).toBeVisible();
|
||
|
|
|
||
|
|
// Close modal
|
||
|
|
await page.getByRole('button', { name: /close|cancel/i }).click();
|
||
|
|
|
||
|
|
// Verify modal is closed
|
||
|
|
await expect(page.getByRole('dialog')).not.toBeVisible();
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should submit modal form', async ({ page }) => {
|
||
|
|
await page.goto('/app/dashboard');
|
||
|
|
|
||
|
|
// Open modal
|
||
|
|
await page.getByRole('button', { name: /add item/i }).click();
|
||
|
|
|
||
|
|
// Fill modal form
|
||
|
|
await page.getByLabel(/item name/i).fill('Test Item');
|
||
|
|
|
||
|
|
// Submit
|
||
|
|
await page.getByRole('button', { name: /save/i }).click();
|
||
|
|
|
||
|
|
// Modal should close
|
||
|
|
await expect(page.getByRole('dialog')).not.toBeVisible();
|
||
|
|
|
||
|
|
// Verify item added
|
||
|
|
await expect(page.getByText('Test Item')).toBeVisible();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// MOBILE VIEWPORT
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test.describe('Mobile Viewport Example', () => {
|
||
|
|
test.use({ storageState: 'tests/.auth/user.json' });
|
||
|
|
|
||
|
|
test('should work on mobile', async ({ page }) => {
|
||
|
|
// Set mobile viewport
|
||
|
|
await page.setViewportSize({ width: 375, height: 667 });
|
||
|
|
|
||
|
|
await page.goto('/app/dashboard');
|
||
|
|
|
||
|
|
// Verify mobile menu
|
||
|
|
const mobileMenuButton = page.getByRole('button', { name: /menu|hamburger/i });
|
||
|
|
await expect(mobileMenuButton).toBeVisible();
|
||
|
|
|
||
|
|
// Open mobile menu
|
||
|
|
await mobileMenuButton.click();
|
||
|
|
|
||
|
|
// Verify menu items
|
||
|
|
await expect(page.getByRole('link', { name: /dashboard/i })).toBeVisible();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// WAITING AND TIMING
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test.describe('Waiting Example', () => {
|
||
|
|
test.use({ storageState: 'tests/.auth/user.json' });
|
||
|
|
|
||
|
|
test('should wait for elements correctly', async ({ page }) => {
|
||
|
|
await page.goto('/app/dashboard');
|
||
|
|
|
||
|
|
// Wait for specific element
|
||
|
|
await page.waitForSelector('[data-testid="dashboard-loaded"]');
|
||
|
|
|
||
|
|
// Wait for API response
|
||
|
|
const response = await page.waitForResponse((resp) =>
|
||
|
|
resp.url().includes('/api/dashboard') && resp.status() === 200
|
||
|
|
);
|
||
|
|
|
||
|
|
// Wait for navigation
|
||
|
|
await page.getByRole('link', { name: /settings/i }).click();
|
||
|
|
await page.waitForURL(/\/settings/);
|
||
|
|
|
||
|
|
// Wait for network idle (use sparingly)
|
||
|
|
await page.waitForLoadState('networkidle');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// ASSERTIONS
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test.describe('Assertion Examples', () => {
|
||
|
|
test.use({ storageState: 'tests/.auth/user.json' });
|
||
|
|
|
||
|
|
test('should demonstrate various assertions', async ({ page }) => {
|
||
|
|
await page.goto('/app/dashboard');
|
||
|
|
|
||
|
|
// Element visibility
|
||
|
|
await expect(page.getByText('Dashboard')).toBeVisible();
|
||
|
|
await expect(page.getByText('Hidden Text')).not.toBeVisible();
|
||
|
|
|
||
|
|
// Text content
|
||
|
|
await expect(page.getByRole('heading')).toContainText('Welcome');
|
||
|
|
|
||
|
|
// URL
|
||
|
|
await expect(page).toHaveURL(/\/dashboard/);
|
||
|
|
|
||
|
|
// Element count
|
||
|
|
await expect(page.getByRole('button')).toHaveCount(5);
|
||
|
|
|
||
|
|
// Attribute
|
||
|
|
await expect(page.getByRole('link', { name: 'Settings' })).toHaveAttribute('href', '/settings');
|
||
|
|
|
||
|
|
// CSS class
|
||
|
|
await expect(page.getByRole('button', { name: 'Active' })).toHaveClass(/active/);
|
||
|
|
|
||
|
|
// Value
|
||
|
|
await expect(page.getByLabel('Search')).toHaveValue('');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// TEST DATA GENERATION
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test.describe('Test Data Example', () => {
|
||
|
|
test.use({ storageState: 'tests/.auth/user.json' });
|
||
|
|
|
||
|
|
test('should use generated test data', async ({ page }) => {
|
||
|
|
await page.goto('/app/products');
|
||
|
|
|
||
|
|
// Generate unique test data
|
||
|
|
const productName = `Test Product ${generateTestId()}`;
|
||
|
|
|
||
|
|
// Use in test
|
||
|
|
await page.getByLabel(/product name/i).fill(productName);
|
||
|
|
await page.getByRole('button', { name: /save/i }).click();
|
||
|
|
|
||
|
|
// Verify
|
||
|
|
await expect(page.getByText(productName)).toBeVisible();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// KEYBOARD AND MOUSE INTERACTIONS
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
test.describe('Interaction Examples', () => {
|
||
|
|
test.use({ storageState: 'tests/.auth/user.json' });
|
||
|
|
|
||
|
|
test('should handle keyboard interactions', async ({ page }) => {
|
||
|
|
await page.goto('/app/search');
|
||
|
|
|
||
|
|
const searchInput = page.getByLabel(/search/i);
|
||
|
|
|
||
|
|
// Type text
|
||
|
|
await searchInput.type('product name');
|
||
|
|
|
||
|
|
// Press Enter
|
||
|
|
await searchInput.press('Enter');
|
||
|
|
|
||
|
|
// Use keyboard shortcuts
|
||
|
|
await page.keyboard.press('Control+K'); // Open search
|
||
|
|
await page.keyboard.press('Escape'); // Close modal
|
||
|
|
});
|
||
|
|
|
||
|
|
test('should handle mouse interactions', async ({ page }) => {
|
||
|
|
await page.goto('/app/dashboard');
|
||
|
|
|
||
|
|
const element = page.getByTestId('draggable-item');
|
||
|
|
|
||
|
|
// Hover
|
||
|
|
await element.hover();
|
||
|
|
|
||
|
|
// Double click
|
||
|
|
await element.dblclick();
|
||
|
|
|
||
|
|
// Right click
|
||
|
|
await element.click({ button: 'right' });
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
// ============================================================================
|
||
|
|
// BEST PRACTICES SUMMARY
|
||
|
|
// ============================================================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* BEST PRACTICES:
|
||
|
|
*
|
||
|
|
* 1. Use semantic selectors (getByRole, getByLabel, getByText)
|
||
|
|
* 2. Avoid hard-coded waits (waitForTimeout) - use auto-waiting
|
||
|
|
* 3. Reuse authentication state to save time
|
||
|
|
* 4. Use helpers for common operations
|
||
|
|
* 5. Generate unique test data to avoid conflicts
|
||
|
|
* 6. Mock APIs for faster, more reliable tests
|
||
|
|
* 7. Keep tests independent and isolated
|
||
|
|
* 8. Use descriptive test names
|
||
|
|
* 9. Clean up test data after tests
|
||
|
|
* 10. Use data-testid for complex elements
|
||
|
|
*/
|