@reportportal/agent-js-jest
Version:
A Jest reporter that uploads test results to ReportPortal
271 lines (225 loc) • 8.22 kB
JavaScript
/*
* Copyright 2025 EPAM Systems
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const stripAnsi = require('strip-ansi');
const RPClient = require('@reportportal/client-javascript');
const getOptions = require('./utils/getOptions');
const ReportingApi = require('./reportingApi');
const {
getAgentOptions,
getStartLaunchObject,
getAgentInfo,
getCodeRef,
getFullStepName,
} = require('./utils/objectUtils');
const { TEST_ITEM_STATUSES, LOG_LEVEL, TEST_ITEM_TYPES } = require('./constants');
const promiseErrorHandler = (promise) => {
promise.catch((err) => {
console.error(err);
});
};
class JestReportPortal {
constructor(globalConfig, options) {
const agentInfo = getAgentInfo();
this.reportOptions = getAgentOptions(getOptions.options(options));
this.client = new RPClient(this.reportOptions, agentInfo);
this.tempSuiteIds = new Map();
this.tempStepIds = new Map();
// TODO: Remove and use `this.tempStepIds` instead.
this.tempStepId = null;
this.promises = [];
global.ReportingApi = new ReportingApi(this);
}
onRunStart() {
const startLaunchObj = getStartLaunchObject(this.reportOptions);
const { tempId, promise } = this.client.startLaunch(startLaunchObj);
this.tempLaunchId = tempId;
promiseErrorHandler(promise);
this.promises.push(promise);
}
// FYI. In most cases it is not even called. It cannot be used for suites handling.
onTestStart() {}
_startSuites(suiteTitles, filePath, startTime) {
suiteTitles.reduce((suitePath, suiteTitle) => {
const fullSuiteName = suitePath ? `${suitePath}/${suiteTitle}` : suiteTitle;
const codeRef = getCodeRef(filePath, fullSuiteName);
const parentCodeRef = getCodeRef(filePath, suitePath);
this._startSuite(suiteTitle, codeRef, parentCodeRef, startTime);
return fullSuiteName;
}, '');
}
// TODO: cover with tests
// Not called for `skipped` and `todo` specs
onTestCaseStart(test, testCaseStartInfo) {
this._startSuites(testCaseStartInfo.ancestorTitles, test.path, testCaseStartInfo.startedAt);
this._startStep(testCaseStartInfo, test.path);
}
// TODO: cover with tests
// Not called for `skipped` and `todo` specs
onTestCaseResult(test, testCaseStartInfo) {
this._finishStep(testCaseStartInfo, test.path);
}
onTestResult(test, testResult) {
// Handling `skipped` tests and their ancestors
testResult.testResults.forEach((testCaseInfo) => {
if (testCaseInfo.status === TEST_ITEM_STATUSES.SKIPPED) {
const testCaseWithStartTime = { startedAt: new Date().valueOf(), ...testCaseInfo };
this._startSuites(testCaseInfo.ancestorTitles, test.path, testCaseWithStartTime.startedAt);
this._startStep(testCaseWithStartTime, test.path);
this._finishStep(testCaseWithStartTime, test.path);
}
});
const suiteFilePathToFinish = getCodeRef(testResult.testFilePath);
// Finishing suites that are related to the test file
this.tempSuiteIds.forEach((suiteTempId, suiteFullName) => {
if (suiteFullName.includes(suiteFilePathToFinish)) {
this._finishSuite(suiteTempId, suiteFullName);
}
});
}
async onRunComplete() {
await Promise.all(this.promises);
if (this.reportOptions.launchId) {
return;
}
const { promise } = this.client.finishLaunch(this.tempLaunchId);
promiseErrorHandler(promise);
await promise;
}
_startSuite(title, codeRef, parentCodeRef = '', startTime = new Date().valueOf()) {
if (this.tempSuiteIds.get(codeRef)) {
return;
}
const testStartObj = {
type: TEST_ITEM_TYPES.SUITE,
name: title,
codeRef,
startTime,
};
const parentId = this.tempSuiteIds.get(parentCodeRef);
const { tempId, promise } = this.client.startTestItem(
testStartObj,
this.tempLaunchId,
parentId,
);
this.tempSuiteIds.set(codeRef, tempId);
promiseErrorHandler(promise);
this.promises.push(promise);
}
_startStep(test, testPath) {
const fullStepName = getFullStepName(test);
const codeRef = getCodeRef(testPath, fullStepName);
const retryIds = this.tempStepIds.get(codeRef);
const isRetried = !!retryIds;
const stepStartObj = {
type: TEST_ITEM_TYPES.STEP,
name: test.title,
codeRef,
startTime: test.startedAt,
retry: isRetried,
};
const parentFullName = test.ancestorTitles.join('/');
const parentCodeRef = getCodeRef(testPath, parentFullName);
const parentId = this.tempSuiteIds.get(parentCodeRef);
const { tempId, promise } = this.client.startTestItem(
stepStartObj,
this.tempLaunchId,
parentId,
);
// store item ids as array to not overwrite retry ids
let tempIdToStore = [tempId];
if (isRetried) {
tempIdToStore = retryIds.concat(tempIdToStore);
}
this.tempStepIds.set(codeRef, tempIdToStore);
this.tempStepId = tempId;
promiseErrorHandler(promise);
this.promises.push(promise);
}
_finishStep(test, testPath) {
const fullName = getFullStepName(test);
const codeRef = getCodeRef(testPath, fullName);
const tempStepIds = this.tempStepIds.get(codeRef);
const tempStepId = Array.isArray(tempStepIds) ? tempStepIds.shift() : undefined;
if (!tempStepId) {
console.error(`Could not finish Test Step - "${codeRef}". tempId not found`);
return;
}
const errorMsg = test.failureMessages[0];
switch (test.status) {
case TEST_ITEM_STATUSES.PASSED:
this._finishPassedStep(tempStepId);
break;
case TEST_ITEM_STATUSES.FAILED:
this._finishFailedStep(tempStepId, errorMsg);
break;
default:
this._finishSkippedStep(tempStepId);
}
}
_finishPassedStep(tempStepId) {
const status = TEST_ITEM_STATUSES.PASSED;
const { promise } = this.client.finishTestItem(tempStepId, { status });
promiseErrorHandler(promise);
this.promises.push(promise);
}
_finishFailedStep(tempStepId, failureMessage) {
const status = TEST_ITEM_STATUSES.FAILED;
const description =
this.reportOptions.extendTestDescriptionWithLastError === false
? null
: `\`\`\`error\n${stripAnsi(failureMessage)}\n\`\`\``;
const finishTestObj = { status, ...(description && { description }) };
this._sendLog({ message: failureMessage, level: LOG_LEVEL.ERROR, tempStepId });
const { promise } = this.client.finishTestItem(tempStepId, finishTestObj);
promiseErrorHandler(promise);
this.promises.push(promise);
}
_sendLog({ level = LOG_LEVEL.INFO, message = '', file, time, tempStepId }) {
const newMessage = stripAnsi(message);
const { promise } = this.client.sendLog(
tempStepId === undefined ? this.tempStepId : tempStepId,
{
message: newMessage,
level,
time: time || this.client.helpers.now(),
},
file,
);
promiseErrorHandler(promise);
this.promises.push(promise);
}
_finishSkippedStep(tempStepId) {
const status = 'skipped';
const issue = this.reportOptions.skippedIssue === false ? { issueType: 'NOT_ISSUE' } : null;
const finishTestObj = {
status,
...(issue && { issue }),
};
const { promise } = this.client.finishTestItem(tempStepId, finishTestObj);
promiseErrorHandler(promise);
this.promises.push(promise);
}
_finishSuite(tempTestId, key) {
if (!tempTestId) {
return;
}
const { promise } = this.client.finishTestItem(tempTestId, {});
this.tempSuiteIds.delete(key);
promiseErrorHandler(promise);
this.promises.push(promise);
}
}
module.exports = JestReportPortal;