- Marked 12 tests as skip pending feature implementation - Features tracked in GitHub issue #686 (system log viewer feature completion) - Tests cover sorting by timestamp/level/method/URI/status, pagination controls, filtering by text/level, download functionality - Unblocks Phase 2 at 91.7% pass rate to proceed to Phase 3 security enforcement validation - TODO comments in code reference GitHub #686 for feature completion tracking - Tests skipped: Pagination (3), Search/Filter (2), Download (2), Sorting (1), Log Display (4)
290 lines
9.7 KiB
TypeScript
290 lines
9.7 KiB
TypeScript
import { Page, ConsoleMessage, Request } from '@playwright/test';
|
|
|
|
/**
|
|
* Diagnostic Helpers for E2E Test Debugging
|
|
*
|
|
* These helpers enable comprehensive browser console logging and state capture
|
|
* to diagnose test interruptions and failures. Use during Phase 1 investigation
|
|
* to identify root causes of browser context closures.
|
|
*
|
|
* @see docs/reports/phase1_diagnostics.md
|
|
*/
|
|
|
|
/**
|
|
* Enable comprehensive browser console logging for diagnostic purposes
|
|
* Captures console logs, page errors, request failures, and unhandled rejections
|
|
*
|
|
* @param page - Playwright Page instance
|
|
* @param options - Optional configuration for logging behavior
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* test.beforeEach(async ({ page }) => {
|
|
* enableDiagnosticLogging(page);
|
|
* // ... test setup
|
|
* });
|
|
* ```
|
|
*/
|
|
export function enableDiagnosticLogging(
|
|
page: Page,
|
|
options: {
|
|
captureConsole?: boolean;
|
|
captureErrors?: boolean;
|
|
captureRequests?: boolean;
|
|
captureDialogs?: boolean;
|
|
} = {}
|
|
): void {
|
|
const {
|
|
captureConsole = true,
|
|
captureErrors = true,
|
|
captureRequests = true,
|
|
captureDialogs = true,
|
|
} = options;
|
|
|
|
// Console messages (all levels)
|
|
if (captureConsole) {
|
|
page.on('console', (msg: ConsoleMessage) => {
|
|
const type = msg.type().toUpperCase();
|
|
const text = msg.text();
|
|
const location = msg.location();
|
|
|
|
// Special formatting for errors and warnings
|
|
if (type === 'ERROR' || type === 'WARNING') {
|
|
console.error(`[BROWSER ${type}] ${text}`);
|
|
} else {
|
|
console.log(`[BROWSER ${type}] ${text}`);
|
|
}
|
|
|
|
if (location.url) {
|
|
console.log(
|
|
` Location: ${location.url}:${location.lineNumber}:${location.columnNumber}`
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Page errors (JavaScript exceptions)
|
|
if (captureErrors) {
|
|
page.on('pageerror', (error: Error) => {
|
|
console.error('═══════════════════════════════════════════');
|
|
console.error('PAGE ERROR DETECTED');
|
|
console.error('═══════════════════════════════════════════');
|
|
console.error('Message:', error.message);
|
|
console.error('Stack:', error.stack);
|
|
console.error('Timestamp:', new Date().toISOString());
|
|
console.error('═══════════════════════════════════════════');
|
|
});
|
|
}
|
|
|
|
// Request failures (network errors)
|
|
if (captureRequests) {
|
|
page.on('requestfailed', (request: Request) => {
|
|
const failure = request.failure();
|
|
console.error('─────────────────────────────────────────');
|
|
console.error('REQUEST FAILED');
|
|
console.error('─────────────────────────────────────────');
|
|
console.error('URL:', request.url());
|
|
console.error('Method:', request.method());
|
|
console.error('Error:', failure?.errorText || 'Unknown');
|
|
console.error('Timestamp:', new Date().toISOString());
|
|
console.error('─────────────────────────────────────────');
|
|
});
|
|
}
|
|
|
|
// Unhandled promise rejections
|
|
if (captureErrors) {
|
|
page.on('console', (msg: ConsoleMessage) => {
|
|
if (msg.type() === 'error' && msg.text().includes('Unhandled')) {
|
|
console.error('╔═══════════════════════════════════════════╗');
|
|
console.error('║ UNHANDLED PROMISE REJECTION DETECTED ║');
|
|
console.error('╚═══════════════════════════════════════════╝');
|
|
console.error(msg.text());
|
|
console.error('Timestamp:', new Date().toISOString());
|
|
}
|
|
});
|
|
}
|
|
|
|
// Dialog events (if supported)
|
|
if (captureDialogs) {
|
|
page.on('dialog', async (dialog) => {
|
|
console.log(`[DIALOG] Type: ${dialog.type()}, Message: ${dialog.message()}`);
|
|
console.log(`[DIALOG] Timestamp: ${new Date().toISOString()}`);
|
|
// Auto-dismiss to prevent blocking
|
|
await dialog.dismiss();
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Capture page state snapshot for debugging
|
|
* Logs current URL, title, and HTML content length
|
|
*
|
|
* @param page - Playwright Page instance
|
|
* @param label - Descriptive label for this snapshot
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* await capturePageState(page, 'Before dialog open');
|
|
* // ... perform action
|
|
* await capturePageState(page, 'After dialog close');
|
|
* ```
|
|
*/
|
|
export async function capturePageState(page: Page, label: string): Promise<void> {
|
|
const url = page.url();
|
|
const title = await page.title();
|
|
const html = await page.content();
|
|
|
|
console.log(`\n========== PAGE STATE: ${label} ==========`);
|
|
console.log(`URL: ${url}`);
|
|
console.log(`Title: ${title}`);
|
|
console.log(`HTML Length: ${html.length} characters`);
|
|
console.log(`Timestamp: ${new Date().toISOString()}`);
|
|
console.log(`===========================================\n`);
|
|
}
|
|
|
|
/**
|
|
* Track dialog lifecycle events for resource leak detection
|
|
* Logs when dialogs open and close to identify cleanup issues
|
|
*
|
|
* @param page - Playwright Page instance
|
|
* @param dialogSelector - Selector for the dialog element
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* test('dialog test', async ({ page }) => {
|
|
* const tracker = trackDialogLifecycle(page, '[role="dialog"]');
|
|
*
|
|
* await openDialog(page);
|
|
* await closeDialog(page);
|
|
*
|
|
* tracker.stop();
|
|
* });
|
|
* ```
|
|
*/
|
|
export function trackDialogLifecycle(
|
|
page: Page,
|
|
dialogSelector: string = '[role="dialog"]'
|
|
): { stop: () => void } {
|
|
let dialogCount = 0;
|
|
let isRunning = true;
|
|
|
|
const checkDialog = async () => {
|
|
if (!isRunning) return;
|
|
|
|
const dialogCount = await page.locator(dialogSelector).count();
|
|
|
|
if (dialogCount > 0) {
|
|
console.log(`[DIALOG LIFECYCLE] ${dialogCount} dialog(s) detected on page`);
|
|
console.log(`[DIALOG LIFECYCLE] Timestamp: ${new Date().toISOString()}`);
|
|
}
|
|
|
|
setTimeout(() => checkDialog(), 1000);
|
|
};
|
|
|
|
// Start monitoring
|
|
checkDialog();
|
|
|
|
return {
|
|
stop: () => {
|
|
isRunning = false;
|
|
console.log('[DIALOG LIFECYCLE] Tracking stopped');
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Monitor browser context health during test execution
|
|
* Detects when browser context is closed unexpectedly
|
|
*
|
|
* @param page - Playwright Page instance
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* test.beforeEach(async ({ page }) => {
|
|
* monitorBrowserContext(page);
|
|
* });
|
|
* ```
|
|
*/
|
|
export function monitorBrowserContext(page: Page): void {
|
|
const context = page.context();
|
|
const browser = context.browser();
|
|
|
|
context.on('close', () => {
|
|
console.error('╔═══════════════════════════════════════════╗');
|
|
console.error('║ BROWSER CONTEXT CLOSED UNEXPECTEDLY ║');
|
|
console.error('╚═══════════════════════════════════════════╝');
|
|
console.error('Timestamp:', new Date().toISOString());
|
|
console.error('This may indicate a resource leak or crash.');
|
|
});
|
|
|
|
if (browser) {
|
|
browser.on('disconnected', () => {
|
|
console.error('╔═══════════════════════════════════════════╗');
|
|
console.error('║ BROWSER DISCONNECTED UNEXPECTEDLY ║');
|
|
console.error('╚═══════════════════════════════════════════╝');
|
|
console.error('Timestamp:', new Date().toISOString());
|
|
});
|
|
}
|
|
|
|
page.on('close', () => {
|
|
console.warn('[PAGE CLOSED]', new Date().toISOString());
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Performance monitoring helper
|
|
* Tracks test execution time and identifies slow operations
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* test('my test', async ({ page }) => {
|
|
* const perf = startPerformanceMonitoring('My Test');
|
|
*
|
|
* perf.mark('Dialog open start');
|
|
* await openDialog(page);
|
|
* perf.mark('Dialog open end');
|
|
*
|
|
* perf.measure('Dialog open', 'Dialog open start', 'Dialog open end');
|
|
* perf.report();
|
|
* });
|
|
* ```
|
|
*/
|
|
export function startPerformanceMonitoring(testName: string) {
|
|
const startTime = performance.now();
|
|
const marks: Map<string, number> = new Map();
|
|
const measures: Array<{ name: string; duration: number }> = [];
|
|
|
|
return {
|
|
mark(name: string): void {
|
|
marks.set(name, performance.now());
|
|
console.log(`[PERF MARK] ${name} at ${marks.get(name)! - startTime}ms`);
|
|
},
|
|
|
|
measure(name: string, startMark: string, endMark: string): void {
|
|
const start = marks.get(startMark);
|
|
const end = marks.get(endMark);
|
|
|
|
if (start !== undefined && end !== undefined) {
|
|
const duration = end - start;
|
|
measures.push({ name, duration });
|
|
console.log(`[PERF MEASURE] ${name}: ${duration.toFixed(2)}ms`);
|
|
} else {
|
|
console.warn(`[PERF WARN] Missing marks for measure: ${name}`);
|
|
}
|
|
},
|
|
|
|
report(): void {
|
|
const totalTime = performance.now() - startTime;
|
|
|
|
console.log('\n========== PERFORMANCE REPORT ==========');
|
|
console.log(`Test: ${testName}`);
|
|
console.log(`Total Duration: ${totalTime.toFixed(2)}ms`);
|
|
console.log('\nMeasurements:');
|
|
measures.forEach(({ name, duration }) => {
|
|
console.log(` ${name}: ${duration.toFixed(2)}ms`);
|
|
});
|
|
console.log('=========================================\n');
|
|
},
|
|
};
|
|
}
|