playwright-archive
Version:
A lightweight CLI tool to archive and serve your Playwright test run history with a web interface. Useful for CI environments and local development.
274 lines (221 loc) • 9.26 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
class RunResultsGenerator {
constructor(data, config) {
this.data = data || { config: {}, stats: {}, suites: [] };
this.config = config?.runResults || {};
}
async generate() {
if (!this.config.enabled) {
return;
}
try {
const outputPath = path.resolve(process.cwd(), this.config.path || './playwright-results.txt');
const format = this.config.format || 'text';
const include = this.config.include || ['status', 'summary'];
const content = format === 'json'
? this.generateJson(include)
: this.generateText(include);
await fs.outputFile(outputPath, content);
console.log(`✅ Run results saved to ${outputPath}`);
} catch (error) {
console.error("Failed to generate run results:", error);
// Don't throw, just log the error to prevent crashing the process
}
}
generateJson(include) {
const result = {};
if (include.includes('status')) {
result.status = this.getOverallStatus();
}
if (include.includes('summary')) {
result.summary = this.getSummary();
}
if (include.includes('duration')) {
result.duration = this.formatDuration(this.data.stats?.duration);
}
if (include.includes('failedTests')) {
result.failedTests = this.getFailedTests();
}
if (include.includes('flakyTests')) {
result.flakyTests = this.getFlakyTests();
}
if (include.includes('projectStats')) {
result.projectStats = this.getProjectStats();
}
if (include.includes('errorMessages')) {
result.errorMessages = this.getErrorMessages();
}
return JSON.stringify(result, null, 2);
}
generateText(include) {
if (this.config.template) {
return this.generateFromTemplate();
}
const sections = [];
sections.push('Test Run Results');
sections.push('---------------');
if (include.includes('status')) {
sections.push(`Status: ${this.getOverallStatus()}`);
}
if (include.includes('duration')) {
sections.push(`Duration: ${this.formatDuration(this.data.stats?.duration)}`);
}
if (include.includes('summary')) {
const summary = this.getSummary();
sections.push('\nSummary:');
sections.push(`* Total: ${summary.total}`);
sections.push(`* Passed: ${summary.passed}`);
sections.push(`* Failed: ${summary.failed}`);
sections.push(`* Skipped: ${summary.skipped}`);
if (summary.flaky > 0) {
sections.push(`* Flaky: ${summary.flaky}`);
}
}
if (include.includes('failedTests')) {
const failedTests = this.getFailedTests();
if (failedTests.length > 0) {
sections.push('\nFailed Tests:');
failedTests.forEach(test => {
sections.push(`* ${test}`);
});
}
}
if (include.includes('flakyTests')) {
const flakyTests = this.getFlakyTests();
if (flakyTests.length > 0) {
sections.push('\nFlaky Tests:');
flakyTests.forEach(test => {
sections.push(`* ${test}`);
});
}
}
if (include.includes('projectStats')) {
const projectStats = this.getProjectStats();
if (Object.keys(projectStats).length > 0) {
sections.push('\nProject Statistics:');
Object.entries(projectStats).forEach(([project, stats]) => {
sections.push(`\n${project}:`);
sections.push(`* Passed: ${stats.passed}`);
sections.push(`* Failed: ${stats.failed}`);
sections.push(`* Skipped: ${stats.skipped}`);
});
}
}
if (include.includes('errorMessages')) {
const errors = this.getErrorMessages();
if (errors.length > 0) {
sections.push('\nError Messages:');
errors.forEach((error, index) => {
sections.push(`\n${index + 1}) ${error}`);
});
}
}
return sections.join('\n');
}
generateFromTemplate() {
let content = this.config.template;
const summary = this.getSummary();
// Replace template variables
const variables = {
status: this.getOverallStatus(),
duration: this.formatDuration(this.data.stats?.duration),
totalTests: summary.total,
passedTests: summary.passed,
failedTests: summary.failed,
skippedTests: summary.skipped,
flakyTests: summary.flaky,
hasFailedTests: summary.failed > 0,
failedTestsList: this.getFailedTests().map(test => `* ${test}`).join('\n'),
errorMessages: this.getErrorMessages().map((err, i) => `${i + 1}) ${err}`).join('\n'),
};
// Handle conditional blocks {condition?content:} in template
content = content.replace(/\{(\w+)\?([\s\S]*?)\:}/g, (match, condition, text) => {
return variables[condition] ? text : '';
});
// Replace variables
Object.entries(variables).forEach(([key, value]) => {
content = content.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
});
return content;
}
getOverallStatus() {
return (this.data?.stats?.unexpected || 0) > 0 ? 'Failed' : 'Passed';
}
getSummary() {
const stats = this.data?.stats || {};
return {
total: (stats.expected || 0) + (stats.unexpected || 0) + (stats.skipped || 0),
passed: stats.expected || 0,
failed: stats.unexpected || 0,
skipped: stats.skipped || 0,
flaky: stats.flaky || 0
};
}
getFailedTests() {
if (!Array.isArray(this.data?.suites)) return [];
return this.data.suites
.filter(suite => suite && Array.isArray(suite.specs) && suite.specs.some(spec => spec && !spec.ok))
.map(suite => suite.title || "Unknown Test")
.filter(Boolean);
}
getFlakyTests() {
if (!Array.isArray(this.data?.suites)) return [];
return this.data.suites
.filter(suite => suite && Array.isArray(suite.specs) && suite.specs.some(spec => spec && spec.flaky))
.map(suite => suite.title || "Unknown Test")
.filter(Boolean);
}
getProjectStats() {
const stats = {};
const projects = this.data?.config?.projects;
if (!Array.isArray(projects)) return stats;
projects.forEach(project => {
if (!project || !Array.isArray(project.suites)) return;
const projectStats = {
passed: 0,
failed: 0,
skipped: 0
};
project.suites.forEach(suite => {
if (!suite || !Array.isArray(suite.specs)) return;
suite.specs.forEach(spec => {
if (!spec) return;
if (!spec.ok) projectStats.failed++;
else if (spec.skipped) projectStats.skipped++;
else projectStats.passed++;
});
});
if (projectStats.passed + projectStats.failed + projectStats.skipped > 0) {
stats[project.name || "Unknown Project"] = projectStats;
}
});
return stats;
}
getErrorMessages() {
if (!Array.isArray(this.data?.suites)) return [];
const errors = [];
this.data.suites.forEach(suite => {
if (!suite || !Array.isArray(suite.specs)) return;
suite.specs.forEach(spec => {
if (spec && !spec.ok && spec.error?.message) {
const title = suite.title || "Unknown Test";
errors.push(`${title}: ${spec.error.message}`);
}
});
});
return errors;
}
formatDuration(ms) {
if (!ms) return '0s';
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(minutes / 60);
const parts = [];
if (hours > 0) parts.push(`${hours}h`);
if (minutes % 60 > 0) parts.push(`${minutes % 60}m`);
if (seconds % 60 > 0) parts.push(`${seconds % 60}s`);
return parts.join(' ') || '0s';
}
}
module.exports = RunResultsGenerator;