wdio-azure-devops-test-reporter
Version:
A WebdriverIO reporter to create reports to import into Azure Test Plan
146 lines (145 loc) • 6.82 kB
JavaScript
import WDIOReporter from "@wdio/reporter";
import { Utils } from "./utils.js";
import { AzureTestResultOutcome, AzureTestRunStatus, AzureTestStatusCucumberStatusMap } from "./constants.js";
import fs from "fs";
export default class AzureDevopsReporter extends WDIOReporter {
reporterOptions;
azureConfigurationId;
utils;
azureTestResult;
screenshots;
currentTestCaseAzureID;
currentIterationID;
currentActionPathIndex;
constructor(options) {
if (!options) {
throw new Error("Invalid Azure Reporter Configuration");
}
const { outputDir = "./reports/azure", ...rest } = options;
super({ ...rest, outputDir });
this.reporterOptions = { ...rest, outputDir };
this.utils = new Utils();
this.azureTestResult = [];
this.azureConfigurationId = "";
this.screenshots = [];
this.currentTestCaseAzureID = "";
this.currentIterationID = 0;
this.currentActionPathIndex = 1;
}
onRunnerStart(runner) {
if (!runner.isMultiremote) {
const capabilities = runner.config.capabilities ?? runner.capabilities;
this.azureConfigurationId = this.utils.getAzureConfigurationIDByCapability(capabilities, this.reporterOptions.azureConfigurationCapabilities);
}
this.outputStream = fs.createWriteStream(`${this.options.outputDir}/result-${runner.sessionId}.json`);
}
onRunnerEnd() {
const report = {
azureConfigurationId: this.azureConfigurationId,
testResults: this.azureTestResult,
screenshots: this.screenshots,
};
this.write(JSON.stringify(report, null, 2));
}
onSuiteStart(suite) {
const isScenario = suite.type === "scenario";
if (isScenario) {
const testCaseAzureID = String(this.utils.parseCaseID(suite.tags, this.reporterOptions.caseIdRegex));
if (testCaseAzureID !== this.currentTestCaseAzureID) {
this.currentTestCaseAzureID = testCaseAzureID;
this.azureTestResult.push({
testCase: { id: testCaseAzureID },
iterationDetails: [],
outcome: AzureTestResultOutcome.Passed,
startedDate: suite.start,
});
this.currentIterationID = 1;
this.currentActionPathIndex = 1;
}
else {
this.currentIterationID++;
}
}
}
onTestStart(test) {
const isStep = test.type === "test";
if (isStep)
this.currentActionPathIndex++;
}
onSuiteEnd(suite) {
const isScenario = suite.type === "scenario";
const isFeature = suite.type === "feature";
if (isScenario) {
if (this.azureTestResult.length > 1) {
const lastFinishTestResult = this.azureTestResult[this.azureTestResult.length - 2];
const lastIteration = lastFinishTestResult.iterationDetails
? lastFinishTestResult.iterationDetails[lastFinishTestResult.iterationDetails.length - 1]
: undefined;
lastFinishTestResult.state = AzureTestRunStatus.COMPLETED;
lastFinishTestResult.completedDate = lastIteration?.completedDate;
}
const iterationDetailsLength = this.azureTestResult[this.azureTestResult.length - 1].iterationDetails?.length ?? 0;
const currentIterationId = iterationDetailsLength + 1;
let isSuitePassed = true;
const actionResults = [];
// The azure step ID value for first step starts with 2
let actionPathIndex = 2;
suite.tests.forEach((test) => {
const actionPath = this.utils.convertIdToAzureActionPathId(actionPathIndex);
const testArgument = this.utils.formatTestArgument(test.argument);
actionResults.push({
iterationId: currentIterationId,
actionPath: actionPath,
stepIdentifier: String(actionPathIndex),
outcome: AzureTestStatusCucumberStatusMap[test.state],
startedDate: test.start,
completedDate: test.end,
durationInMs: test._duration,
errorMessage: `Step: ${test.title}\n\n${testArgument ? testArgument + "\n\n" : ""}Error message: ${test.error?.message ? test.error?.message + "\n\n" : ""}Error stack: ${test.error?.stack ? this.utils.removeColorCode(test.error.stack) : ""}`,
});
actionPathIndex++;
if (test.state === "failed") {
isSuitePassed = false;
}
});
const iterationModel = {
outcome: isSuitePassed
? AzureTestStatusCucumberStatusMap.passed
: AzureTestStatusCucumberStatusMap.failed,
id: currentIterationId,
startedDate: suite.start,
completedDate: suite.end,
/* Must be converted from ms to hundreds of microseconds
to display correctly on the iteration detail section.
This may be a bug from azure side.
*/
durationInMs: suite._duration * 10000,
actionResults: actionResults,
};
if (!isSuitePassed)
this.azureTestResult[this.azureTestResult.length - 1].outcome = AzureTestResultOutcome.Failed;
this.azureTestResult[this.azureTestResult.length - 1].iterationDetails?.push(iterationModel);
this.currentActionPathIndex = 1;
}
else if (isFeature) {
const lastFinishTestResult = this.azureTestResult[this.azureTestResult.length - 1];
const lastIteration = lastFinishTestResult.iterationDetails
? lastFinishTestResult.iterationDetails[lastFinishTestResult.iterationDetails.length - 1]
: undefined;
lastFinishTestResult.state = AzureTestRunStatus.COMPLETED;
const resultCompletedDate = lastIteration?.completedDate;
lastFinishTestResult.completedDate = resultCompletedDate;
}
}
onAfterCommand(command) {
const isScreenshot = this.utils.isScreenshotCommand(command);
if (isScreenshot && command.result?.value) {
this.screenshots.push({
testCaseId: this.currentTestCaseAzureID,
iterationId: this.currentIterationID,
actionPath: this.utils.convertIdToAzureActionPathId(this.currentActionPathIndex),
base64encodedContent: command.result?.value,
});
}
}
}