forked from cardosofelipe/fast-next-template
- Consolidated and modularized `eslint.config.mjs` with defined rules for source, test, E2E, and scripts. - Improved test and E2E rules with relaxed settings for flexibility and enhanced mocking. - Standardized variable naming and removed redundant imports in unit and E2E tests. - Updated error handling and comments to align with modern TypeScript best practices (e.g., `@ts-expect-error`).
277 lines
6.9 KiB
TypeScript
277 lines
6.9 KiB
TypeScript
/**
|
|
* 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
|
|
* ```
|
|
*/
|