- Add HVAC_Test_User_Factory class with: * User creation with specific roles * Multiple role support * Persona management system * Account cleanup integration - Create comprehensive test suite in HVAC_Test_User_Factory_Test.php - Update testing improvement plan documentation - Add implementation decisions to project memory bank - Restructure .gitignore with: * Whitelist approach for better file management * Explicit backup exclusions * Specific bin directory inclusions Part of the Account Management component from the testing framework improvement plan.
175 lines
No EOL
6.6 KiB
TypeScript
175 lines
No EOL
6.6 KiB
TypeScript
import { TestCase, TestResult } from '@playwright/test/reporter';
|
|
import { BaseReporter, ReportMetrics } from './BaseReporter';
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
|
|
export class HtmlReporter extends BaseReporter {
|
|
private outputDir: string;
|
|
private template: string;
|
|
|
|
constructor(options: { outputDir: string }) {
|
|
super();
|
|
this.outputDir = options.outputDir;
|
|
this.template = `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Test Report</title>
|
|
<style>
|
|
body { font-family: Arial, sans-serif; margin: 2rem; }
|
|
.summary { background: #f5f5f5; padding: 1rem; border-radius: 4px; }
|
|
.test-case { margin: 1rem 0; padding: 1rem; border: 1px solid #ddd; }
|
|
.passed { border-left: 4px solid #4CAF50; }
|
|
.failed { border-left: 4px solid #f44336; }
|
|
.skipped { border-left: 4px solid #9E9E9E; }
|
|
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
|
|
.metric { background: #fff; padding: 0.5rem; border-radius: 4px; }
|
|
.attachments { display: flex; gap: 1rem; flex-wrap: wrap; margin-top: 1rem; }
|
|
.attachment { max-width: 200px; }
|
|
.error { background: #ffebee; padding: 1rem; margin: 1rem 0; border-radius: 4px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="report-content"></div>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
async onEnd() {
|
|
const counts = this.getStatusCounts();
|
|
const content = [];
|
|
|
|
// Summary section
|
|
content.push(`
|
|
<div class="summary">
|
|
<h2>Test Summary</h2>
|
|
<p>
|
|
Total Tests: ${counts.passed + counts.failed + counts.skipped}<br>
|
|
Passed: ${counts.passed}<br>
|
|
Failed: ${counts.failed}<br>
|
|
Skipped: ${counts.skipped}
|
|
</p>
|
|
</div>
|
|
`);
|
|
|
|
// Test cases
|
|
this.testResults.forEach((results, title) => {
|
|
results.forEach(result => {
|
|
const metrics = this.metrics.get(title);
|
|
const attachments = this.getAttachments(result);
|
|
|
|
content.push(`
|
|
<div class="test-case ${result.status}">
|
|
<h3>${title}</h3>
|
|
<div class="metrics">
|
|
<div class="metric">
|
|
<strong>Duration:</strong> ${this.formatDuration(metrics?.duration || 0)}
|
|
</div>
|
|
<div class="metric">
|
|
<strong>Memory:</strong> ${Math.round(metrics?.memory || 0)}MB
|
|
</div>
|
|
<div class="metric">
|
|
<strong>Network Requests:</strong> ${metrics?.networkRequests || 0}
|
|
</div>
|
|
</div>
|
|
${result.error ? `
|
|
<div class="error">
|
|
<strong>Error:</strong><br>
|
|
<pre>${result.error.message}</pre>
|
|
</div>
|
|
` : ''}
|
|
${this.renderAttachments(attachments)}
|
|
</div>
|
|
`);
|
|
});
|
|
});
|
|
|
|
// Generate the final HTML
|
|
const html = this.template.replace(
|
|
'<div id="report-content"></div>',
|
|
`<div id="report-content">${content.join('')}</div>`
|
|
);
|
|
|
|
// Ensure output directory exists
|
|
await fs.mkdir(this.outputDir, { recursive: true });
|
|
|
|
// Write the report
|
|
await fs.writeFile(
|
|
path.join(this.outputDir, 'report.html'),
|
|
html
|
|
);
|
|
|
|
// Copy attachments to the output directory
|
|
await this.copyAttachments();
|
|
}
|
|
|
|
private async copyAttachments() {
|
|
const attachmentsDir = path.join(this.outputDir, 'attachments');
|
|
await fs.mkdir(attachmentsDir, { recursive: true });
|
|
|
|
for (const results of this.testResults.values()) {
|
|
for (const result of results) {
|
|
for (const attachment of result.attachments) {
|
|
if (attachment.path) {
|
|
const destPath = path.join(attachmentsDir, path.basename(attachment.path));
|
|
await fs.copyFile(attachment.path, destPath);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private renderAttachments(attachments: ReturnType<typeof this.getAttachments>) {
|
|
const elements = [];
|
|
|
|
if (attachments.screenshots.length) {
|
|
elements.push('<div class="attachments"><h4>Screenshots:</h4>');
|
|
attachments.screenshots.forEach(screenshot => {
|
|
if (screenshot.path) {
|
|
const filename = path.basename(screenshot.path);
|
|
elements.push(`
|
|
<div class="attachment">
|
|
<img src="attachments/${filename}" alt="Screenshot" style="max-width: 100%">
|
|
</div>
|
|
`);
|
|
}
|
|
});
|
|
elements.push('</div>');
|
|
}
|
|
|
|
if (attachments.videos.length) {
|
|
elements.push('<div class="attachments"><h4>Videos:</h4>');
|
|
attachments.videos.forEach(video => {
|
|
if (video.path) {
|
|
const filename = path.basename(video.path);
|
|
elements.push(`
|
|
<div class="attachment">
|
|
<video controls style="max-width: 100%">
|
|
<source src="attachments/${filename}" type="${video.contentType}">
|
|
</video>
|
|
</div>
|
|
`);
|
|
}
|
|
});
|
|
elements.push('</div>');
|
|
}
|
|
|
|
if (attachments.traces.length) {
|
|
elements.push('<div class="attachments"><h4>Traces:</h4>');
|
|
attachments.traces.forEach(trace => {
|
|
if (trace.path) {
|
|
const filename = path.basename(trace.path);
|
|
elements.push(`
|
|
<div class="attachment">
|
|
<a href="attachments/${filename}" target="_blank">View Trace</a>
|
|
</div>
|
|
`);
|
|
}
|
|
});
|
|
elements.push('</div>');
|
|
}
|
|
|
|
return elements.join('');
|
|
}
|
|
} |