wdio-reportportal-reporter
Version:
A WebdriverIO v6 plugin. Report results to Report Portal.
445 lines (444 loc) • 20.6 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const logger_1 = require("@wdio/logger");
const reporter_1 = require("@wdio/reporter");
const crypto_1 = require("crypto");
const path = require("path");
const ReportPortalClient = require("reportportal-js-client");
const constants_1 = require("./constants");
const entities_1 = require("./entities");
const ReporterOptions_1 = require("./ReporterOptions");
const storage_1 = require("./storage");
const utils_1 = require("./utils");
const log = (0, logger_1.default)("wdio-reportportal-reporter");
class ReportPortalReporter extends reporter_1.default {
constructor(options) {
super(options);
this.storage = new storage_1.Storage();
this.rpPromisesCompleted = true;
this.currentTestAttributes = [];
this.currentSuiteAttributes = [];
this.currentSuiteDescription = [];
this.suitesDescription = [];
this.reporterOptions = Object.assign(new ReporterOptions_1.default(), options);
this.registerListeners();
if (this.reporterOptions.cucumberNestedSteps) {
this.featureStatus = constants_1.STATUS.PASSED;
}
}
get isSynchronised() {
return this.rpPromisesCompleted;
}
set isSynchronised(value) {
this.rpPromisesCompleted = value;
}
static sendLog(level, message) {
(0, utils_1.sendToReporter)(constants_1.EVENTS.RP_LOG, { level, message });
}
static sendFile(level, name, content, type = "image/png", message = "") {
(0, utils_1.sendToReporter)(constants_1.EVENTS.RP_FILE, { level, name, content, type, message });
}
static sendLogToTest(test, level, message) {
(0, utils_1.sendToReporter)(constants_1.EVENTS.RP_TEST_LOG, { test, level, message });
}
static sendFileToTest(test, level, name, content, type = "image/png", message = "") {
(0, utils_1.sendToReporter)(constants_1.EVENTS.RP_TEST_FILE, { test, level, name, content, type, message });
}
static finishTestManually(test) {
(0, utils_1.sendToReporter)(constants_1.EVENTS.RP_TEST_RETRY, { test });
}
static addDescriptionToCurrentSuite(description) {
(0, utils_1.sendToReporter)(constants_1.EVENTS.RP_SUITE_ADD_DESCRIPTION, description);
}
static addDescriptionToAllSuites(description) {
(0, utils_1.sendToReporter)(constants_1.EVENTS.RP_ALL_SUITE_ADD_DESCRIPTION, description);
}
static getValidatedAttribute(attribute) {
if (!attribute) {
throw new Error("Attribute should be an object");
}
const clonedAttribute = Object.assign({}, attribute);
if (clonedAttribute.value) {
clonedAttribute.value = String(clonedAttribute.value);
if (clonedAttribute.value.trim().length === 0) {
throw Error("Attribute value should not be an empty string");
}
}
else {
throw new Error("Invalid attribute: " + JSON.stringify(attribute));
}
if (clonedAttribute.key) {
clonedAttribute.key = String(clonedAttribute.key);
if (clonedAttribute.key.trim().length === 0) {
throw Error("Attribute key should not be an empty string");
}
}
return clonedAttribute;
}
static addAttribute(attribute) {
(0, utils_1.sendToReporter)(constants_1.EVENTS.RP_TEST_ATTRIBUTES, Object.assign({}, this.getValidatedAttribute(attribute)));
}
static addAttributeToSuite(attribute) {
(0, utils_1.sendToReporter)(constants_1.EVENTS.RP_SUITE_ATTRIBUTES, Object.assign({}, this.getValidatedAttribute(attribute)));
}
onSuiteStart(suite) {
log.debug(`Start suite ${suite.title} ${suite.uid}`);
this.specFilePath = suite.file;
const isCucumberFeature = suite.type === constants_1.CUCUMBER_TYPE.FEATURE;
const isCucumberScenario = suite.type === constants_1.CUCUMBER_TYPE.SCENARIO;
const suiteStartObj = this.reporterOptions.cucumberNestedSteps ?
new entities_1.StartTestItem(suite.title, isCucumberFeature ? constants_1.TYPE.TEST : constants_1.TYPE.STEP) :
new entities_1.StartTestItem(suite.title, constants_1.TYPE.SUITE);
if (isCucumberFeature) {
(0, utils_1.addSauceLabAttributes)(this.reporterOptions, suiteStartObj, this.sessionId);
}
if (isCucumberScenario) {
suiteStartObj.codeRef = (0, utils_1.getRelativePath)(this.specFilePath) + ':' + suite.uid.replace(suite.title, '').trim();
}
if (this.reporterOptions.cucumberNestedSteps && this.reporterOptions.autoAttachCucumberFeatureToScenario) {
switch (suite.type) {
case constants_1.CUCUMBER_TYPE.FEATURE:
this.featureName = suite.title;
break;
case constants_1.CUCUMBER_TYPE.SCENARIO:
suiteStartObj.attributes = [
{
key: constants_1.CUCUMBER_TYPE.FEATURE,
value: this.featureName,
},
];
if (this.reporterOptions.setRetryTrue) {
suiteStartObj.retry = true;
}
break;
}
}
const suiteItem = this.storage.getCurrentSuite();
let parentId = null;
if (suiteItem !== null) {
parentId = suiteItem.id;
}
if (this.reporterOptions.parseTagsFromTestTitle) {
suiteStartObj.addTags();
}
suiteStartObj.description = [...this.suitesDescription, ...this.currentSuiteDescription].join('\n');
const { tempId, promise } = this.client.startTestItem(suiteStartObj, this.tempLaunchId, parentId);
(0, utils_1.promiseErrorHandler)(promise);
this.storage.addSuite(new entities_1.StorageEntity(suiteStartObj.type, tempId, promise, suite));
}
onSuiteEnd(suite) {
log.debug(`End suite ${suite.title} ${suite.uid}`);
const isSomeTestFailed = suite.tests.some(({ state }) => state === constants_1.WDIO_TEST_STATUS.FAILED);
let suiteStatus = isSomeTestFailed ? constants_1.STATUS.FAILED : constants_1.STATUS.PASSED;
if (this.reporterOptions.cucumberNestedSteps) {
switch (suite.type) {
case constants_1.CUCUMBER_TYPE.SCENARIO:
const scenarioStepsAllPassed = suite.tests.every(({ state }) => state === constants_1.WDIO_TEST_STATUS.PASSED);
const scenarioStepsSkipped = isSomeTestFailed ? false : suite.tests.some(({ state }) => state === constants_1.WDIO_TEST_STATUS.SKIPPED);
suiteStatus = scenarioStepsAllPassed ? constants_1.STATUS.PASSED : scenarioStepsSkipped ? constants_1.STATUS.SKIPPED : constants_1.STATUS.FAILED;
this.featureStatus = this.featureStatus === constants_1.STATUS.PASSED && suiteStatus === constants_1.STATUS.PASSED ? constants_1.STATUS.PASSED : constants_1.STATUS.FAILED;
break;
case constants_1.CUCUMBER_TYPE.FEATURE:
suiteStatus = this.featureStatus;
break;
}
}
const suiteItem = this.storage.getCurrentSuite();
const finishSuiteObj = suiteStatus === constants_1.STATUS.SKIPPED ? new entities_1.EndTestItem(constants_1.STATUS.SKIPPED, new entities_1.Issue("NOT_ISSUE")) : { status: suiteStatus, attributes: this.currentSuiteAttributes, description: [...this.suitesDescription, ...this.currentSuiteDescription].join('\n') };
const { promise } = this.client.finishTestItem(suiteItem.id, finishSuiteObj);
(0, utils_1.promiseErrorHandler)(promise);
this.currentSuiteDescription = [];
this.storage.removeSuite();
}
onTestStart(test, type = constants_1.TYPE.STEP) {
log.debug(`Start test ${test.title} ${test.uid}`);
if (this.storage.getCurrentTest()) {
return;
}
const suite = this.storage.getCurrentSuite();
const testStartObj = new entities_1.StartTestItem(test.title, type);
if (this.isCucumberFramework) {
(0, utils_1.addCodeRefCucumber)(this.specFilePath, test, testStartObj);
}
else {
(0, utils_1.addCodeRef)(this.specFilePath, test.fullTitle, testStartObj);
}
if (this.reporterOptions.cucumberNestedSteps) {
testStartObj.hasStats = false;
}
else {
(0, utils_1.addSauceLabAttributes)(this.reporterOptions, testStartObj, this.sessionId);
}
if (this.reporterOptions.parseTagsFromTestTitle) {
testStartObj.addTags();
}
if (this.reporterOptions.setRetryTrue) {
testStartObj.retry = true;
}
(0, utils_1.addBrowserParam)(this.sanitizedCapabilities, testStartObj);
const { tempId, promise } = this.client.startTestItem(testStartObj, this.tempLaunchId, suite.id);
(0, utils_1.promiseErrorHandler)(promise);
this.storage.addTest(test.uid, new entities_1.StorageEntity(testStartObj.type, tempId, promise, test));
return promise;
}
onTestPass(test) {
log.debug(`Pass test ${test.title} ${test.uid}`);
this.testFinished(test, constants_1.STATUS.PASSED);
}
onTestFail(test) {
log.debug(`Fail test ${test.title} ${test.uid} ${test.error.stack}`);
const testItem = this.storage.getCurrentTest();
if (testItem === null) {
this.onTestStart(test, constants_1.TYPE.BEFORE_METHOD);
}
this.testFinished(test, constants_1.STATUS.FAILED);
}
onTestSkip(test) {
log.debug(`Skip test ${test.title} ${test.uid}`);
const testItem = this.storage.getCurrentTest();
if (testItem === null) {
this.onTestStart(test);
}
this.testFinished(test, constants_1.STATUS.SKIPPED, new entities_1.Issue("NOT_ISSUE"));
}
testFinished(test, status, issue) {
log.debug(`Finish test ${test.title} ${test.uid}`);
const testItem = this.storage.getCurrentTest();
if (testItem === null) {
return;
}
const finishTestObj = new entities_1.EndTestItem(status, issue);
if (status === constants_1.STATUS.FAILED) {
const stacktrace = test.error.stack;
const message = this.reporterOptions.sanitizeErrorMessages ?
stacktrace.replace((0, utils_1.ansiRegex)(), '') : `${stacktrace}`;
finishTestObj.description = `❌ ${message}`;
this.client.sendLog(testItem.id, {
level: constants_1.LEVEL.ERROR,
message,
});
}
finishTestObj.attributes = [...this.currentTestAttributes];
const { promise } = this.client.finishTestItem(testItem.id, finishTestObj);
(0, utils_1.promiseErrorHandler)(promise);
this.storage.removeTest(testItem);
this.currentTestAttributes = [];
}
onRunnerStart(runner) {
log.debug(`Runner start`);
this.rpPromisesCompleted = false;
this.isMultiremote = runner.isMultiremote;
this.sanitizedCapabilities = runner.sanitizedCapabilities;
this.sessionId = runner.sessionId;
this.isCucumberFramework = runner.config.framework === 'cucumber';
this.client = this.getReportPortalClient();
this.launchId = process.env.RP_LAUNCH_ID;
const startLaunchObj = {
attributes: this.reporterOptions.reportPortalClientConfig.attributes,
description: this.reporterOptions.reportPortalClientConfig.description,
id: this.launchId,
mode: this.reporterOptions.reportPortalClientConfig.mode,
rerun: false,
rerunOf: null,
};
if (runner.retry > 0) {
delete startLaunchObj.id;
startLaunchObj.rerun = true;
startLaunchObj.rerunOf = this.launchId;
const result = this.client.startLaunch(startLaunchObj);
this.tempLaunchId = result.tempId;
}
else {
const { tempId } = this.client.startLaunch(startLaunchObj);
this.tempLaunchId = tempId;
}
}
onRunnerEnd(runnerStats) {
return __awaiter(this, void 0, void 0, function* () {
log.debug(`Runner end`);
try {
return yield this.client.getPromiseFinishAllItems(this.tempLaunchId);
}
catch (e) {
log.error("An error occurs on finish test items");
log.error(e);
}
finally {
this.isSynchronised = true;
log.debug(`Runner end sync`);
}
});
}
onBeforeCommand(command) {
if (!this.reporterOptions.reportSeleniumCommands || this.isMultiremote) {
return;
}
const method = `${command.method} ${command.endpoint}`;
if (!(0, utils_1.isEmpty)(command.body)) {
const data = JSON.stringify((0, utils_1.limit)(command.body));
this.sendLog({ message: `${method} ${data}`, level: this.reporterOptions.seleniumCommandsLogLevel });
}
else {
this.sendLog({ message: `${method}`, level: this.reporterOptions.seleniumCommandsLogLevel });
}
}
onAfterCommand(command) {
if (this.isMultiremote) {
return;
}
const isScreenshot = (0, utils_1.isScreenshotCommand)(command) && command.result.value;
const { autoAttachScreenshots, screenshotsLogLevel, seleniumCommandsLogLevel, reportSeleniumCommands } = this.reporterOptions;
if (isScreenshot) {
if (autoAttachScreenshots) {
const obj = {
content: command.result.value,
level: screenshotsLogLevel,
name: "screenshot.png",
message: "screenshot"
};
this.sendFile(obj);
}
}
if (reportSeleniumCommands) {
if (command.body && !(0, utils_1.isEmpty)(command.result.value)) {
delete command.result.sessionId;
const data = JSON.stringify((0, utils_1.limit)(command.result));
this.sendLog({ message: `${data}`, level: seleniumCommandsLogLevel });
}
}
}
onHookStart(hook) {
log.debug(`Start hook ${hook.title} ${hook.uid}`);
}
onHookEnd(hook) {
log.debug(`End hook ${hook.title} ${hook.uid} ${JSON.stringify(hook)}`);
if (hook.error) {
const testItem = this.storage.getCurrentTest();
if (testItem === null) {
this.onTestStart(hook, constants_1.TYPE.BEFORE_METHOD);
}
this.testFinished(hook, constants_1.STATUS.FAILED);
}
}
getReportPortalClient() {
return new ReportPortalClient(this.reporterOptions.reportPortalClientConfig);
}
addDescriptionToCurrentSuite(description) {
this.currentSuiteDescription.push(description);
}
addDescriptionToAllSuites(description) {
this.suitesDescription.push(description);
}
addAttribute(attribute) {
this.currentTestAttributes.push(Object.assign({}, attribute));
}
addAttributeToSuite(attribute) {
this.currentSuiteAttributes.push(attribute);
}
finishTestManually(event) {
const testItem = this.storage.getCurrentTest();
if (testItem === null) {
return;
}
const err = {
stack: event.test.error,
};
const test = {
error: err,
title: testItem.wdioEntity.title,
uid: testItem.wdioEntity.uid,
};
this.testFinished(test, constants_1.STATUS.FAILED);
}
sendLog(event) {
const testItem = this.storage.getCurrentTest();
if (testItem === null) {
log.warn("Cannot send log message. There is no running tests");
return;
}
const { promise } = this.client.sendLog(testItem.id, {
level: event.level,
message: String(event.message),
});
(0, utils_1.promiseErrorHandler)(promise);
}
sendFile({ level, name, content, type = "image/png", message = "" }) {
const testItem = this.storage.getCurrentTest();
if (!testItem) {
log.warn(`Can not send file to test. There is no running tests`);
return;
}
const { promise } = this.client.sendLog(testItem.id, { level, message }, { name, content, type, message });
(0, utils_1.promiseErrorHandler)(promise);
}
sendLogToTest({ test, level, message }) {
return __awaiter(this, void 0, void 0, function* () {
const testObj = this.storage.getStartedTests().reverse().find((startedTest) => {
return startedTest.wdioEntity.title === test.title;
});
if (!testObj) {
log.warn(`Can not send log to test ${test.title}`);
return;
}
const rs = yield testObj.promise;
const saveLogRQ = {
itemUuid: rs.id,
launchUuid: this.launchId,
level,
message,
time: this.now(),
};
const url = [this.client.baseURL, "log"].join("/");
const promise = this.client.helpers.getServerResult(url, saveLogRQ, { headers: this.client.headers }, "POST");
(0, utils_1.promiseErrorHandler)(promise);
});
}
sendFileToTest({ test, level, name, content, type = "image/png", message = "" }) {
return __awaiter(this, void 0, void 0, function* () {
const testObj = this.storage.getStartedTests().reverse().find((startedTest) => {
return startedTest.wdioEntity.title === test.title;
});
if (!testObj) {
log.warn(`Can not send file to test ${test.title}`);
return;
}
const rs = yield testObj.promise;
const saveLogRQ = {
itemUuid: rs.id,
launchUuid: this.launchId,
level,
message,
time: this.now(),
};
// to avoid https://github.com/BorisOsipov/wdio-reportportal-reporter/issues/42#issuecomment-456573592
const fileName = (0, crypto_1.createHash)("md5").update(name).digest("hex");
const extension = path.extname(name) || ".dat";
const promise = this.client.getRequestLogWithFile(saveLogRQ, { name: `${fileName}${extension}`, content, type });
(0, utils_1.promiseErrorHandler)(promise);
});
}
registerListeners() {
process.on(constants_1.EVENTS.RP_LOG, this.sendLog.bind(this));
process.on(constants_1.EVENTS.RP_FILE, this.sendFile.bind(this));
process.on(constants_1.EVENTS.RP_TEST_LOG, this.sendLogToTest.bind(this));
process.on(constants_1.EVENTS.RP_TEST_FILE, this.sendFileToTest.bind(this));
process.on(constants_1.EVENTS.RP_TEST_RETRY, this.finishTestManually.bind(this));
process.on(constants_1.EVENTS.RP_TEST_ATTRIBUTES, this.addAttribute.bind(this));
process.on(constants_1.EVENTS.RP_SUITE_ATTRIBUTES, this.addAttributeToSuite.bind(this));
process.on(constants_1.EVENTS.RP_SUITE_ADD_DESCRIPTION, this.addDescriptionToCurrentSuite.bind(this));
process.on(constants_1.EVENTS.RP_ALL_SUITE_ADD_DESCRIPTION, this.addDescriptionToAllSuites.bind(this));
}
now() {
return this.client.helpers.now();
}
}
ReportPortalReporter.reporterName = "reportportal";
module.exports = ReportPortalReporter;