Playwright in CI/CD: Performance Testing, Visual Regression, and Production Monitoring
Learn how to integrate Playwright into CI/CD pipelines for performance testing, visual regression detection, and production monitoring with advanced reporting.
#Playwright in CI/CD: Performance Testing, Visual Regression, and Production Monitoring
Integrating Playwright into your CI/CD pipeline transforms it from a testing tool into a comprehensive quality assurance platform. This guide covers advanced CI/CD integration, performance testing, visual regression detection, and production monitoring strategies.
#CI/CD Pipeline Integration
#GitHub Actions Configuration
# .github/workflows/playwright.ymlname: Playwright Testson:push:branches: [main, develop]pull_request:branches: [main]schedule:# Run tests daily at 2 AM UTC- cron: '0 2 * * *'jobs:test:timeout-minutes: 60runs-on: ubuntu-lateststrategy:fail-fast: falsematrix:shard: [1, 2, 3, 4]steps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with:node-version: 18cache: 'npm'- name: Install dependenciesrun: npm ci- name: Install Playwright Browsersrun: npx playwright install --with-deps- name: Run Playwright testsrun: npx playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }}env:PLAYWRIGHT_BASE_URL: ${{ secrets.PLAYWRIGHT_BASE_URL }}- uses: actions/upload-artifact@v4if: failure()with:name: playwright-report-${{ matrix.shard }}path: playwright-report/retention-days: 30- name: Upload test results to Tesultsif: always()run: |npm install tesultsnode upload-results.jsenv:TESULTS_TOKEN: ${{ secrets.TESULTS_TOKEN }}merge-reports:if: always()needs: [test]runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- uses: actions/setup-node@v4with:node-version: 18- name: Install dependenciesrun: npm ci- name: Download all reportsuses: actions/download-artifact@v4with:pattern: playwright-report-*path: all-reports/- name: Merge reportsrun: npx playwright merge-reports --reporter html ./all-reports- name: Upload merged reportuses: actions/upload-artifact@v4with:name: final-playwright-reportpath: playwright-report/
#Docker Integration
# Dockerfile.playwrightFROM mcr.microsoft.com/playwright:v1.40.0-focalWORKDIR /app# Copy package filesCOPY package*.json ./# Install dependenciesRUN npm ci --only=production# Copy test filesCOPY tests/ tests/COPY playwright.config.js .# Set environmentENV CI=trueENV PLAYWRIGHT_BASE_URL=http://host.docker.internal:3000# Run testsCMD ["npx", "playwright", "test"]
#Performance Testing with Playwright
#Core Web Vitals Monitoring
// tests/performance/core-web-vitals.spec.jsimport { test, expect } from '@playwright/test'test.describe('Core Web Vitals', () => {test('measures LCP, FID, and CLS', async ({ page }) => {await page.goto('/')// Inject Web Vitals libraryawait page.addScriptTag({url: 'https://unpkg.com/web-vitals@3/dist/web-vitals.iife.js',})const metrics = await page.evaluate(() => {return new Promise(resolve => {const vitals = {}webVitals.onLCP(metric => {vitals.lcp = metric.value})webVitals.onFID(metric => {vitals.fid = metric.value})webVitals.onCLS(metric => {vitals.cls = metric.valueresolve(vitals)})// Trigger layout shift measurementsetTimeout(() => {if (!vitals.cls) {webVitals.onCLS(metric => {vitals.cls = metric.valueresolve(vitals)},{ reportAllChanges: true })}}, 3000)})})// Assert Core Web Vitals thresholdsexpect(metrics.lcp).toBeLessThan(2500) // Good LCP < 2.5sexpect(metrics.fid).toBeLessThan(100) // Good FID < 100msexpect(metrics.cls).toBeLessThan(0.1) // Good CLS < 0.1console.log('Core Web Vitals:', metrics)})})
#Network Performance Testing
// tests/performance/network.spec.jsimport { test, expect } from '@playwright/test'test.describe('Network Performance', () => {test('measures resource loading performance', async ({ page }) => {const performanceData = []page.on('response', response => {performanceData.push({url: response.url(),status: response.status(),timing: response.timing(),size: response.headers()['content-length'],})})await page.goto('/')// Wait for all network activity to completeawait page.waitForLoadState('networkidle')// Analyze performanceconst slowRequests = performanceData.filter(req => req.timing && req.timing.responseEnd - req.timing.requestStart > 1000)const totalSize = performanceData.filter(req => req.size).reduce((sum, req) => sum + parseInt(req.size), 0)// Performance assertionsexpect(slowRequests.length).toBeLessThan(3) // Max 2 slow requestsexpect(totalSize).toBeLessThan(2 * 1024 * 1024) // Max 2MB totalconsole.log(`Total requests: ${performanceData.length}`)console.log(`Slow requests: ${slowRequests.length}`)console.log(`Total size: ${(totalSize / 1024).toFixed(2)} KB`)})test('tests performance under slow network conditions', async ({ page }) => {// Simulate slow 3Gawait page.context().route('**/*', (route, request) => {// Add artificial delaysetTimeout(() => route.continue(), 100)})const startTime = Date.now()await page.goto('/')await page.waitForSelector('[data-testid="main-content"]')const loadTime = Date.now() - startTime// Should still load in reasonable time even on slow networkexpect(loadTime).toBeLessThan(8000) // 8 seconds max on slow 3G})})
#Memory and CPU Monitoring
// tests/performance/memory.spec.jsimport { test, expect } from '@playwright/test'test.describe('Memory Performance', () => {test('monitors memory usage during navigation', async ({ page }) => {const memoryUsage = []// Function to collect memory metricsconst collectMemory = async () => {const metrics = await page.evaluate(() => {const memory = performance.memoryreturn {usedJSHeapSize: memory.usedJSHeapSize,totalJSHeapSize: memory.totalJSHeapSize,jsHeapSizeLimit: memory.jsHeapSizeLimit,timestamp: Date.now(),}})memoryUsage.push(metrics)}// Initial memory measurementawait page.goto('/')await collectMemory()// Navigate through multiple pagesconst routes = ['/products', '/about', '/contact', '/blog']for (const route of routes) {await page.goto(route)await page.waitForLoadState('networkidle')await collectMemory()}// Check for memory leaksconst initialMemory = memoryUsage[0].usedJSHeapSizeconst finalMemory = memoryUsage[memoryUsage.length - 1].usedJSHeapSizeconst memoryIncrease = finalMemory - initialMemory// Memory should not increase by more than 50MB during navigationexpect(memoryIncrease).toBeLessThan(50 * 1024 * 1024)console.log('Memory Usage:', memoryUsage)})})
#Visual Regression Testing
#Screenshot Comparison
// tests/visual/visual-regression.spec.jsimport { test, expect } from '@playwright/test'test.describe('Visual Regression', () => {test.beforeEach(async ({ page }) => {// Consistent font loadingawait page.addStyleTag({content: `* {font-family: -apple-system, BlinkMacSystemFont, sans-serif !important;-webkit-font-smoothing: antialiased !important;}`,})})test('homepage visual test', async ({ page }) => {await page.goto('/')// Wait for images and animations to loadawait page.waitForFunction(() => {const images = document.querySelectorAll('img')return Array.from(images).every(img => img.complete)})// Hide dynamic contentawait page.addStyleTag({content: `[data-testid="timestamp"],[data-testid="live-chat"],.animate-pulse {visibility: hidden !important;}`,})await expect(page).toHaveScreenshot('homepage.png', {fullPage: true,threshold: 0.3, // 30% difference threshold})})test('component visual tests', async ({ page }) => {await page.goto('/components')// Test individual componentsconst components = ['button', 'card', 'modal', 'form', 'navigation']for (const component of components) {const element = page.locator(`[data-testid="${component}-demo"]`)await expect(element).toHaveScreenshot(`${component}.png`, {threshold: 0.2,})}})test('responsive visual tests', async ({ page }) => {const viewports = [{ width: 320, height: 568, name: 'mobile' },{ width: 768, height: 1024, name: 'tablet' },{ width: 1440, height: 900, name: 'desktop' },]for (const viewport of viewports) {await page.setViewportSize(viewport)await page.goto('/')await expect(page).toHaveScreenshot(`homepage-${viewport.name}.png`, {fullPage: true,})}})})
#Cross-browser Visual Testing
// playwright.config.js - Visual testing configurationexport default defineConfig({projects: [{name: 'chromium-visual',use: {...devices['Desktop Chrome'],// Consistent rendering settingscolorScheme: 'light',locale: 'en-US',timezoneId: 'America/New_York',},testDir: './tests/visual',},{name: 'firefox-visual',use: {...devices['Desktop Firefox'],colorScheme: 'light',locale: 'en-US',timezoneId: 'America/New_York',},testDir: './tests/visual',},],expect: {// Global screenshot settingsthreshold: 0.2,mode: 'default',},})
#Production Monitoring
#Health Check Automation
// tests/monitoring/health-checks.spec.jsimport { test, expect } from '@playwright/test'test.describe('Production Health Checks', () => {test('API endpoints are responsive', async ({ request }) => {const endpoints = ['/api/health', '/api/users', '/api/products', '/api/orders']for (const endpoint of endpoints) {const response = await request.get(endpoint)expect(response.status()).toBe(200)const responseTime = response.timing().responseEnd - response.timing().requestStartexpect(responseTime).toBeLessThan(2000) // 2 second max response time}})test('critical user journeys work', async ({ page }) => {// Test critical path: Homepage -> Product -> Checkoutawait page.goto('/')await expect(page.locator('h1')).toBeVisible()// Navigate to productawait page.click('[data-testid="featured-product"]')await expect(page.locator('[data-testid="product-details"]')).toBeVisible()// Add to cartawait page.click('[data-testid="add-to-cart"]')await expect(page.locator('[data-testid="cart-count"]')).toContainText('1')// Start checkoutawait page.click('[data-testid="checkout"]')await expect(page.locator('[data-testid="checkout-form"]')).toBeVisible()})test('error pages are properly configured', async ({ page }) => {// Test 404 pageconst response = await page.goto('/non-existent-page')expect(response.status()).toBe(404)await expect(page.locator('h1')).toContainText('Page Not Found')// Test 500 error handlingawait page.route('**/api/error-test', route => {route.fulfill({ status: 500, body: 'Server Error' })})await page.goto('/error-test-page')await expect(page.locator('[data-testid="error-message"]')).toBeVisible()})})
#Performance Monitoring
// tests/monitoring/performance-monitoring.spec.jsimport { test, expect } from '@playwright/test'test.describe('Performance Monitoring', () => {test('tracks performance metrics over time', async ({ page }) => {const metrics = []// Monitor multiple pagesconst pages = ['/', '/products', '/about']for (const pagePath of pages) {const startTime = Date.now()await page.goto(pagePath)await page.waitForLoadState('networkidle')const performanceMetrics = await page.evaluate(() => {const navigation = performance.getEntriesByType('navigation')[0]const paint = performance.getEntriesByType('paint')return {domContentLoaded:navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,loadComplete: navigation.loadEventEnd - navigation.loadEventStart,firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0,firstContentfulPaint:paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0,}})metrics.push({page: pagePath,timestamp: new Date().toISOString(),...performanceMetrics,totalTime: Date.now() - startTime,})}// Log metrics for external monitoring systemsconsole.log('Performance Metrics:', JSON.stringify(metrics, null, 2))// Assert performance thresholdsmetrics.forEach(metric => {expect(metric.firstContentfulPaint).toBeLessThan(1500) // FCP < 1.5sexpect(metric.domContentLoaded).toBeLessThan(2000) // DCL < 2s})})})
#Advanced Reporting and Analytics
#Custom Reporter
// reporters/custom-reporter.jsclass CustomReporter {onBegin(config, suite) {console.log(`Starting test run with ${suite.allTests().length} tests`)this.startTime = Date.now()}onTestEnd(test, result) {const duration = result.durationconst status = result.status// Send metrics to external systemthis.sendMetrics({testName: test.title,duration,status,project: test.parent.project()?.name,retry: result.retry,})}onEnd(result) {const duration = Date.now() - this.startTimeconsole.log(`Test run completed in ${duration}ms`)console.log(`Passed: ${result.passed}, Failed: ${result.failed}`)// Send summary to monitoring systemthis.sendSummary({totalDuration: duration,passed: result.passed,failed: result.failed,timestamp: new Date().toISOString(),})}sendMetrics(data) {// Integration with monitoring systems like DataDog, New Relic, etc.if (process.env.DATADOG_API_KEY) {// Send to DataDog}}sendSummary(data) {// Send summary to Slack, Teams, etc.if (process.env.SLACK_WEBHOOK) {// Send Slack notification}}}module.exports = CustomReporter
#Integration with Monitoring Services
// utils/monitoring.jsexport class MonitoringIntegration {static async sendToDataDog(metrics) {if (!process.env.DATADOG_API_KEY) returnconst response = await fetch('https://api.datadoghq.com/api/v1/series', {method: 'POST',headers: {'Content-Type': 'application/json','DD-API-KEY': process.env.DATADOG_API_KEY,},body: JSON.stringify({series: [{metric: 'playwright.test.duration',points: [[Math.floor(Date.now() / 1000), metrics.duration]],tags: [`test:${metrics.testName}`, `status:${metrics.status}`],},],}),})return response.json()}static async sendSlackNotification(summary) {if (!process.env.SLACK_WEBHOOK) returnconst color = summary.failed > 0 ? 'danger' : 'good'const message = {attachments: [{color,title: 'Playwright Test Results',fields: [{ title: 'Passed', value: summary.passed, short: true },{ title: 'Failed', value: summary.failed, short: true },{ title: 'Duration', value: `${summary.totalDuration}ms`, short: true },],},],}return fetch(process.env.SLACK_WEBHOOK, {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify(message),})}}
#Conclusion
Integrating Playwright into CI/CD pipelines creates a comprehensive quality assurance system that goes beyond simple functional testing. Key benefits include:
#Performance Monitoring:
- Core Web Vitals tracking ensures optimal user experience
- Network and memory monitoring catches performance regressions
- Real-time production monitoring provides early warning systems
#Visual Quality:
- Visual regression testing catches UI changes automatically
- Cross-browser visual consistency ensures brand integrity
- Component-level visual testing enables design system validation
#Production Readiness:
- Health check automation verifies system availability
- Critical user journey monitoring ensures core functionality
- Error handling verification provides confidence in failure scenarios
#DevOps Integration:
- Automated reporting provides visibility to stakeholders
- Integration with monitoring services enables data-driven decisions
- Slack/Teams notifications keep teams informed of quality status
This comprehensive approach transforms Playwright from a testing tool into a quality assurance platform that continuously monitors and validates your application's performance, functionality, and visual integrity across the entire development lifecycle.