End-to-End Testing with Element Plus
Overview
End-to-End (E2E) testing validates your entire application from the user's perspective, ensuring that all components, services, and integrations work together correctly. This guide covers comprehensive E2E testing strategies for Element Plus applications.
Testing Framework Setup
Playwright Configuration
bash
# Install Playwright
npm install --save-dev @playwright/test
npx playwright install
javascript
// playwright.config.js
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/results.xml' }]
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 10000,
navigationTimeout: 30000
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120000
},
})
Cypress Configuration
bash
# Install Cypress
npm install --save-dev cypress
javascript
// cypress.config.js
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
supportFile: 'cypress/support/e2e.js',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
viewportWidth: 1280,
viewportHeight: 720,
video: true,
screenshotOnRunFailure: true,
defaultCommandTimeout: 10000,
requestTimeout: 10000,
responseTimeout: 10000,
retries: {
runMode: 2,
openMode: 0
},
env: {
apiUrl: 'http://localhost:3001/api',
coverage: false
}
},
component: {
devServer: {
framework: 'vue',
bundler: 'vite',
},
specPattern: 'src/**/*.cy.{js,jsx,ts,tsx}',
supportFile: 'cypress/support/component.js'
},
})
Page Object Model
Base Page Class
javascript
// tests/e2e/pages/BasePage.js
export class BasePage {
constructor(page) {
this.page = page
}
async goto(path = '/') {
await this.page.goto(path)
await this.page.waitForLoadState('networkidle')
}
async waitForElement(selector, options = {}) {
return await this.page.waitForSelector(selector, { timeout: 10000, ...options })
}
async clickElement(selector) {
await this.page.click(selector)
}
async fillInput(selector, value) {
await this.page.fill(selector, value)
}
async selectOption(selector, value) {
await this.page.click(selector)
await this.page.click(`.el-select-dropdown__item:has-text("${value}")`)
}
async uploadFile(selector, filePath) {
await this.page.setInputFiles(selector, filePath)
}
async waitForLoading() {
await this.page.waitForSelector('.el-loading-mask', { state: 'hidden', timeout: 30000 })
}
async expectMessage(type, text) {
const messageSelector = `.el-message--${type}`
await this.page.waitForSelector(messageSelector)
const message = this.page.locator(messageSelector)
await expect(message).toContainText(text)
}
async expectNotification(type, title) {
const notificationSelector = `.el-notification--${type}`
await this.page.waitForSelector(notificationSelector)
const notification = this.page.locator(notificationSelector)
await expect(notification).toContainText(title)
}
async takeScreenshot(name) {
await this.page.screenshot({ path: `screenshots/${name}.png`, fullPage: true })
}
}
Login Page Object
javascript
// tests/e2e/pages/LoginPage.js
import { BasePage } from './BasePage.js'
import { expect } from '@playwright/test'
export class LoginPage extends BasePage {
constructor(page) {
super(page)
this.usernameInput = '[data-testid="username"] input'
this.passwordInput = '[data-testid="password"] input'
this.loginButton = '[data-testid="login-button"]'
this.forgotPasswordLink = '[data-testid="forgot-password"]'
this.registerLink = '[data-testid="register-link"]'
this.rememberMeCheckbox = '[data-testid="remember-me"] input'
this.errorMessage = '.el-form-item__error'
}
async goto() {
await super.goto('/login')
await this.waitForElement(this.loginButton)
}
async login(username, password, rememberMe = false) {
await this.fillInput(this.usernameInput, username)
await this.fillInput(this.passwordInput, password)
if (rememberMe) {
await this.page.check(this.rememberMeCheckbox)
}
await this.clickElement(this.loginButton)
}
async expectLoginSuccess() {
await this.page.waitForURL('/dashboard')
await expect(this.page.locator('h1')).toContainText('Dashboard')
}
async expectLoginError(message) {
await this.expectMessage('error', message)
}
async expectValidationError(field, message) {
const errorElement = this.page.locator(`[data-testid="${field}"] + ${this.errorMessage}`)
await expect(errorElement).toContainText(message)
}
async clickForgotPassword() {
await this.clickElement(this.forgotPasswordLink)
await this.page.waitForURL('/forgot-password')
}
async clickRegister() {
await this.clickElement(this.registerLink)
await this.page.waitForURL('/register')
}
}
Dashboard Page Object
javascript
// tests/e2e/pages/DashboardPage.js
import { BasePage } from './BasePage.js'
import { expect } from '@playwright/test'
export class DashboardPage extends BasePage {
constructor(page) {
super(page)
this.userMenu = '[data-testid="user-menu"]'
this.logoutButton = '[data-testid="logout"]'
this.profileButton = '[data-testid="profile"]'
this.settingsButton = '[data-testid="settings"]'
this.notificationBell = '[data-testid="notifications"]'
this.sidebarToggle = '[data-testid="sidebar-toggle"]'
this.breadcrumb = '.el-breadcrumb'
this.statsCards = '[data-testid="stats-card"]'
this.chartContainer = '[data-testid="chart-container"]'
}
async goto() {
await super.goto('/dashboard')
await this.waitForElement(this.userMenu)
}
async logout() {
await this.clickElement(this.userMenu)
await this.clickElement(this.logoutButton)
await this.page.waitForURL('/login')
}
async openProfile() {
await this.clickElement(this.userMenu)
await this.clickElement(this.profileButton)
await this.page.waitForURL('/profile')
}
async openSettings() {
await this.clickElement(this.userMenu)
await this.clickElement(this.settingsButton)
await this.page.waitForURL('/settings')
}
async toggleSidebar() {
await this.clickElement(this.sidebarToggle)
}
async expectStatsLoaded() {
await this.waitForElement(this.statsCards)
const cards = this.page.locator(this.statsCards)
const count = await cards.count()
expect(count).toBeGreaterThan(0)
}
async expectChartLoaded() {
await this.waitForElement(this.chartContainer)
// Wait for chart to render
await this.page.waitForTimeout(2000)
}
async getNotificationCount() {
const badge = this.page.locator(`${this.notificationBell} .el-badge__content`)
if (await badge.isVisible()) {
return parseInt(await badge.textContent())
}
return 0
}
}
Complete User Workflows
User Authentication Flow
javascript
// tests/e2e/auth-flow.spec.js
import { test, expect } from '@playwright/test'
import { LoginPage } from './pages/LoginPage.js'
import { DashboardPage } from './pages/DashboardPage.js'
test.describe('Authentication Flow', () => {
let loginPage
let dashboardPage
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page)
dashboardPage = new DashboardPage(page)
})
test('successful login and logout', async ({ page }) => {
await loginPage.goto()
// Login with valid credentials
await loginPage.login('admin@example.com', 'password123')
await loginPage.expectLoginSuccess()
// Verify dashboard loads
await dashboardPage.expectStatsLoaded()
await dashboardPage.expectChartLoaded()
// Logout
await dashboardPage.logout()
// Should redirect to login page
await expect(page).toHaveURL('/login')
})
test('login with invalid credentials', async ({ page }) => {
await loginPage.goto()
// Try invalid credentials
await loginPage.login('invalid@example.com', 'wrongpassword')
await loginPage.expectLoginError('Invalid credentials')
// Should remain on login page
await expect(page).toHaveURL('/login')
})
test('form validation', async ({ page }) => {
await loginPage.goto()
// Try to login without credentials
await loginPage.login('', '')
// Should show validation errors
await loginPage.expectValidationError('username', 'Please input your email')
await loginPage.expectValidationError('password', 'Please input your password')
// Try invalid email format
await loginPage.login('invalid-email', 'password')
await loginPage.expectValidationError('username', 'Please input valid email')
})
test('remember me functionality', async ({ page, context }) => {
await loginPage.goto()
// Login with remember me checked
await loginPage.login('admin@example.com', 'password123', true)
await loginPage.expectLoginSuccess()
// Close browser and reopen
await page.close()
const newPage = await context.newPage()
// Should automatically redirect to dashboard
await newPage.goto('/login')
await expect(newPage).toHaveURL('/dashboard')
})
test('forgot password flow', async ({ page }) => {
await loginPage.goto()
// Click forgot password
await loginPage.clickForgotPassword()
// Should navigate to forgot password page
await expect(page).toHaveURL('/forgot-password')
// Fill email and submit
await page.fill('[data-testid="email"] input', 'admin@example.com')
await page.click('[data-testid="send-reset"]')
// Should show success message
await loginPage.expectMessage('success', 'Password reset email sent')
})
test('registration flow', async ({ page }) => {
await loginPage.goto()
// Click register link
await loginPage.clickRegister()
// Should navigate to registration page
await expect(page).toHaveURL('/register')
// Fill registration form
await page.fill('[data-testid="first-name"] input', 'John')
await page.fill('[data-testid="last-name"] input', 'Doe')
await page.fill('[data-testid="email"] input', 'john.doe@example.com')
await page.fill('[data-testid="password"] input', 'password123')
await page.fill('[data-testid="confirm-password"] input', 'password123')
await page.check('[data-testid="accept-terms"] input')
// Submit registration
await page.click('[data-testid="register-button"]')
// Should show success and redirect to login
await loginPage.expectMessage('success', 'Registration successful')
await expect(page).toHaveURL('/login')
})
})
E-commerce Checkout Flow
javascript
// tests/e2e/checkout-flow.spec.js
import { test, expect } from '@playwright/test'
test.describe('E-commerce Checkout Flow', () => {
test.beforeEach(async ({ page }) => {
// Login as customer
await page.goto('/login')
await page.fill('[data-testid="username"] input', 'customer@example.com')
await page.fill('[data-testid="password"] input', 'password123')
await page.click('[data-testid="login-button"]')
await page.waitForURL('/dashboard')
})
test('complete purchase flow', async ({ page }) => {
// Browse products
await page.goto('/products')
// Search for product
await page.fill('[data-testid="search-input"] input', 'laptop')
await page.press('[data-testid="search-input"] input', 'Enter')
// Select first product
await page.click('[data-testid="product-card"]:first-child')
// Add to cart
await page.click('[data-testid="add-to-cart"]')
await expect(page.locator('.el-message--success')).toContainText('Added to cart')
// Go to cart
await page.click('[data-testid="cart-icon"]')
await page.waitForURL('/cart')
// Verify item in cart
await expect(page.locator('[data-testid="cart-item"]')).toHaveCount(1)
// Update quantity
await page.click('[data-testid="quantity-increase"]')
await expect(page.locator('[data-testid="quantity-input"] input')).toHaveValue('2')
// Proceed to checkout
await page.click('[data-testid="checkout-button"]')
await page.waitForURL('/checkout')
// Fill shipping information
await page.fill('[data-testid="shipping-address"] textarea', '123 Main St, City, State 12345')
await page.click('[data-testid="shipping-method"]')
await page.click('.el-select-dropdown__item:has-text("Standard Shipping")')
// Continue to payment
await page.click('[data-testid="continue-to-payment"]')
// Fill payment information
await page.fill('[data-testid="card-number"] input', '4111111111111111')
await page.fill('[data-testid="card-expiry"] input', '12/25')
await page.fill('[data-testid="card-cvv"] input', '123')
await page.fill('[data-testid="card-name"] input', 'John Doe')
// Review order
await page.click('[data-testid="review-order"]')
// Verify order summary
await expect(page.locator('[data-testid="order-total"]')).toContainText('$')
// Place order
await page.click('[data-testid="place-order"]')
// Wait for order confirmation
await page.waitForURL('/order-confirmation')
await expect(page.locator('h1')).toContainText('Order Confirmed')
// Verify order number
const orderNumber = await page.locator('[data-testid="order-number"]').textContent()
expect(orderNumber).toMatch(/^ORD-\d+$/)
})
test('cart persistence across sessions', async ({ page, context }) => {
// Add item to cart
await page.goto('/products')
await page.click('[data-testid="product-card"]:first-child')
await page.click('[data-testid="add-to-cart"]')
// Close browser and reopen
await page.close()
const newPage = await context.newPage()
// Login again
await newPage.goto('/login')
await newPage.fill('[data-testid="username"] input', 'customer@example.com')
await newPage.fill('[data-testid="password"] input', 'password123')
await newPage.click('[data-testid="login-button"]')
// Check cart
await newPage.click('[data-testid="cart-icon"]')
// Item should still be in cart
await expect(newPage.locator('[data-testid="cart-item"]')).toHaveCount(1)
})
test('handles payment failures', async ({ page }) => {
// Mock payment failure
await page.route('**/api/payments', route => {
route.fulfill({
status: 400,
contentType: 'application/json',
body: JSON.stringify({ error: 'Payment declined' })
})
})
// Add item and proceed to checkout
await page.goto('/products')
await page.click('[data-testid="product-card"]:first-child')
await page.click('[data-testid="add-to-cart"]')
await page.click('[data-testid="cart-icon"]')
await page.click('[data-testid="checkout-button"]')
// Fill forms and attempt payment
await page.fill('[data-testid="shipping-address"] textarea', '123 Main St')
await page.click('[data-testid="continue-to-payment"]')
await page.fill('[data-testid="card-number"] input', '4000000000000002') // Declined card
await page.fill('[data-testid="card-expiry"] input', '12/25')
await page.fill('[data-testid="card-cvv"] input', '123')
await page.fill('[data-testid="card-name"] input', 'John Doe')
await page.click('[data-testid="place-order"]')
// Should show payment error
await expect(page.locator('.el-message--error')).toContainText('Payment declined')
// Should remain on checkout page
await expect(page).toHaveURL('/checkout')
})
})
Admin Panel Workflow
javascript
// tests/e2e/admin-workflow.spec.js
import { test, expect } from '@playwright/test'
test.describe('Admin Panel Workflow', () => {
test.beforeEach(async ({ page }) => {
// Login as admin
await page.goto('/login')
await page.fill('[data-testid="username"] input', 'admin@example.com')
await page.fill('[data-testid="password"] input', 'admin123')
await page.click('[data-testid="login-button"]')
await page.waitForURL('/admin/dashboard')
})
test('manages users', async ({ page }) => {
// Navigate to users section
await page.click('[data-testid="sidebar-users"]')
await page.waitForURL('/admin/users')
// Create new user
await page.click('[data-testid="add-user"]')
await page.fill('[data-testid="user-name"] input', 'Test User')
await page.fill('[data-testid="user-email"] input', 'testuser@example.com')
await page.fill('[data-testid="user-password"] input', 'password123')
await page.click('[data-testid="user-role"]')
await page.click('.el-select-dropdown__item:has-text("User")')
await page.click('[data-testid="save-user"]')
// Verify user created
await expect(page.locator('.el-message--success')).toContainText('User created successfully')
// Search for user
await page.fill('[data-testid="search-input"] input', 'testuser@example.com')
await page.press('[data-testid="search-input"] input', 'Enter')
await expect(page.locator('tbody tr')).toHaveCount(1)
await expect(page.locator('tbody tr td').nth(1)).toContainText('testuser@example.com')
// Edit user
await page.click('[data-testid="edit-user"]')
await page.fill('[data-testid="user-name"] input', 'Updated Test User')
await page.click('[data-testid="save-user"]')
await expect(page.locator('tbody tr td').nth(0)).toContainText('Updated Test User')
// Delete user
await page.click('[data-testid="delete-user"]')
await page.click('.el-message-box__btns .el-button--primary')
await expect(page.locator('.el-empty')).toContainText('No data')
})
test('manages content', async ({ page }) => {
// Navigate to content section
await page.click('[data-testid="sidebar-content"]')
await page.waitForURL('/admin/content')
// Create new article
await page.click('[data-testid="add-article"]')
await page.fill('[data-testid="article-title"] input', 'Test Article')
await page.fill('[data-testid="article-slug"] input', 'test-article')
// Use rich text editor
const editor = page.locator('[data-testid="article-content"] .ql-editor')
await editor.fill('This is a test article content.')
// Set category
await page.click('[data-testid="article-category"]')
await page.click('.el-select-dropdown__item:has-text("Technology")')
// Set tags
await page.click('[data-testid="article-tags"]')
await page.type('[data-testid="article-tags"] input', 'test')
await page.press('[data-testid="article-tags"] input', 'Enter')
await page.type('[data-testid="article-tags"] input', 'demo')
await page.press('[data-testid="article-tags"] input', 'Enter')
// Upload featured image
await page.setInputFiles('[data-testid="featured-image"]', 'tests/fixtures/test-image.jpg')
// Set publish date
await page.click('[data-testid="publish-date"]')
await page.click('.el-date-picker__header-label')
await page.click('.el-year-table td:has-text("2024")')
await page.click('.el-month-table td:has-text("Dec")')
await page.click('.el-date-table td:has-text("25")')
// Save as draft
await page.click('[data-testid="save-draft"]')
await expect(page.locator('.el-message--success')).toContainText('Article saved as draft')
// Publish article
await page.click('[data-testid="publish-article"]')
await expect(page.locator('.el-message--success')).toContainText('Article published successfully')
// Verify in articles list
await page.goto('/admin/content')
await expect(page.locator('tbody tr').first().locator('td').nth(0)).toContainText('Test Article')
await expect(page.locator('tbody tr').first().locator('td').nth(3)).toContainText('Published')
})
test('views analytics dashboard', async ({ page }) => {
// Navigate to analytics
await page.click('[data-testid="sidebar-analytics"]')
await page.waitForURL('/admin/analytics')
// Wait for charts to load
await page.waitForSelector('[data-testid="visitors-chart"]')
await page.waitForSelector('[data-testid="revenue-chart"]')
// Change date range
await page.click('[data-testid="date-range"]')
await page.click('.el-picker-panel__shortcut:has-text("Last 30 days")')
// Wait for charts to update
await page.waitForTimeout(2000)
// Export report
await page.click('[data-testid="export-report"]')
await page.click('.el-dropdown-menu__item:has-text("Export as PDF")')
// Wait for download
const downloadPromise = page.waitForEvent('download')
const download = await downloadPromise
expect(download.suggestedFilename()).toContain('analytics-report')
})
})
Mobile Testing
Responsive Design Testing
javascript
// tests/e2e/mobile.spec.js
import { test, expect, devices } from '@playwright/test'
test.describe('Mobile Responsive Tests', () => {
test('mobile navigation', async ({ browser }) => {
const context = await browser.newContext({
...devices['iPhone 12']
})
const page = await context.newPage()
await page.goto('/dashboard')
// Mobile menu should be collapsed by default
await expect(page.locator('[data-testid="sidebar"]')).not.toBeVisible()
// Open mobile menu
await page.click('[data-testid="mobile-menu-toggle"]')
await expect(page.locator('[data-testid="sidebar"]')).toBeVisible()
// Navigate to different section
await page.click('[data-testid="sidebar-users"]')
await page.waitForURL('/admin/users')
// Menu should close after navigation
await expect(page.locator('[data-testid="sidebar"]')).not.toBeVisible()
await context.close()
})
test('mobile form interactions', async ({ browser }) => {
const context = await browser.newContext({
...devices['iPhone 12']
})
const page = await context.newPage()
await page.goto('/contact')
// Test mobile-optimized form
await page.fill('[data-testid="name"] input', 'John Doe')
await page.fill('[data-testid="email"] input', 'john@example.com')
// Test mobile select
await page.click('[data-testid="inquiry-type"]')
await expect(page.locator('.el-select-dropdown')).toBeVisible()
await page.click('.el-select-dropdown__item:has-text("Support")')
// Test mobile date picker
await page.click('[data-testid="preferred-date"]')
await expect(page.locator('.el-date-picker')).toBeVisible()
await page.click('.el-date-table td:has-text("15")')
// Submit form
await page.click('[data-testid="submit-form"]')
await expect(page.locator('.el-message--success')).toContainText('Message sent successfully')
await context.close()
})
test('mobile table interactions', async ({ browser }) => {
const context = await browser.newContext({
...devices['iPhone 12']
})
const page = await context.newPage()
await page.goto('/admin/users')
// Table should be horizontally scrollable on mobile
const table = page.locator('.el-table')
await expect(table).toHaveCSS('overflow-x', 'auto')
// Test mobile-optimized actions
await page.click('[data-testid="mobile-actions-toggle"]')
await expect(page.locator('[data-testid="mobile-actions-menu"]')).toBeVisible()
await page.click('[data-testid="mobile-action-edit"]')
await expect(page.locator('.el-dialog')).toBeVisible()
await context.close()
})
})
Performance Testing
Load Time and Core Web Vitals
javascript
// tests/e2e/performance.spec.js
import { test, expect } from '@playwright/test'
test.describe('Performance Tests', () => {
test('measures Core Web Vitals', async ({ page }) => {
// Navigate to page
await page.goto('/dashboard')
// Measure performance metrics
const metrics = await page.evaluate(() => {
return new Promise((resolve) => {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries()
const lcp = entries.find(entry => entry.entryType === 'largest-contentful-paint')
if (lcp) {
const navigation = performance.getEntriesByType('navigation')[0]
const paint = performance.getEntriesByType('paint')
resolve({
// Core Web Vitals
lcp: lcp.startTime,
fcp: paint.find(entry => entry.name === 'first-contentful-paint')?.startTime,
// Other metrics
domContentLoaded: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
loadComplete: navigation.loadEventEnd - navigation.loadEventStart,
// Resource timing
resourceCount: performance.getEntriesByType('resource').length,
totalResourceSize: performance.getEntriesByType('resource')
.reduce((total, resource) => total + (resource.transferSize || 0), 0)
})
}
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
})
})
// Assert performance thresholds
expect(metrics.lcp).toBeLessThan(2500) // LCP should be under 2.5s
expect(metrics.fcp).toBeLessThan(1800) // FCP should be under 1.8s
expect(metrics.domContentLoaded).toBeLessThan(1000) // DOM ready should be under 1s
expect(metrics.totalResourceSize).toBeLessThan(2 * 1024 * 1024) // Total resources under 2MB
console.log('Performance Metrics:', metrics)
})
test('measures JavaScript bundle size', async ({ page }) => {
const resourceSizes = []
page.on('response', async (response) => {
if (response.url().includes('.js') && response.status() === 200) {
const headers = response.headers()
const contentLength = headers['content-length']
if (contentLength) {
resourceSizes.push({
url: response.url(),
size: parseInt(contentLength)
})
}
}
})
await page.goto('/dashboard')
await page.waitForLoadState('networkidle')
const totalJSSize = resourceSizes.reduce((total, resource) => total + resource.size, 0)
const mainBundle = resourceSizes.find(resource => resource.url.includes('main'))
// Assert bundle size thresholds
expect(totalJSSize).toBeLessThan(1024 * 1024) // Total JS under 1MB
if (mainBundle) {
expect(mainBundle.size).toBeLessThan(512 * 1024) // Main bundle under 512KB
}
console.log('JavaScript Bundle Sizes:', resourceSizes)
})
test('tests with slow network', async ({ browser }) => {
const context = await browser.newContext()
// Simulate slow 3G network
await context.route('**/*', async route => {
await new Promise(resolve => setTimeout(resolve, 100)) // Add 100ms delay
route.continue()
})
const page = await context.newPage()
const startTime = Date.now()
await page.goto('/dashboard')
await page.waitForLoadState('networkidle')
const loadTime = Date.now() - startTime
// Should still load within reasonable time even on slow network
expect(loadTime).toBeLessThan(10000) // Under 10 seconds
// Verify critical content is visible
await expect(page.locator('h1')).toContainText('Dashboard')
await context.close()
})
})
Accessibility Testing
A11y Compliance Testing
javascript
// tests/e2e/accessibility.spec.js
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
test.describe('Accessibility Tests', () => {
test('dashboard accessibility', async ({ page }) => {
await page.goto('/dashboard')
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
.analyze()
expect(accessibilityScanResults.violations).toEqual([])
})
test('keyboard navigation', async ({ page }) => {
await page.goto('/dashboard')
// Test tab navigation
await page.keyboard.press('Tab')
await expect(page.locator(':focus')).toBeVisible()
// Navigate through main menu
await page.keyboard.press('Tab')
await page.keyboard.press('Tab')
await page.keyboard.press('Enter')
// Should navigate to focused item
await page.waitForURL('/admin/users')
})
test('screen reader compatibility', async ({ page }) => {
await page.goto('/contact')
// Check for proper ARIA labels
const nameInput = page.locator('[data-testid="name"] input')
await expect(nameInput).toHaveAttribute('aria-label')
const emailInput = page.locator('[data-testid="email"] input')
await expect(emailInput).toHaveAttribute('aria-describedby')
// Check form validation announcements
await page.click('[data-testid="submit-form"]')
const errorMessage = page.locator('.el-form-item__error')
await expect(errorMessage).toHaveAttribute('role', 'alert')
})
test('color contrast', async ({ page }) => {
await page.goto('/dashboard')
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2aa'])
.include('.el-button')
.analyze()
const colorContrastViolations = accessibilityScanResults.violations
.filter(violation => violation.id === 'color-contrast')
expect(colorContrastViolations).toEqual([])
})
})
Cross-browser Testing
Browser Compatibility
javascript
// tests/e2e/cross-browser.spec.js
import { test, expect, devices } from '@playwright/test'
const browsers = ['chromium', 'firefox', 'webkit']
browsers.forEach(browserName => {
test.describe(`${browserName} compatibility`, () => {
test(`login flow works in ${browserName}`, async ({ browser }) => {
const context = await browser.newContext()
const page = await context.newPage()
await page.goto('/login')
await page.fill('[data-testid="username"] input', 'admin@example.com')
await page.fill('[data-testid="password"] input', 'password123')
await page.click('[data-testid="login-button"]')
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('h1')).toContainText('Dashboard')
await context.close()
})
test(`form interactions work in ${browserName}`, async ({ browser }) => {
const context = await browser.newContext()
const page = await context.newPage()
await page.goto('/contact')
// Test various form elements
await page.fill('[data-testid="name"] input', 'Test User')
await page.fill('[data-testid="email"] input', 'test@example.com')
await page.click('[data-testid="inquiry-type"]')
await page.click('.el-select-dropdown__item:first-child')
await page.click('[data-testid="date"]')
await page.click('.el-date-table td:has-text("15")')
await page.fill('[data-testid="message"] textarea', 'Test message')
await page.click('[data-testid="submit-form"]')
await expect(page.locator('.el-message--success')).toContainText('Message sent')
await context.close()
})
})
})
Test Data Management
Database Seeding and Cleanup
javascript
// tests/e2e/helpers/database.js
export class DatabaseHelper {
static async seedTestData() {
// Seed test users
await fetch('http://localhost:3001/api/test/seed-users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
users: [
{ email: 'admin@example.com', role: 'admin', password: 'password123' },
{ email: 'user@example.com', role: 'user', password: 'password123' },
{ email: 'customer@example.com', role: 'customer', password: 'password123' }
]
})
})
// Seed test products
await fetch('http://localhost:3001/api/test/seed-products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
products: [
{ name: 'Laptop', price: 999.99, category: 'Electronics' },
{ name: 'Phone', price: 599.99, category: 'Electronics' },
{ name: 'Book', price: 19.99, category: 'Books' }
]
})
})
}
static async cleanupTestData() {
await fetch('http://localhost:3001/api/test/cleanup', {
method: 'DELETE'
})
}
static async createTestUser(userData) {
const response = await fetch('http://localhost:3001/api/test/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(userData)
})
return response.json()
}
}
Global Test Setup
javascript
// tests/e2e/global-setup.js
import { DatabaseHelper } from './helpers/database.js'
export default async function globalSetup() {
console.log('Setting up test environment...')
// Wait for server to be ready
await waitForServer('http://localhost:3001/health')
// Seed test data
await DatabaseHelper.seedTestData()
console.log('Test environment ready')
}
async function waitForServer(url, timeout = 30000) {
const start = Date.now()
while (Date.now() - start < timeout) {
try {
const response = await fetch(url)
if (response.ok) {
return
}
} catch (error) {
// Server not ready yet
}
await new Promise(resolve => setTimeout(resolve, 1000))
}
throw new Error(`Server not ready after ${timeout}ms`)
}
javascript
// tests/e2e/global-teardown.js
import { DatabaseHelper } from './helpers/database.js'
export default async function globalTeardown() {
console.log('Cleaning up test environment...')
// Cleanup test data
await DatabaseHelper.cleanupTestData()
console.log('Test environment cleaned up')
}
CI/CD Integration
GitHub Actions Configuration
yaml
# .github/workflows/e2e-tests.yml
name: E2E Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
e2e-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Install Playwright
run: npx playwright install --with-deps
- name: Start application
run: |
npm run start:test &
npx wait-on http://localhost:3000
env:
NODE_ENV: test
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
- name: Run E2E tests
run: npx playwright test
env:
CI: true
- name: Upload test results
uses: actions/upload-artifact@v3
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Best Practices
1. Test Strategy
- Focus on critical user journeys
- Test happy paths and error scenarios
- Maintain test independence
- Use realistic test data
2. Reliability
- Use explicit waits instead of fixed delays
- Handle flaky tests with proper retry logic
- Clean up test data between runs
- Use stable selectors
3. Maintainability
- Implement Page Object Model
- Create reusable helper functions
- Keep tests simple and focused
- Document complex test scenarios
4. Performance
- Run tests in parallel when possible
- Use test data fixtures
- Optimize test execution time
- Monitor test suite performance
5. Reporting
- Generate comprehensive test reports
- Include screenshots and videos for failures
- Track test metrics over time
- Integrate with CI/CD pipelines
E2E testing ensures your Element Plus application works correctly from the user's perspective, providing confidence in your application's quality and reliability.