wdio-html-enhanced-reporter
Version:
Light weight custom reporter for large wdio automation projects
301 lines (251 loc) • 8.46 kB
JavaScript
import WDIOReporter from '@wdio/reporter';
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import { join, relative } from 'path';
import CreateHTMLReport from './create_html_report.js';
import Helper from './utils/helper.js';
class CustomHtmlReporter extends WDIOReporter {
constructor(options) {
options = Object.assign(
{
outputDir: './reports',
filename: 'wdio-custom-html-reporter.html',
reportTitle: 'Test Report',
showInBrowser: false,
collapseTests: true,
screenshotDir: null,
saveScreenshots: true,
thumbnailWidth: 200,
},
options,
);
if (!options.logFile) {
options.logFile = `${options.outputDir}/logfile.txt`;
}
super(options);
this.options = options;
this.suites = [];
this.suiteMapByUid = new Map();
this.suiteMapByTitle = new Map();
this.specs = [];
this.results = {
passed: 0,
failed: 0,
skipped: 0,
};
this.startTime = null;
this.endTime = null;
this.options.screenshotDir = this.options.screenshotDir || join(this.options.outputDir, 'screenshots');
this.screenshotCounts = {};
this.currentTestUid = null;
this.testScreenshots = {};
this.testLogs = {};
this.testCountPerSpec = {};
process.on('test:screenshot', filepath => {
if (this.currentTestUid) {
if (!this.testScreenshots[this.currentTestUid]) {
this.testScreenshots[this.currentTestUid] = [];
}
this.testScreenshots[this.currentTestUid].push({
path: filepath,
title: `Screenshot ${this.testScreenshots[this.currentTestUid].length + 1}`,
});
}
});
process.on('test:log', message => {
if (this.currentTestUid) {
if (!this.testLogs[this.currentTestUid]) {
this.testLogs[this.currentTestUid] = [];
}
this.testLogs[this.currentTestUid].push({ level: 'info', message });
}
});
}
onRunnerStart(runner) {
this.startTime = new Date();
this.specs = runner.specs || [];
this.specFileIndex = 0;
}
onSuiteStart(suite) {
const parentUid = suite.parentUid || suite.parent || null;
let file =
!parentUid && this.specs && this.specFileIndex < this.specs.length
? new URL(this.specs[this.specFileIndex++]).pathname
: '';
const newSuite = {
uid: suite.uid,
title: suite.title,
tests: [],
suites: [],
parentUid,
file,
totalTestsInSpec: this.testCountPerSpec[file] || 0,
};
if (!this.suiteMapByTitle.has(suite.title)) {
this.suiteMapByTitle.set(suite.title, []);
}
this.suiteMapByTitle.get(suite.title).push(newSuite);
if (!parentUid) {
this.suites.push(newSuite);
} else {
const parentSuite =
Helper.findSuiteByUid(parentUid, this.suites, this.suiteMapByTitle) ||
Helper.findLatestSuiteByTitle(this.suiteMapByTitle, parentUid);
if (parentSuite) {
parentSuite.suites.push(newSuite);
} else {
console.warn(`Parent suite not found for uid or title: ${parentUid}`);
this.suites.push(newSuite);
}
}
}
onTestStart(test) {
const currentFile = this.specs[this.specFileIndex - 1];
if (!this.testCountPerSpec[currentFile]) this.testCountPerSpec[currentFile] = 0;
this.testCountPerSpec[currentFile]++;
const parentUid = test.parentUid || test.parent || null;
let currentSuite = Helper.findSuiteByUid(parentUid, this.suites, this.suiteMapByTitle);
if (!currentSuite && this.suiteMapByTitle.has(parentUid)) {
const candidateSuites = this.suiteMapByTitle.get(parentUid);
currentSuite = candidateSuites[candidateSuites.length - 1];
}
const testEntry = {
uid: test.uid,
title: test.title,
state: 'pending',
duration: 0,
logs: [],
screenshots: [],
error: null,
};
if (currentSuite) {
currentSuite.tests.push(testEntry);
} else {
console.warn(`Test '${test.title}' could not find suite with uid/title: ${parentUid}`);
}
this.currentTest = testEntry;
this.currentTestUid = test.uid;
this.screenshotCounts[test.uid] = 0;
}
onTestPass(test) {
Helper.updateTestStatus(test, 'passed', this.suiteMapByTitle, this.suites, this.testScreenshots, this.testLogs);
this.results.passed++;
}
onTestFail(test) {
Helper.updateTestStatus(test, 'failed', this.suiteMapByTitle, this.suites, this.testScreenshots, this.testLogs);
this.results.failed++;
}
onTestSkip(test) {
Helper.updateTestStatus(test, 'skipped', this.suiteMapByTitle, this.suites, this.testScreenshots, this.testLogs);
this.results.skipped++;
}
onAfterCommand(command) {
if (command.method === 'saveScreenshot' && command.result) {
const testUid = command.cid;
if (this.screenshotCounts[testUid] < this.options.screenshotsPerTest) {
this.handleScreenshot(command.result, testUid, `Screenshot ${this.screenshotCounts[testUid] + 1}`);
this.screenshotCounts[testUid]++;
}
}
}
handleScreenshot(base64Image, testUid, title) {
let currentTest = null;
for (const suite of this.suites) {
currentTest = suite.tests.find(t => t.uid === testUid);
if (currentTest) break;
}
if (!currentTest) return;
if (this.options.saveScreenshots) {
if (!existsSync(this.options.screenshotDir)) {
mkdirSync(this.options.screenshotDir, { recursive: true });
}
const timestamp = new Date().toISOString().replace(/:/g, '-');
const filename = `${timestamp}.png`;
const filepath = join(this.options.screenshotDir, filename);
const imageBuffer = Buffer.from(base64Image.replace(/^data:image\/png;base64,/, ''), 'base64');
writeFileSync(filepath, imageBuffer);
const relativePath = relative(this.options.outputDir, filepath);
currentTest.screenshots.push({
title,
path: relativePath,
timestamp: new Date(),
});
} else {
if (currentTest.screenshots.length < this.options.screenshotsPerTest) {
currentTest.screenshots.push({
title,
data: base64Image,
timestamp: new Date(),
});
}
}
}
onSuiteEnd(suite) {
const currentSuite = Helper.findSuiteByUid(suite.uid, this.suites, this.suiteMapByTitle);
if (currentSuite) {
currentSuite.end = new Date();
if (currentSuite.start) {
currentSuite.duration = currentSuite.end - currentSuite.start;
}
} else {
console.warn(`Suite with UID ${suite.uid} not found on end`);
}
}
onHookEnd(hook) {
if (hook.error) {
this.hasFailures = true;
const parentSuite = this.findSuiteByUid(hook.parentUid, this.suites);
if (parentSuite) {
parentSuite.tests.push({
uid: hook.uid,
title: `${hook.title} (hook)`,
state: 'failed',
duration: hook._duration || 0,
logs: [],
screenshots: [],
error: {
message: hook.error.message,
stack: hook.error.stack,
},
});
}
}
}
onRunnerEnd() {
this.endTime = new Date();
for (const suite of this.suites) {
suite.totalTestsInSpec = this.countTestsInSuite(suite);
}
const hasTestFailures = this.suites.some(suite => suite.tests.some(test => test.state === 'failed'));
const hasFailures = hasTestFailures || this.hasFailures;
this.generateReport(hasFailures);
}
countTestsInSuite(suite) {
let count = suite.tests.length;
for (const nestedSuite of suite.suites) {
count += this.countTestsInSuite(nestedSuite);
}
return count;
}
generateReport(hasFailures = false) {
if (!existsSync(this.options.outputDir)) {
mkdirSync(this.options.outputDir, { recursive: true });
}
const htmlContent = CreateHTMLReport.createHtmlReport(
this.suites,
this.results,
this.startTime,
this.endTime,
this.specs,
this.options,
hasFailures,
);
const reportPath = join(this.options.outputDir, this.options.filename);
writeFileSync(reportPath, htmlContent);
if (this.options.showInBrowser) {
const open = require('open');
open(reportPath);
}
console.log(`Report generated: ${reportPath}`);
}
}
export default CustomHtmlReporter;