/** * E2E Coverage Helpers * * Utilities for collecting code coverage during Playwright E2E tests. * Supports both V8 coverage (Chromium-only) and Istanbul instrumentation. * * Usage in E2E tests: * * ```typescript * import { startCoverage, stopAndSaveCoverage } from './helpers/coverage'; * * test.describe('My Tests', () => { * test.beforeEach(async ({ page }) => { * await startCoverage(page); * await page.goto('/'); * }); * * test.afterEach(async ({ page }, testInfo) => { * await stopAndSaveCoverage(page, testInfo.title); * }); * * test('my test', async ({ page }) => { * // Your test code... * }); * }); * ``` */ import type { Page } from '@playwright/test'; import fs from 'fs/promises'; import path from 'path'; /** * Check if coverage collection is enabled via environment variable */ export function isCoverageEnabled(): boolean { return process.env.E2E_COVERAGE === 'true'; } /** * Start collecting V8 coverage for a page * * @param page - Playwright page instance * @param options - Coverage options */ export async function startCoverage( page: Page, options?: { resetOnNavigation?: boolean; includeRawScriptCoverage?: boolean; } ) { if (!isCoverageEnabled()) { return; } try { await page.coverage.startJSCoverage({ resetOnNavigation: options?.resetOnNavigation ?? false, // @ts-expect-error - includeRawScriptCoverage is not in official types but supported by Playwright includeRawScriptCoverage: options?.includeRawScriptCoverage ?? false, }); } catch (error) { console.warn('⚠️ Failed to start coverage:', error); } } /** * Stop coverage collection and save to file * * @param page - Playwright page instance * @param testName - Name of the test (used for filename) */ export async function stopAndSaveCoverage(page: Page, testName: string) { if (!isCoverageEnabled()) { return; } try { const coverage = await page.coverage.stopJSCoverage(); if (coverage.length === 0) { console.warn('⚠️ No coverage collected for:', testName); return; } // Save V8 coverage await saveV8Coverage(coverage, testName); } catch (error) { console.warn('⚠️ Failed to stop/save coverage for', testName, ':', error); } } /** * Save V8 coverage data to disk * * @param coverage - V8 coverage data * @param testName - Test name for the filename */ async function saveV8Coverage(coverage: any[], testName: string) { const coverageDir = path.join(process.cwd(), 'coverage-e2e', 'raw'); await fs.mkdir(coverageDir, { recursive: true }); const filename = sanitizeFilename(testName); const filepath = path.join(coverageDir, `${filename}.json`); await fs.writeFile(filepath, JSON.stringify(coverage, null, 2)); } /** * Collect Istanbul coverage from browser window object * * Use this if you're using Istanbul instrumentation instead of V8 coverage. * Requires babel-plugin-istanbul or similar instrumentation. * * @param page - Playwright page instance * @param testName - Name of the test */ export async function saveIstanbulCoverage(page: Page, testName: string) { if (!isCoverageEnabled()) { return; } try { // Extract coverage from window.__coverage__ (set by Istanbul instrumentation) const coverage = await page.evaluate(() => (window as any).__coverage__); if (!coverage) { console.warn('⚠️ No Istanbul coverage found for:', testName); console.warn(' Make sure babel-plugin-istanbul is configured'); return; } // Save Istanbul coverage const coverageDir = path.join(process.cwd(), 'coverage-e2e', '.nyc_output'); await fs.mkdir(coverageDir, { recursive: true }); const filename = sanitizeFilename(testName); const filepath = path.join(coverageDir, `${filename}.json`); await fs.writeFile(filepath, JSON.stringify(coverage, null, 2)); } catch (error) { console.warn('⚠️ Failed to save Istanbul coverage for', testName, ':', error); } } /** * Combined coverage helper for test hooks * * Automatically uses V8 coverage if available, falls back to Istanbul * * Usage in beforeEach/afterEach: * ```typescript * test.beforeEach(async ({ page }) => { * await withCoverage.start(page); * }); * * test.afterEach(async ({ page }, testInfo) => { * await withCoverage.stop(page, testInfo.title); * }); * ``` */ export const withCoverage = { /** * Start coverage collection (V8 approach) */ async start(page: Page) { await startCoverage(page); }, /** * Stop coverage and save (tries V8, then Istanbul) */ async stop(page: Page, testName: string) { if (!isCoverageEnabled()) { return; } // Try V8 coverage first try { const v8Coverage = await page.coverage.stopJSCoverage(); if (v8Coverage && v8Coverage.length > 0) { await saveV8Coverage(v8Coverage, testName); return; } } catch { // V8 coverage not available, try Istanbul } // Fall back to Istanbul coverage await saveIstanbulCoverage(page, testName); }, }; /** * Sanitize test name for use as filename * * @param name - Test name * @returns Sanitized filename */ function sanitizeFilename(name: string): string { return name .replace(/[^a-z0-9\s-]/gi, '') // Remove special chars .replace(/\s+/g, '_') // Replace spaces with underscores .toLowerCase() .substring(0, 100); // Limit length } /** * Get coverage statistics (for debugging) * * @param page - Playwright page instance * @returns Coverage statistics */ export async function getCoverageStats(page: Page): Promise<{ v8Available: boolean; istanbulAvailable: boolean; istanbulFileCount?: number; }> { const stats = { v8Available: false, istanbulAvailable: false, istanbulFileCount: undefined as number | undefined, }; // Check V8 coverage try { await page.coverage.startJSCoverage(); await page.coverage.stopJSCoverage(); stats.v8Available = true; } catch { stats.v8Available = false; } // Check Istanbul coverage try { const coverage = await page.evaluate(() => (window as any).__coverage__); if (coverage) { stats.istanbulAvailable = true; stats.istanbulFileCount = Object.keys(coverage).length; } } catch { stats.istanbulAvailable = false; } return stats; } /** * Example usage in a test file: * * ```typescript * import { test, expect } from '@playwright/test'; * import { withCoverage } from './helpers/coverage'; * * test.describe('Homepage Tests', () => { * test.beforeEach(async ({ page }) => { * await withCoverage.start(page); * await page.goto('/'); * }); * * test.afterEach(async ({ page }, testInfo) => { * await withCoverage.stop(page, testInfo.title); * }); * * test('displays header', async ({ page }) => { * await expect(page.getByRole('heading')).toBeVisible(); * }); * }); * ``` * * Then run with: * ```bash * E2E_COVERAGE=true npm run test:e2e * ``` */