๐งช Comprehensive Testing Strategy
Testing Pyramid from Discovery Requirements
E2E (10%): Critical user journeys from discovery phaseIntegration (30%): API and service integration testsUnit (60%): Business logic and utility functions
Modern Testing Stack
Playwright
E2E testing with multi-browser support
Vitest
Lightning-fast unit testing
MSW
API mocking for integration tests
E2E Testing with Playwright
- Critical Path Tests
- Mobile Testing
- Cross-Browser Testing
Setup Playwright
Copy
# Install Playwright with all browsers
pnpm add -D @playwright/test
pnpm exec playwright install
# Configure playwright.config.ts
Copy
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
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: 'pnpm dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})
User Journey Tests
Copy
// e2e/critical-paths.spec.ts
import { test, expect } from '@playwright/test'
// From user journey mapping in discovery
test.describe('User Onboarding Flow', () => {
test('new user can complete full onboarding', async ({ page }) => {
// 1. Landing page
await page.goto('/')
await expect(page).toHaveTitle(/Welcome/)
// Visual regression test
await expect(page).toHaveScreenshot('landing-page.png')
// 2. Sign up
await page.click('[data-testid="signup-button"]')
await page.fill('[name="email"]', '[email protected]')
await page.fill('[name="password"]', 'SecurePass123!')
await page.click('[type="submit"]')
// 3. Email verification (mock in test env)
await page.goto('/verify-email?token=test-token')
// 4. Profile setup
await page.fill('[name="fullName"]', 'Test User')
await page.fill('[name="company"]', 'Test Corp')
await page.click('[data-testid="continue"]')
// 5. Onboarding tour
await expect(page.locator('[data-tour="step-1"]')).toBeVisible()
await page.click('[data-testid="skip-tour"]')
// 6. Dashboard
await expect(page).toHaveURL('/dashboard')
await expect(page.locator('h1')).toContainText('Welcome, Test User')
// Performance metrics
const metrics = await page.evaluate(() => performance.getEntriesByType('navigation'))
expect(metrics[0].loadEventEnd).toBeLessThan(3000)
})
test('existing user can sign in', async ({ page }) => {
await page.goto('/signin')
await page.fill('[name="email"]', '[email protected]')
await page.fill('[name="password"]', 'password')
await page.click('[type="submit"]')
await expect(page).toHaveURL('/dashboard')
})
})
Accessibility Testing
Copy
// e2e/accessibility.spec.ts
import { test, expect } from '@playwright/test'
import AxeBuilder from '@axe-core/playwright'
test.describe('Accessibility', () => {
test('homepage should have no violations', async ({ page }) => {
await page.goto('/')
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.analyze()
expect(accessibilityScanResults.violations).toEqual([])
})
test('keyboard navigation works', async ({ page }) => {
await page.goto('/')
// Tab through interactive elements
await page.keyboard.press('Tab')
const firstFocused = await page.evaluate(() => document.activeElement?.tagName)
expect(firstFocused).toBeTruthy()
// Check skip to content link
await page.keyboard.press('Enter')
const mainContent = await page.locator('main')
await expect(mainContent).toBeFocused()
})
test('screen reader compatibility', async ({ page }) => {
await page.goto('/')
// Check ARIA labels
const buttons = await page.getByRole('button').all()
for (const button of buttons) {
const ariaLabel = await button.getAttribute('aria-label')
const text = await button.textContent()
expect(ariaLabel || text).toBeTruthy()
}
// Check heading hierarchy
const headings = await page.evaluate(() => {
return Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6'))
.map(h => ({ level: parseInt(h.tagName[1]), text: h.textContent }))
})
// Verify proper heading structure
let previousLevel = 0
for (const heading of headings) {
expect(heading.level - previousLevel).toBeLessThanOrEqual(1)
previousLevel = heading.level
}
})
})
Performance Testing
Copy
// e2e/performance.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Performance Metrics', () => {
test('meets Core Web Vitals', async ({ page }) => {
await page.goto('/')
// Measure Core Web Vitals
const metrics = await page.evaluate(() => {
return new Promise((resolve) => {
let lcp, fid, cls
// Largest Contentful Paint
new PerformanceObserver((list) => {
const entries = list.getEntries()
lcp = entries[entries.length - 1].renderTime || entries[entries.length - 1].loadTime
}).observe({ entryTypes: ['largest-contentful-paint'] })
// First Input Delay (simulated)
addEventListener('click', (e) => {
fid = performance.now() - e.timeStamp
}, { once: true })
// Cumulative Layout Shift
let clsValue = 0
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value
}
}
cls = clsValue
}).observe({ entryTypes: ['layout-shift'] })
setTimeout(() => {
resolve({ lcp, fid, cls })
}, 5000)
})
})
// Assert against targets from discovery
expect(metrics.lcp).toBeLessThan(2500) // Good LCP
expect(metrics.cls).toBeLessThan(0.1) // Good CLS
})
test('bundle size check', async ({ page }) => {
const response = await page.goto('/')
const resources = await page.evaluate(() =>
performance.getEntriesByType('resource')
.filter(r => r.name.includes('.js'))
.reduce((acc, r) => acc + r.transferSize, 0)
)
// JavaScript budget from discovery
expect(resources).toBeLessThan(200 * 1024) // 200KB
})
})
Copy
// e2e/mobile.spec.ts
import { test, expect, devices } from '@playwright/test'
// Test on devices from discovery audience analysis
const devicesToTest = [
{ name: 'iPhone 12', device: devices['iPhone 12'] },
{ name: 'iPhone SE', device: devices['iPhone SE'] },
{ name: 'Pixel 5', device: devices['Pixel 5'] },
{ name: 'Galaxy S9+', device: devices['Galaxy S9+'] },
{ name: 'iPad (gen 7)', device: devices['iPad (gen 7)'] },
{ name: 'iPad Pro 11', device: devices['iPad Pro 11'] },
]
devicesToTest.forEach(({ name, device }) => {
test.describe(`Mobile: ${name}`, () => {
test.use({ ...device })
test('responsive navigation works', async ({ page }) => {
await page.goto('/')
// Mobile menu should be visible
await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible()
// Desktop menu should be hidden
await expect(page.locator('[data-testid="desktop-menu"]')).toBeHidden()
// Test mobile interactions
await page.click('[data-testid="mobile-menu"]')
await expect(page.locator('[data-testid="mobile-nav"]')).toBeVisible()
// Test touch gestures
await page.locator('[data-testid="carousel"]').swipe({
direction: 'left',
distance: 100
})
})
test('viewport and orientation', async ({ page }) => {
await page.goto('/')
// Test portrait mode
await page.setViewportSize({ width: 375, height: 812 })
await expect(page.locator('.mobile-layout')).toBeVisible()
// Test landscape mode
await page.setViewportSize({ width: 812, height: 375 })
await expect(page.locator('.landscape-layout')).toBeVisible()
})
test('touch interactions', async ({ page }) => {
await page.goto('/interactive')
// Test tap
await page.tap('[data-testid="touch-button"]')
// Test long press
await page.locator('[data-testid="long-press"]').press('Enter', { delay: 1000 })
// Test pinch zoom
await page.evaluate(() => {
const event = new TouchEvent('gesturestart', {
scale: 2
})
document.dispatchEvent(event)
})
})
})
})
Copy
// e2e/cross-browser.spec.ts
import { test, expect, Browser, chromium, firefox, webkit } from '@playwright/test'
const browsers = [
{ name: 'Chrome', launch: chromium },
{ name: 'Firefox', launch: firefox },
{ name: 'Safari', launch: webkit },
]
browsers.forEach(({ name, launch }) => {
test.describe(`Cross-browser: ${name}`, () => {
let browser: Browser
test.beforeAll(async () => {
browser = await launch()
})
test.afterAll(async () => {
await browser.close()
})
test('CSS features support', async () => {
const page = await browser.newPage()
await page.goto('/')
// Test CSS Grid support
const gridSupport = await page.evaluate(() => {
return CSS.supports('display', 'grid')
})
expect(gridSupport).toBe(true)
// Test CSS custom properties
const customPropsSupport = await page.evaluate(() => {
return CSS.supports('--custom', 'value')
})
expect(customPropsSupport).toBe(true)
// Test modern CSS features
const features = [
'aspect-ratio: 16 / 9',
'container-type: inline-size',
'color: oklch(0.5 0.5 0)',
]
for (const feature of features) {
const [property, value] = feature.split(':')
const supported = await page.evaluate(([p, v]) => {
return CSS.supports(p, v.trim())
}, [property, value])
if (!supported) {
console.warn(`${name} doesn't support ${feature}`)
}
}
})
test('JavaScript API compatibility', async () => {
const page = await browser.newPage()
await page.goto('/')
// Test modern JavaScript APIs
const apis = await page.evaluate(() => {
return {
intersectionObserver: 'IntersectionObserver' in window,
resizeObserver: 'ResizeObserver' in window,
webComponents: 'customElements' in window,
webWorkers: 'Worker' in window,
serviceWorker: 'serviceWorker' in navigator,
webGL: 'WebGLRenderingContext' in window,
webRTC: 'RTCPeerConnection' in window,
}
})
// All modern browsers should support these
expect(apis.intersectionObserver).toBe(true)
expect(apis.resizeObserver).toBe(true)
expect(apis.webComponents).toBe(true)
})
})
})
Integration Testing
- API Testing
- Database Testing
- Service Testing
Copy
// __tests__/api/users.test.ts
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { createTestClient } from '@/tests/utils'
const server = setupServer(
http.post('/api/users', async ({ request }) => {
const body = await request.json()
// Validate request
if (!body.email || !body.password) {
return HttpResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
)
}
return HttpResponse.json({
id: '123',
email: body.email,
createdAt: new Date().toISOString()
}, { status: 201 })
})
)
beforeAll(() => server.listen())
afterAll(() => server.close())
describe('User API', () => {
const client = createTestClient()
describe('POST /api/users', () => {
it('creates user with valid data', async () => {
const userData = {
email: '[email protected]',
password: 'SecurePass123!',
fullName: 'New User'
}
const response = await client.post('/api/users', userData)
expect(response.status).toBe(201)
expect(response.data).toHaveProperty('id')
expect(response.data.email).toBe(userData.email)
})
it('validates email format', async () => {
const response = await client.post('/api/users', {
email: 'invalid-email',
password: 'password'
})
expect(response.status).toBe(400)
expect(response.data.error).toContain('email')
})
it('enforces password requirements from discovery', async () => {
const response = await client.post('/api/users', {
email: '[email protected]',
password: '123' // Too short
})
expect(response.status).toBe(400)
expect(response.data.error).toContain('password')
})
})
describe('Rate limiting', () => {
it('enforces rate limits', async () => {
const requests = Array(100).fill(null).map(() =>
client.get('/api/users')
)
const responses = await Promise.all(requests)
const rateLimited = responses.filter(r => r.status === 429)
expect(rateLimited.length).toBeGreaterThan(0)
})
})
})
Copy
// __tests__/db/repository.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { PrismaClient } from '@prisma/client'
import { mockDeep, mockReset, DeepMockProxy } from 'jest-mock-extended'
import { UserRepository } from '@/repositories/user'
// Mock Prisma for unit tests
export const prismaMock = mockDeep<PrismaClient>() as DeepMockProxy<PrismaClient>
describe('UserRepository', () => {
let repo: UserRepository
beforeEach(() => {
mockReset(prismaMock)
repo = new UserRepository(prismaMock)
})
describe('Unit Tests with Mocked DB', () => {
it('creates user with profile', async () => {
const userData = {
email: '[email protected]',
password: 'hashed',
profile: {
fullName: 'Test User',
role: 'user'
}
}
prismaMock.user.create.mockResolvedValue({
id: '123',
email: userData.email,
createdAt: new Date(),
updatedAt: new Date(),
profile: {
id: '456',
userId: '123',
fullName: userData.profile.fullName,
role: userData.profile.role
}
})
const user = await repo.createWithProfile(userData)
expect(user).toBeDefined()
expect(user.profile).toBeDefined()
expect(user.profile.fullName).toBe('Test User')
})
it('handles unique constraint violations', async () => {
prismaMock.user.create.mockRejectedValue(
new Error('Unique constraint failed on the fields: (`email`)')
)
await expect(
repo.create({ email: '[email protected]' })
).rejects.toThrow('unique constraint')
})
})
describe('Integration Tests with Real DB', () => {
// Use test database for integration tests
const testPrisma = new PrismaClient({
datasources: {
db: {
url: process.env.TEST_DATABASE_URL
}
}
})
beforeEach(async () => {
// Clean database before each test
await testPrisma.$executeRaw`TRUNCATE TABLE users CASCADE`
})
afterEach(async () => {
await testPrisma.$disconnect()
})
it('performs transactions correctly', async () => {
const repo = new UserRepository(testPrisma)
const result = await repo.createWithProfileTransaction({
email: '[email protected]',
profile: {
fullName: 'Transaction Test'
}
})
// Verify both user and profile were created
const user = await testPrisma.user.findUnique({
where: { email: '[email protected]' },
include: { profile: true }
})
expect(user).toBeDefined()
expect(user?.profile).toBeDefined()
expect(user?.profile?.fullName).toBe('Transaction Test')
})
it('rolls back on transaction failure', async () => {
const repo = new UserRepository(testPrisma)
// This should fail and rollback
await expect(
repo.createWithProfileTransaction({
email: null, // Invalid
profile: { fullName: 'Test' }
})
).rejects.toThrow()
// Verify nothing was created
const count = await testPrisma.user.count()
expect(count).toBe(0)
})
})
})
Copy
// __tests__/services/auth.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { AuthService } from '@/services/auth'
import { EmailService } from '@/services/email'
import { TokenService } from '@/services/token'
import { UserRepository } from '@/repositories/user'
// Mock dependencies
vi.mock('@/services/email')
vi.mock('@/services/token')
vi.mock('@/repositories/user')
describe('AuthService', () => {
let authService: AuthService
let emailService: EmailService
let tokenService: TokenService
let userRepo: UserRepository
beforeEach(() => {
emailService = new EmailService()
tokenService = new TokenService()
userRepo = new UserRepository()
authService = new AuthService(userRepo, emailService, tokenService)
vi.clearAllMocks()
})
describe('signup', () => {
it('creates user and sends verification email', async () => {
const signupData = {
email: '[email protected]',
password: 'SecurePass123!',
name: 'New User'
}
vi.mocked(userRepo.findByEmail).mockResolvedValue(null)
vi.mocked(userRepo.create).mockResolvedValue({
id: '123',
email: signupData.email
})
vi.mocked(tokenService.generateVerificationToken).mockReturnValue('token123')
vi.mocked(emailService.sendVerificationEmail).mockResolvedValue(true)
const result = await authService.signup(signupData)
expect(result.user).toBeDefined()
expect(result.user.email).toBe(signupData.email)
expect(emailService.sendVerificationEmail).toHaveBeenCalledWith(
signupData.email,
'token123'
)
})
it('prevents duplicate signups', async () => {
vi.mocked(userRepo.findByEmail).mockResolvedValue({
id: '123',
email: '[email protected]'
})
await expect(
authService.signup({
email: '[email protected]',
password: 'password'
})
).rejects.toThrow('already exists')
})
it('validates password strength', async () => {
await expect(
authService.signup({
email: '[email protected]',
password: 'weak'
})
).rejects.toThrow('password requirements')
})
})
describe('login', () => {
it('authenticates valid credentials', async () => {
const user = {
id: '123',
email: '[email protected]',
password: '$2b$10$hashedpassword'
}
vi.mocked(userRepo.findByEmail).mockResolvedValue(user)
vi.mocked(tokenService.generateAccessToken).mockReturnValue('access123')
vi.mocked(tokenService.generateRefreshToken).mockReturnValue('refresh123')
const result = await authService.login(
'[email protected]',
'correctpassword'
)
expect(result.accessToken).toBe('access123')
expect(result.refreshToken).toBe('refresh123')
expect(result.user).toBeDefined()
})
it('rejects invalid credentials', async () => {
vi.mocked(userRepo.findByEmail).mockResolvedValue(null)
await expect(
authService.login('[email protected]', 'password')
).rejects.toThrow('Invalid credentials')
})
it('handles rate limiting', async () => {
// Simulate multiple failed attempts
for (let i = 0; i < 5; i++) {
await expect(
authService.login('[email protected]', 'wrong')
).rejects.toThrow()
}
// Next attempt should be rate limited
await expect(
authService.login('[email protected]', 'correct')
).rejects.toThrow('Too many attempts')
})
})
})
Unit Testing with Vitest
- Component Testing
- Hook Testing
- Utility Testing
Copy
// __tests__/components/Button.test.tsx
import { describe, it, expect, vi } from 'vitest'
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from '@/components/ui/button'
describe('Button Component', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('handles click events', () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick}>Click me</Button>)
fireEvent.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('shows loading state', () => {
render(<Button loading>Submit</Button>)
expect(screen.getByTestId('spinner')).toBeInTheDocument()
expect(screen.getByText('Submit')).toHaveAttribute('disabled')
})
it('applies variant styles', () => {
const { rerender } = render(<Button variant="primary">Button</Button>)
expect(screen.getByText('Button')).toHaveClass('bg-primary')
rerender(<Button variant="secondary">Button</Button>)
expect(screen.getByText('Button')).toHaveClass('bg-secondary')
rerender(<Button variant="ghost">Button</Button>)
expect(screen.getByText('Button')).toHaveClass('hover:bg-accent')
})
it('handles disabled state', () => {
const handleClick = vi.fn()
render(<Button disabled onClick={handleClick}>Disabled</Button>)
const button = screen.getByText('Disabled')
expect(button).toBeDisabled()
fireEvent.click(button)
expect(handleClick).not.toHaveBeenCalled()
})
})
Copy
// __tests__/hooks/useAuth.test.tsx
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { renderHook, act, waitFor } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useAuth } from '@/hooks/useAuth'
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
})
return ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
describe('useAuth Hook', () => {
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
})
it('initializes with unauthenticated state', () => {
const { result } = renderHook(() => useAuth(), {
wrapper: createWrapper()
})
expect(result.current.isAuthenticated).toBe(false)
expect(result.current.user).toBeNull()
expect(result.current.isLoading).toBe(true)
})
it('handles successful login', async () => {
const { result } = renderHook(() => useAuth(), {
wrapper: createWrapper()
})
await act(async () => {
await result.current.login('[email protected]', 'password')
})
await waitFor(() => {
expect(result.current.isAuthenticated).toBe(true)
expect(result.current.user).toMatchObject({
email: '[email protected]'
})
expect(localStorage.getItem('token')).toBeTruthy()
})
})
it('handles login failure', async () => {
const { result } = renderHook(() => useAuth(), {
wrapper: createWrapper()
})
await act(async () => {
await expect(
result.current.login('[email protected]', 'wrong')
).rejects.toThrow('Invalid credentials')
})
expect(result.current.isAuthenticated).toBe(false)
expect(result.current.user).toBeNull()
expect(localStorage.getItem('token')).toBeNull()
})
it('persists authentication across renders', async () => {
localStorage.setItem('token', 'valid-token')
const { result } = renderHook(() => useAuth(), {
wrapper: createWrapper()
})
await waitFor(() => {
expect(result.current.isAuthenticated).toBe(true)
expect(result.current.user).toBeTruthy()
})
})
it('handles logout', async () => {
const { result } = renderHook(() => useAuth(), {
wrapper: createWrapper()
})
// Login first
await act(async () => {
await result.current.login('[email protected]', 'password')
})
// Then logout
await act(async () => {
await result.current.logout()
})
expect(result.current.isAuthenticated).toBe(false)
expect(result.current.user).toBeNull()
expect(localStorage.getItem('token')).toBeNull()
})
it('refreshes token automatically', async () => {
vi.useFakeTimers()
const { result } = renderHook(() => useAuth(), {
wrapper: createWrapper()
})
await act(async () => {
await result.current.login('[email protected]', 'password')
})
// Fast forward to token refresh time
await act(async () => {
vi.advanceTimersByTime(1000 * 60 * 55) // 55 minutes
})
await waitFor(() => {
// Token should be refreshed
expect(result.current.isAuthenticated).toBe(true)
})
vi.useRealTimers()
})
})
Copy
// __tests__/utils/validation.test.ts
import { describe, it, expect } from 'vitest'
import {
validateEmail,
validatePassword,
validatePhone,
validateCreditCard,
validatePostalCode
} from '@/utils/validation'
describe('Validation Utilities', () => {
describe('validateEmail', () => {
it('accepts valid emails', () => {
const validEmails = [
'[email protected]',
'[email protected]',
'[email protected]',
'[email protected]'
]
validEmails.forEach(email => {
expect(validateEmail(email)).toBe(true)
})
})
it('rejects invalid emails', () => {
const invalidEmails = [
'notanemail',
'@example.com',
'user@',
'user @example.com',
'[email protected]',
'user@example'
]
invalidEmails.forEach(email => {
expect(validateEmail(email)).toBe(false)
})
})
})
describe('validatePassword', () => {
it('enforces requirements from security discovery', () => {
// Requirements: 8+ chars, uppercase, lowercase, number, special
expect(validatePassword('ValidPass123!')).toBe(true)
expect(validatePassword('password')).toBe(false) // no uppercase
expect(validatePassword('PASSWORD')).toBe(false) // no lowercase
expect(validatePassword('Password')).toBe(false) // no number
expect(validatePassword('Password1')).toBe(false) // no special
expect(validatePassword('Pass1!')).toBe(false) // too short
})
it('checks for common passwords', () => {
const commonPasswords = [
'password123!',
'Admin123!',
'Qwerty123!',
'Password123!'
]
commonPasswords.forEach(password => {
expect(validatePassword(password, { checkCommon: true })).toBe(false)
})
})
})
describe('validatePhone', () => {
it('validates international phone numbers', () => {
const validPhones = [
'+1-555-555-5555',
'+44 20 7123 1234',
'+33 1 42 68 53 00',
'+49 30 12345678',
'+81 3-1234-5678',
'+86 10 1234 5678'
]
validPhones.forEach(phone => {
expect(validatePhone(phone)).toBe(true)
})
})
it('validates US phone numbers', () => {
const validUSPhones = [
'(555) 555-5555',
'555-555-5555',
'555.555.5555',
'5555555555'
]
validUSPhones.forEach(phone => {
expect(validatePhone(phone, { country: 'US' })).toBe(true)
})
})
})
describe('validateCreditCard', () => {
it('validates card numbers using Luhn algorithm', () => {
const validCards = [
'4532015112830366', // Visa
'5425233430109903', // MasterCard
'374245455400126', // Amex
'6011000991300009' // Discover
]
validCards.forEach(card => {
expect(validateCreditCard(card)).toBe(true)
})
})
it('identifies card types', () => {
expect(validateCreditCard('4532015112830366')).toMatchObject({
valid: true,
type: 'visa'
})
expect(validateCreditCard('5425233430109903')).toMatchObject({
valid: true,
type: 'mastercard'
})
expect(validateCreditCard('374245455400126')).toMatchObject({
valid: true,
type: 'amex'
})
})
})
describe('validatePostalCode', () => {
it('validates postal codes by country', () => {
// US ZIP codes
expect(validatePostalCode('12345', 'US')).toBe(true)
expect(validatePostalCode('12345-6789', 'US')).toBe(true)
// UK postcodes
expect(validatePostalCode('SW1A 1AA', 'UK')).toBe(true)
expect(validatePostalCode('EC1A 1BB', 'UK')).toBe(true)
// Canadian postal codes
expect(validatePostalCode('K1A 0B1', 'CA')).toBe(true)
expect(validatePostalCode('M5W 1E6', 'CA')).toBe(true)
})
})
})
Test Coverage & Quality
Coverage Requirements
Based on discovery phase requirements:
- Unit Tests: 80% coverage minimum
- Integration Tests: Critical paths covered
- E2E Tests: User journeys from discovery
- Performance Tests: Meet Core Web Vitals
- Security Tests: OWASP Top 10 covered
Copy
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './tests/setup.ts',
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'node_modules/',
'tests/',
'*.config.ts',
'**/*.d.ts',
'**/*.test.tsx',
'**/*.spec.tsx'
],
thresholds: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
mockReset: true,
restoreMocks: true,
include: ['**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
watchExclude: ['node_modules', 'dist', 'coverage']
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
})
Next Step
With comprehensive testing in place, proceed to deployment strategies โ

