upskill-event-manager/tests/framework/utils/ScreenshotManager.js
Ben 7c9ca65cf2
Some checks are pending
HVAC Plugin CI/CD Pipeline / Security Analysis (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Code Quality & Standards (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Unit Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Integration Tests (push) Waiting to run
HVAC Plugin CI/CD Pipeline / Deploy to Staging (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Deploy to Production (push) Blocked by required conditions
HVAC Plugin CI/CD Pipeline / Notification (push) Blocked by required conditions
Security Monitoring & Compliance / Dependency Vulnerability Scan (push) Waiting to run
Security Monitoring & Compliance / Secrets & Credential Scan (push) Waiting to run
Security Monitoring & Compliance / WordPress Security Analysis (push) Waiting to run
Security Monitoring & Compliance / Static Code Security Analysis (push) Waiting to run
Security Monitoring & Compliance / Security Compliance Validation (push) Waiting to run
Security Monitoring & Compliance / Security Summary Report (push) Blocked by required conditions
Security Monitoring & Compliance / Security Team Notification (push) Blocked by required conditions
feat: add comprehensive test framework and test files
- Add 90+ test files including E2E, unit, and integration tests
- Implement Page Object Model (POM) architecture
- Add Docker testing environment with comprehensive services
- Include modernized test framework with error recovery
- Add specialized test suites for master trainer and trainer workflows
- Update .gitignore to properly track test infrastructure

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-29 23:23:26 -03:00

469 lines
No EOL
16 KiB
JavaScript

/**
* Screenshot Manager for HVAC Testing Framework
*
* Provides comprehensive screenshot functionality:
* - Organized screenshot storage with naming conventions
* - Failure capture with context information
* - Full page and element-specific screenshots
* - Screenshot comparison utilities
*
* @package HVAC_Community_Events
* @version 2.0.0
* @created 2025-08-27
*/
const fs = require('fs').promises;
const path = require('path');
const ConfigManager = require('../core/ConfigManager');
class ScreenshotManager {
constructor() {
this.config = ConfigManager;
this.baseDir = this.config.get('media.screenshotDir', './test-results/screenshots');
this.quality = this.config.get('media.quality', 80);
this.fullPage = this.config.get('media.fullPage', false);
this.ensureDirectoryExists();
}
/**
* Ensure screenshots directory exists
*/
async ensureDirectoryExists() {
try {
await fs.mkdir(this.baseDir, { recursive: true });
} catch (error) {
console.warn('Could not create screenshots directory:', error.message);
}
}
/**
* Capture screenshot with context and naming
*/
async capture(page, name, basePath = null, options = {}) {
try {
const screenshotPath = await this.generatePath(name, basePath, options);
const screenshotOptions = {
path: screenshotPath,
quality: options.quality || this.quality,
fullPage: options.fullPage !== undefined ? options.fullPage : this.fullPage,
clip: options.clip,
timeout: options.timeout || 5000,
...options
};
// Remove custom options that aren't part of Playwright's screenshot API
delete screenshotOptions.category;
delete screenshotOptions.testId;
delete screenshotOptions.metadata;
await page.screenshot(screenshotOptions);
console.log(`📸 Screenshot captured: ${screenshotPath}`);
// Save metadata if provided
if (options.metadata) {
await this.saveMetadata(screenshotPath, options.metadata);
}
return screenshotPath;
} catch (error) {
console.error('❌ Screenshot capture failed:', error.message);
throw error;
}
}
/**
* Capture failure screenshot with additional context
*/
async captureFailure(page, testInfo, error, options = {}) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const testName = this.sanitizeFileName(testInfo.title);
const name = `failure-${testName}-${timestamp}`;
const metadata = {
testTitle: testInfo.title,
testFile: testInfo.file,
error: error.message,
url: page.url(),
timestamp: new Date().toISOString(),
viewport: await page.viewportSize(),
userAgent: await page.evaluate(() => navigator.userAgent),
...options.metadata
};
try {
const screenshotPath = await this.capture(page, name, null, {
fullPage: true,
metadata,
category: 'failures',
...options
});
// Also capture console logs
await this.captureConsoleLogs(page, screenshotPath, testInfo);
// Capture page source
await this.capturePageSource(page, screenshotPath);
return screenshotPath;
} catch (captureError) {
console.error('Failed to capture failure screenshot:', captureError.message);
return null;
}
}
/**
* Capture element-specific screenshot
*/
async captureElement(page, selector, name, options = {}) {
try {
const element = await page.locator(selector);
await element.waitFor({ state: 'visible', timeout: 5000 });
const boundingBox = await element.boundingBox();
if (!boundingBox) {
throw new Error(`Element not visible or has no bounding box: ${selector}`);
}
return await this.capture(page, name, null, {
clip: boundingBox,
...options
});
} catch (error) {
console.error(`Failed to capture element screenshot (${selector}):`, error.message);
throw error;
}
}
/**
* Capture comparison screenshots (before/after)
*/
async captureComparison(page, name, action, options = {}) {
const beforePath = await this.capture(page, `${name}-before`, null, options);
// Perform the action
if (typeof action === 'function') {
await action();
}
const afterPath = await this.capture(page, `${name}-after`, null, options);
return { before: beforePath, after: afterPath };
}
/**
* Capture mobile/desktop responsive comparison
*/
async captureResponsive(page, name, options = {}) {
const originalViewport = page.viewportSize();
// Desktop screenshot
const desktopViewport = this.config.get('browser.viewport');
await page.setViewportSize(desktopViewport);
const desktopPath = await this.capture(page, `${name}-desktop`, null, options);
// Mobile screenshot
const mobileViewport = this.config.get('browser.mobileViewport');
await page.setViewportSize(mobileViewport);
const mobilePath = await this.capture(page, `${name}-mobile`, null, options);
// Restore original viewport
if (originalViewport) {
await page.setViewportSize(originalViewport);
}
return { desktop: desktopPath, mobile: mobilePath };
}
/**
* Generate screenshot file path
*/
async generatePath(name, basePath = null, options = {}) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const sanitizedName = this.sanitizeFileName(name);
let directory = basePath || this.baseDir;
// Create category subdirectory if specified
if (options.category) {
directory = path.join(directory, options.category);
await fs.mkdir(directory, { recursive: true });
}
// Create test-specific subdirectory if testId provided
if (options.testId) {
directory = path.join(directory, this.sanitizeFileName(options.testId));
await fs.mkdir(directory, { recursive: true });
}
const filename = `${sanitizedName}-${timestamp}.png`;
return path.join(directory, filename);
}
/**
* Save screenshot metadata
*/
async saveMetadata(screenshotPath, metadata) {
try {
const metadataPath = screenshotPath.replace('.png', '.json');
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
} catch (error) {
console.warn('Could not save screenshot metadata:', error.message);
}
}
/**
* Capture console logs alongside screenshot
*/
async captureConsoleLogs(page, screenshotPath, testInfo) {
try {
const logs = [];
// Get console messages from page context
page.on('console', msg => {
logs.push({
type: msg.type(),
text: msg.text(),
location: msg.location(),
timestamp: new Date().toISOString()
});
});
// Save console logs
const logsPath = screenshotPath.replace('.png', '-console.json');
const logData = {
testTitle: testInfo.title,
url: page.url(),
timestamp: new Date().toISOString(),
logs
};
await fs.writeFile(logsPath, JSON.stringify(logData, null, 2));
} catch (error) {
console.warn('Could not capture console logs:', error.message);
}
}
/**
* Capture page source alongside screenshot
*/
async capturePageSource(page, screenshotPath) {
try {
const pageSource = await page.content();
const sourcePath = screenshotPath.replace('.png', '-source.html');
await fs.writeFile(sourcePath, pageSource);
} catch (error) {
console.warn('Could not capture page source:', error.message);
}
}
/**
* Sanitize filename for cross-platform compatibility
*/
sanitizeFileName(name) {
return name
.replace(/[^a-zA-Z0-9\-_.]/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '')
.toLowerCase();
}
/**
* Clean up old screenshots (keep last N days)
*/
async cleanup(daysToKeep = 7) {
try {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
const files = await this.getScreenshotFiles(this.baseDir);
let deletedCount = 0;
for (const file of files) {
const stats = await fs.stat(file);
if (stats.mtime < cutoffDate) {
await fs.unlink(file);
deletedCount++;
}
}
console.log(`🧹 Cleaned up ${deletedCount} old screenshots`);
} catch (error) {
console.warn('Could not clean up screenshots:', error.message);
}
}
/**
* Get all screenshot files recursively
*/
async getScreenshotFiles(directory) {
const files = [];
try {
const entries = await fs.readdir(directory, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
const subFiles = await this.getScreenshotFiles(fullPath);
files.push(...subFiles);
} else if (entry.name.endsWith('.png')) {
files.push(fullPath);
}
}
} catch (error) {
// Directory doesn't exist or can't be read
}
return files;
}
/**
* Generate screenshot summary report
*/
async generateReport() {
try {
const files = await this.getScreenshotFiles(this.baseDir);
const report = {
totalScreenshots: files.length,
categories: {},
generatedAt: new Date().toISOString()
};
// Group by category (subdirectory)
for (const file of files) {
const relativePath = path.relative(this.baseDir, file);
const category = path.dirname(relativePath);
if (!report.categories[category]) {
report.categories[category] = 0;
}
report.categories[category]++;
}
const reportPath = path.join(this.baseDir, 'screenshot-report.json');
await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
console.log(`📊 Screenshot report generated: ${reportPath}`);
return report;
} catch (error) {
console.warn('Could not generate screenshot report:', error.message);
return null;
}
}
/**
* Create screenshot gallery HTML
*/
async createGallery(title = 'Test Screenshots') {
try {
const files = await this.getScreenshotFiles(this.baseDir);
const galleryData = [];
for (const file of files) {
const relativePath = path.relative(this.baseDir, file);
const stats = await fs.stat(file);
// Try to load metadata
const metadataPath = file.replace('.png', '.json');
let metadata = null;
try {
const metadataContent = await fs.readFile(metadataPath, 'utf8');
metadata = JSON.parse(metadataContent);
} catch (error) {
// No metadata file
}
galleryData.push({
path: relativePath,
filename: path.basename(file),
size: stats.size,
created: stats.birthtime,
modified: stats.mtime,
metadata
});
}
// Sort by creation date (newest first)
galleryData.sort((a, b) => b.created - a.created);
// Generate HTML
const html = this.generateGalleryHTML(title, galleryData);
const galleryPath = path.join(this.baseDir, 'gallery.html');
await fs.writeFile(galleryPath, html);
console.log(`🖼️ Screenshot gallery created: ${galleryPath}`);
return galleryPath;
} catch (error) {
console.warn('Could not create screenshot gallery:', error.message);
return null;
}
}
/**
* Generate HTML for screenshot gallery
*/
generateGalleryHTML(title, screenshots) {
const screenshotCards = screenshots.map(screenshot => `
<div class="screenshot-card">
<img src="${screenshot.path}" alt="${screenshot.filename}" loading="lazy">
<div class="screenshot-info">
<h4>${screenshot.filename}</h4>
<p><strong>Created:</strong> ${screenshot.created.toLocaleString()}</p>
<p><strong>Size:</strong> ${(screenshot.size / 1024).toFixed(1)} KB</p>
${screenshot.metadata ? `
<details>
<summary>Metadata</summary>
<pre>${JSON.stringify(screenshot.metadata, null, 2)}</pre>
</details>
` : ''}
</div>
</div>
`).join('');
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
<style>
body { font-family: system-ui, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
h1 { text-align: center; color: #333; }
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
.screenshot-card { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.screenshot-card img { width: 100%; height: 200px; object-fit: cover; }
.screenshot-info { padding: 15px; }
.screenshot-info h4 { margin: 0 0 10px 0; color: #333; }
.screenshot-info p { margin: 5px 0; color: #666; font-size: 14px; }
details { margin-top: 10px; }
pre { background: #f8f8f8; padding: 10px; border-radius: 4px; font-size: 12px; overflow-x: auto; }
.stats { text-align: center; margin-bottom: 30px; padding: 20px; background: white; border-radius: 8px; }
</style>
</head>
<body>
<h1>${title}</h1>
<div class="stats">
<p><strong>Total Screenshots:</strong> ${screenshots.length}</p>
<p><strong>Generated:</strong> ${new Date().toLocaleString()}</p>
</div>
<div class="gallery">
${screenshotCards}
</div>
</body>
</html>
`;
}
}
module.exports = ScreenshotManager;