import { Page, Locator, PageScreenshotOptions } from '@playwright/test'; import { testLogger } from '../shared/utils/test-logger'; import * as path from 'path'; import * as fs from 'fs'; export interface ScreenshotConfig { outputDir: string; filename: string; fullPage: boolean; quality?: number; type: 'png' | 'jpeg'; timeout: number; } export interface ScreenshotMetadata { filename: string; path: string; timestamp: string; testName?: string; description?: string; } export class ScreenshotHelper { private page: Page; private outputDir: string; private defaultConfig: Partial; private screenshots: Map = new Map(); constructor(page: Page, outputDir: string = 'test-results/screenshots') { this.page = page; this.outputDir = outputDir; this.defaultConfig = { outputDir, fullPage: false, type: 'png', timeout: 5000 }; this.ensureOutputDir(); testLogger.info(`ScreenshotHelper initialized with output dir: ${outputDir}`); } setDefaultConfig(config: Partial): void { this.defaultConfig = { ...this.defaultConfig, ...config }; testLogger.debug('Default screenshot config updated'); } async capture(config: Partial = {}): Promise { const finalConfig = { ...this.defaultConfig, ...config }; const filename = this.generateFilename(finalConfig.filename); const filePath = path.join(finalConfig.outputDir || this.outputDir, filename); testLogger.info(`Capturing screenshot: ${filename}`); const options: PageScreenshotOptions = { path: filePath, type: finalConfig.type, fullPage: finalConfig.fullPage, timeout: finalConfig.timeout }; if (finalConfig.quality && finalConfig.type === 'jpeg') { options.quality = finalConfig.quality; } await this.page.screenshot(options); const metadata: ScreenshotMetadata = { filename, path: filePath, timestamp: new Date().toISOString() }; this.screenshots.set(filename, metadata); testLogger.info(`Screenshot captured: ${filePath}`); return filePath; } async captureElement(locator: Locator, config: Partial = {}): Promise { const finalConfig = { ...this.defaultConfig, ...config }; const filename = this.generateFilename(finalConfig.filename); const filePath = path.join(finalConfig.outputDir || this.outputDir, filename); testLogger.info(`Capturing element screenshot: ${filename}`); const options: PageScreenshotOptions = { path: filePath, type: finalConfig.type, timeout: finalConfig.timeout }; if (finalConfig.quality && finalConfig.type === 'jpeg') { options.quality = finalConfig.quality; } await locator.screenshot(options); const metadata: ScreenshotMetadata = { filename, path: filePath, timestamp: new Date().toISOString() }; this.screenshots.set(filename, metadata); testLogger.info(`Element screenshot captured: ${filePath}`); return filePath; } async captureFullPage(config: Partial = {}): Promise { return this.capture({ ...config, fullPage: true }); } async captureViewport(config: Partial = {}): Promise { return this.capture({ ...config, fullPage: false }); } async captureOnFailure(testName: string, error?: Error): Promise { const filename = `failure-${testName}-${Date.now()}`; const filePath = await this.captureFullPage({ filename }); testLogger.error(`Screenshot captured on failure: ${testName}`, error); if (error) { const errorLogPath = path.join(this.outputDir, `failure-${testName}-${Date.now()}.log`); const errorLog = ` Test Name: ${testName} Timestamp: ${new Date().toISOString()} Error: ${error.message} Stack Trace: ${error.stack} `.trim(); fs.writeFileSync(errorLogPath, errorLog); testLogger.info(`Error log saved: ${errorLogPath}`); } return filePath; } async captureWithDescription(description: string, config: Partial = {}): Promise { const filename = `${description}-${Date.now()}`; const filePath = await this.capture({ ...config, filename }); const metadata = this.screenshots.get(filename); if (metadata) { metadata.description = description; this.screenshots.set(filename, metadata); } return filePath; } async captureBeforeAction(actionName: string, config: Partial = {}): Promise { const filename = `before-${actionName}-${Date.now()}`; return this.capture({ ...config, filename }); } async captureAfterAction(actionName: string, config: Partial = {}): Promise { const filename = `after-${actionName}-${Date.now()}`; return this.capture({ ...config, filename }); } async captureStep(stepName: string, config: Partial = {}): Promise { const filename = `step-${stepName}-${Date.now()}`; return this.capture({ ...config, filename }); } async captureMultiple(configs: Partial[]): Promise { testLogger.info(`Capturing ${configs.length} screenshots`); const filePaths: string[] = []; for (let i = 0; i < configs.length; i++) { const filePath = await this.capture(configs[i]); filePaths.push(filePath); } testLogger.info(`Captured ${filePaths.length} screenshots`); return filePaths; } async captureWithDelay(delay: number, config: Partial = {}): Promise { testLogger.info(`Waiting ${delay}ms before capturing screenshot`); await this.page.waitForTimeout(delay); return this.capture(config); } async captureOnHover(locator: Locator, config: Partial = {}): Promise { testLogger.info('Capturing screenshot on hover'); await locator.hover(); await this.page.waitForTimeout(300); return this.capture(config); } async captureOnFocus(locator: Locator, config: Partial = {}): Promise { testLogger.info('Capturing screenshot on focus'); await locator.focus(); await this.page.waitForTimeout(300); return this.capture(config); } async captureVisibleArea(config: Partial = {}): Promise { testLogger.info('Capturing visible area screenshot'); const viewportSize = this.page.viewportSize(); if (viewportSize) { const clip = { x: 0, y: 0, width: viewportSize.width, height: viewportSize.height }; const finalConfig = { ...this.defaultConfig, ...config }; const filename = this.generateFilename(finalConfig.filename); const filePath = path.join(finalConfig.outputDir || this.outputDir, filename); const options: PageScreenshotOptions = { path: filePath, type: finalConfig.type, clip, timeout: finalConfig.timeout }; if (finalConfig.quality && finalConfig.type === 'jpeg') { options.quality = finalConfig.quality; } await this.page.screenshot(options); const metadata: ScreenshotMetadata = { filename, path: filePath, timestamp: new Date().toISOString() }; this.screenshots.set(filename, metadata); testLogger.info(`Visible area screenshot captured: ${filePath}`); return filePath; } return this.captureViewport(config); } async captureElementBounds(locator: Locator, config: Partial = {}): Promise { testLogger.info('Capturing element bounds screenshot'); const box = await locator.boundingBox(); if (box) { const finalConfig = { ...this.defaultConfig, ...config }; const filename = this.generateFilename(finalConfig.filename); const filePath = path.join(finalConfig.outputDir || this.outputDir, filename); const options: PageScreenshotOptions = { path: filePath, type: finalConfig.type, clip: box, timeout: finalConfig.timeout }; if (finalConfig.quality && finalConfig.type === 'jpeg') { options.quality = finalConfig.quality; } await this.page.screenshot(options); const metadata: ScreenshotMetadata = { filename, path: filePath, timestamp: new Date().toISOString() }; this.screenshots.set(filename, metadata); testLogger.info(`Element bounds screenshot captured: ${filePath}`); return filePath; } return this.captureElement(locator, config); } getScreenshotPath(filename: string): string | undefined { const metadata = this.screenshots.get(filename); return metadata?.path; } getAllScreenshots(): ScreenshotMetadata[] { return Array.from(this.screenshots.values()); } getScreenshotCount(): number { return this.screenshots.size; } clearScreenshots(): void { this.screenshots.clear(); testLogger.info('Screenshots cleared'); } deleteScreenshot(filename: string): boolean { const metadata = this.screenshots.get(filename); if (metadata && fs.existsSync(metadata.path)) { fs.unlinkSync(metadata.path); this.screenshots.delete(filename); testLogger.info(`Screenshot deleted: ${filename}`); return true; } return false; } deleteAllScreenshots(): void { const screenshotValues = Array.from(this.screenshots.values()); for (const metadata of screenshotValues) { if (fs.existsSync(metadata.path)) { fs.unlinkSync(metadata.path); } } this.screenshots.clear(); testLogger.info('All screenshots deleted'); } async compareScreenshots(beforePath: string, afterPath: string): Promise { testLogger.info(`Comparing screenshots: ${beforePath} vs ${afterPath}`); const beforeExists = fs.existsSync(beforePath); const afterExists = fs.existsSync(afterPath); if (!beforeExists || !afterExists) { testLogger.warn('Screenshot comparison failed: files not found'); return false; } const beforeStats = fs.statSync(beforePath); const afterStats = fs.statSync(afterPath); const areEqual = beforeStats.size === afterStats.size; testLogger.info(`Screenshot comparison result: ${areEqual}`); return areEqual; } async createScreenshotReport(): Promise { testLogger.info('Creating screenshot report'); const reportPath = path.join(this.outputDir, 'screenshot-report.md'); const screenshots = this.getAllScreenshots(); let report = '# Screenshot Report\n\n'; report += `Generated at: ${new Date().toISOString()}\n`; report += `Total screenshots: ${screenshots.length}\n\n`; report += '## Screenshots\n\n'; for (const screenshot of screenshots) { report += `### ${screenshot.filename}\n`; report += `- **Path**: ${screenshot.path}\n`; report += `- **Timestamp**: ${screenshot.timestamp}\n`; if (screenshot.description) { report += `- **Description**: ${screenshot.description}\n`; } report += '\n'; } fs.writeFileSync(reportPath, report); testLogger.info(`Screenshot report created: ${reportPath}`); return reportPath; } private generateFilename(filename?: string): string { if (filename) { return `${filename}.${this.defaultConfig.type || 'png'}`; } return `screenshot-${Date.now()}.${this.defaultConfig.type || 'png'}`; } private ensureOutputDir(): void { if (!fs.existsSync(this.outputDir)) { fs.mkdirSync(this.outputDir, { recursive: true }); testLogger.info(`Output directory created: ${this.outputDir}`); } } }