@reportportal/agent-js-playwright
Version:
Agent to integrate Playwright with ReportPortal.
489 lines • 24.5 kB
JavaScript
"use strict";
/*
* Copyright 2022 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.
*
*/
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());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RPReporter = void 0;
const client_javascript_1 = __importDefault(require("@reportportal/client-javascript"));
const helpers_1 = __importDefault(require("@reportportal/client-javascript/lib/helpers"));
const strip_ansi_1 = __importDefault(require("strip-ansi"));
const constants_1 = require("./constants");
const utils_1 = require("./utils");
const events_1 = require("@reportportal/client-javascript/lib/constants/events");
const crypto_1 = require("crypto");
class RPReporter {
constructor(config) {
this.config = Object.assign(Object.assign({ uploadTrace: true, uploadVideo: true, extendTestDescriptionWithLastError: true }, config), { launchId: process.env.RP_LAUNCH_ID || config.launchId });
this.suites = new Map();
this.suitesInfo = new Map();
this.testItems = new Map();
this.promises = [];
this.customLaunchStatus = '';
this.launchLogs = new Map();
this.nestedSteps = new Map();
const agentInfo = (0, utils_1.getAgentInfo)();
this.client = new client_javascript_1.default(this.config, agentInfo);
}
addRequestToPromisesQueue(promise, failMessage) {
this.promises.push((0, utils_1.promiseErrorHandler)(promise, failMessage));
}
onEventReport({ type, data, suiteName }, test) {
switch (type) {
case events_1.EVENTS.ADD_ATTRIBUTES:
this.addAttributes(data, test, suiteName);
break;
case events_1.EVENTS.SET_DESCRIPTION:
this.setDescription(data, test, suiteName);
break;
case events_1.EVENTS.SET_TEST_CASE_ID:
this.setTestCaseId(data, test, suiteName);
break;
case events_1.EVENTS.SET_STATUS:
this.setStatus(data, test, suiteName);
break;
case events_1.EVENTS.SET_LAUNCH_STATUS:
this.setLaunchStatus(data);
break;
case events_1.EVENTS.ADD_LOG:
this.sendTestItemLog(data, test, suiteName);
break;
case events_1.EVENTS.ADD_LAUNCH_LOG:
this.sendLaunchLog(data);
break;
}
}
onStdOut(chunk, test) {
const chunkString = String(chunk);
try {
const event = JSON.parse(chunkString);
this.onEventReport({ type: event.type, data: event.data, suiteName: event.suite }, test);
}
catch (e) {
if (test) {
this.sendTestItemLog({ message: chunkString }, test);
}
}
}
onStdErr(chunk, test) {
if (test) {
const message = String(chunk);
const level = (0, utils_1.isErrorLog)(message) ? constants_1.LOG_LEVELS.ERROR : constants_1.LOG_LEVELS.WARN;
this.sendTestItemLog({ level, message }, test);
}
}
addAttributes(attr, test, suiteName) {
if (suiteName) {
const suiteItem = this.suitesInfo.get(suiteName);
const attributes = ((suiteItem === null || suiteItem === void 0 ? void 0 : suiteItem.attributes) || []).concat(attr);
this.suitesInfo.set(suiteName, Object.assign(Object.assign({}, suiteItem), { attributes }));
}
else if (test) {
const testItem = this.testItems.get(test.id);
if (testItem) {
const attributes = (testItem.attributes || []).concat(attr);
this.testItems.set(test.id, Object.assign(Object.assign({}, testItem), { attributes }));
}
}
}
setDescription(description, test, suiteName) {
if (suiteName) {
this.suitesInfo.set(suiteName, Object.assign(Object.assign({}, this.suitesInfo.get(suiteName)), { description }));
}
else if (test) {
const testItem = this.testItems.get(test.id);
if (testItem) {
this.testItems.set(test.id, Object.assign(Object.assign({}, testItem), { description }));
}
}
}
setTestCaseId(testCaseId, test, suiteName) {
if (suiteName) {
this.suitesInfo.set(suiteName, Object.assign(Object.assign({}, this.suitesInfo.get(suiteName)), { testCaseId }));
}
else if (test) {
const testItem = this.testItems.get(test.id);
if (testItem) {
this.testItems.set(test.id, Object.assign(Object.assign({}, testItem), { testCaseId }));
}
}
}
setStatus(status, test, suiteName) {
if (suiteName) {
this.suitesInfo.set(suiteName, Object.assign(Object.assign({}, this.suitesInfo.get(suiteName)), { status }));
}
else if (test) {
const testItem = this.testItems.get(test.id);
if (testItem) {
this.testItems.set(test.id, Object.assign(Object.assign({}, testItem), { status }));
}
}
}
setLaunchStatus(status) {
this.customLaunchStatus = status;
}
sendTestItemLog(log, test, suiteName) {
if (suiteName) {
const suiteItem = this.suitesInfo.get(suiteName);
const logs = ((suiteItem === null || suiteItem === void 0 ? void 0 : suiteItem.logs) || []).concat(log);
this.suitesInfo.set(suiteName, Object.assign(Object.assign({}, suiteItem), { logs }));
}
else if (test) {
const testItem = this.testItems.get(test.id);
if (testItem) {
this.sendLog(testItem.id, log);
}
}
}
sendLaunchLog(log) {
const currentLog = this.launchLogs.get(log.message);
if (!currentLog) {
this.sendLog(this.launchId, log);
this.launchLogs.set(log.message, log);
}
}
sendLog(tempId, { level = constants_1.LOG_LEVELS.INFO, message = '', time = helpers_1.default.now(), file }) {
const { promise } = this.client.sendLog(tempId, {
message,
level,
time,
}, file);
this.addRequestToPromisesQueue(promise, 'Failed to send log');
}
finishSuites() {
const suitesToFinish = Array.from(this.suites).filter(([, { testInvocationsLeft }]) => testInvocationsLeft < 1);
suitesToFinish.forEach(([key, { id, status, logs }]) => {
if (logs) {
logs.forEach((log) => {
this.sendLog(id, log);
});
}
const finishSuiteObj = Object.assign({ endTime: helpers_1.default.now() }, (status && { status }));
const { promise } = this.client.finishTestItem(id, finishSuiteObj);
this.addRequestToPromisesQueue(promise, 'Failed to finish suite.');
this.suites.delete(key);
});
}
onBegin() {
const { launch, description, attributes, skippedIssue, rerun, rerunOf, mode, launchId } = this.config;
const systemAttributes = (0, utils_1.getSystemAttributes)(skippedIssue);
const startLaunchObj = {
name: launch,
startTime: helpers_1.default.now(),
description,
attributes: attributes && attributes.length ? attributes.concat(systemAttributes) : systemAttributes,
rerun,
rerunOf,
mode: mode || constants_1.LAUNCH_MODES.DEFAULT,
id: launchId,
};
const { tempId, promise } = this.client.startLaunch(startLaunchObj);
this.addRequestToPromisesQueue(promise, 'Failed to start launch.');
this.launchId = tempId;
}
createSuitesOrder(suite, suitesOrder) {
if (!(suite === null || suite === void 0 ? void 0 : suite.title)) {
return;
}
suitesOrder.push(suite);
this.createSuitesOrder(suite.parent, suitesOrder);
}
createSuites(test) {
var _a, _b, _c;
const orderedSuites = [];
this.createSuitesOrder(test.parent, orderedSuites);
const lastSuiteIndex = orderedSuites.length - 1;
const projectName = test.parent.project().name;
for (let i = lastSuiteIndex; i >= 0; i--) {
const currentSuite = orderedSuites[i];
const currentSuiteTitle = currentSuite.title;
const fullSuiteName = (0, utils_1.getCodeRef)(test, currentSuiteTitle);
if ((_a = this.suites.get(fullSuiteName)) === null || _a === void 0 ? void 0 : _a.id) {
continue;
}
const testItemType = i === lastSuiteIndex ? constants_1.TEST_ITEM_TYPES.SUITE : constants_1.TEST_ITEM_TYPES.TEST;
const codeRef = (0, utils_1.getCodeRef)(test, currentSuiteTitle, projectName);
const { attributes, description, testCaseId, status, logs } = this.suitesInfo.get(currentSuiteTitle) || {};
const startSuiteObj = Object.assign(Object.assign(Object.assign({ name: currentSuiteTitle, startTime: helpers_1.default.now(), type: testItemType, codeRef }, (attributes && { attributes })), (description && { description })), (testCaseId && { testCaseId }));
const parentSuiteName = (0, utils_1.getCodeRef)(test, (_b = orderedSuites[i + 1]) === null || _b === void 0 ? void 0 : _b.title);
const parentId = (_c = this.suites.get(parentSuiteName)) === null || _c === void 0 ? void 0 : _c.id;
const suiteObj = this.client.startTestItem(startSuiteObj, this.launchId, parentId);
this.addRequestToPromisesQueue(suiteObj.promise, 'Failed to start suite.');
// @ts-ignore access to private property _parallelMode
const allSuiteTests = currentSuite.allTests();
const descendants = allSuiteTests.map((testCase) => testCase.id);
const testCount = allSuiteTests.length;
let testInvocationsLeft = testCount;
// TODO: cover with tests
if (test.retries) {
const possibleTestInvocations = test.retries + 1;
testInvocationsLeft = testCount * possibleTestInvocations;
}
this.suites.set(fullSuiteName, Object.assign(Object.assign({ id: suiteObj.tempId, name: currentSuiteTitle, testInvocationsLeft,
descendants }, (status && { status })), (logs && { logs })));
this.suitesInfo.delete(currentSuiteTitle);
}
return projectName;
}
onTestBegin(test) {
var _a;
if (this.isLaunchFinishSend) {
return;
}
const playwrightProjectName = this.createSuites(test);
const fullSuiteName = (0, utils_1.getCodeRef)(test, test.parent.title);
const parentSuiteObj = this.suites.get(fullSuiteName);
// create test case
if (parentSuiteObj) {
const { includePlaywrightProjectNameToCodeReference } = this.config;
const codeRef = (0, utils_1.getCodeRef)(test, test.title, !includePlaywrightProjectNameToCodeReference && playwrightProjectName);
const { id: parentId } = parentSuiteObj;
const startTestItem = {
name: test.title,
startTime: helpers_1.default.now(),
type: constants_1.TEST_ITEM_TYPES.STEP,
codeRef,
retry: ((_a = test.results) === null || _a === void 0 ? void 0 : _a.length) > 1,
};
const stepObj = this.client.startTestItem(startTestItem, this.launchId, parentId);
this.addRequestToPromisesQueue(stepObj.promise, 'Failed to start test.');
this.testItems.set(test.id, {
name: test.title,
id: stepObj.tempId,
});
}
}
onStepBegin(test, result, step) {
if (this.isLaunchFinishSend) {
return;
}
const { includeTestSteps } = this.config;
if (!includeTestSteps)
return;
let parent;
if (step.parent) {
const stepParentName = (0, utils_1.getCodeRef)(step.parent, step.parent.title);
const fullStepParentName = `${test.id}/${stepParentName}-${step.parent.id}`;
parent = this.nestedSteps.get(fullStepParentName);
}
else {
parent = this.testItems.get(test.id);
}
if (!parent)
return;
const stepStartObj = {
name: step.title,
type: constants_1.TEST_ITEM_TYPES.STEP,
hasStats: false,
startTime: helpers_1.default.now(),
};
if (!step.hasOwnProperty('id')) {
Object.defineProperty(step, 'id', {
value: (0, crypto_1.randomUUID)(),
});
}
const stepName = (0, utils_1.getCodeRef)(step, step.title);
const fullStepName = `${test.id}/${stepName}-${step.id}`;
const { tempId, promise } = this.client.startTestItem(stepStartObj, this.launchId, parent.id);
this.addRequestToPromisesQueue(promise, 'Failed to start nested step.');
this.nestedSteps.set(fullStepName, {
name: step.title,
id: tempId,
});
}
onStepEnd(test, result, step) {
const { includeTestSteps } = this.config;
if (!includeTestSteps)
return;
const stepName = (0, utils_1.getCodeRef)(step, step.title);
const fullStepName = `${test.id}/${stepName}-${step.id}`;
const nestedStep = this.nestedSteps.get(fullStepName);
if (!nestedStep)
return;
const stepFinishObj = {
status: step.error ? constants_1.STATUSES.FAILED : constants_1.STATUSES.PASSED,
endTime: helpers_1.default.now(),
};
const { promise } = this.client.finishTestItem(nestedStep.id, stepFinishObj);
this.addRequestToPromisesQueue(promise, 'Failed to finish nested step.');
this.nestedSteps.delete(fullStepName);
}
processAnnotations({ annotations, test }) {
annotations.forEach(({ type, description }) => {
if (type && description && Object.values(events_1.EVENTS).includes(type)) {
try {
const data = (0, utils_1.safeParse)(description);
const reportData = {
type,
data,
};
this.onEventReport(reportData, test);
}
catch (error) {
console.warn(`[ReportPortal] Skipping annotation with type "${type}" as description is not valid JSON: "${description}". ` +
`Only JSON-formatted annotation descriptions are supported for ReportPortal event processing.`);
}
}
});
}
onTestEnd(test, result) {
var _a;
return __awaiter(this, void 0, void 0, function* () {
this.processAnnotations({ annotations: test.annotations, test });
const savedTestItem = this.testItems.get(test.id);
if (!savedTestItem) {
return Promise.resolve();
}
const { id: testItemId, attributes, description, testCaseId, status: predefinedStatus, } = savedTestItem;
let withoutIssue;
let testDescription = description;
const calculatedStatus = (0, utils_1.calculateRpStatus)(test.outcome(), result.status, test.annotations);
const status = predefinedStatus || calculatedStatus;
if (status === constants_1.STATUSES.SKIPPED) {
withoutIssue = (0, utils_1.isFalse)(this.config.skippedIssue);
}
// TODO: cover with tests
if ((_a = result.attachments) === null || _a === void 0 ? void 0 : _a.length) {
const { uploadVideo, uploadTrace } = this.config;
const attachmentsFiles = yield (0, utils_1.getAttachments)(result.attachments, {
uploadVideo,
uploadTrace,
}, test.title);
// TODO: use bulk log request
attachmentsFiles.forEach((file) => {
this.sendLog(testItemId, {
message: `Attachment ${file.name} with type ${file.type}`,
file,
});
});
}
if (result.error) {
const stacktrace = (0, strip_ansi_1.default)(result.error.stack || result.error.message);
this.sendLog(testItemId, {
level: constants_1.LOG_LEVELS.ERROR,
message: stacktrace,
});
if (this.config.extendTestDescriptionWithLastError) {
testDescription = (description || '').concat(`\n\`\`\`error\n${stacktrace}\n\`\`\``);
}
}
[...this.nestedSteps.entries()].forEach(([key, value]) => {
if (key.includes(test.id)) {
const { id: stepId } = value;
const itemObject = {
status: result.status === 'timedOut' ? constants_1.STATUSES.INTERRUPTED : constants_1.STATUSES.FAILED,
endTime: helpers_1.default.now(),
};
const { promise } = this.client.finishTestItem(stepId, itemObject);
this.addRequestToPromisesQueue(promise, 'Failed to finish nested step.');
this.nestedSteps.delete(key);
}
});
const finishTestItemObj = Object.assign(Object.assign(Object.assign(Object.assign({ endTime: helpers_1.default.now(), status }, (withoutIssue && { issue: { issueType: 'NOT_ISSUE' } })), (attributes && { attributes })), (testDescription && { description: testDescription })), (testCaseId && { testCaseId }));
const { promise } = this.client.finishTestItem(testItemId, finishTestItemObj);
this.addRequestToPromisesQueue(promise, 'Failed to finish test.');
this.testItems.delete(test.id);
this.updateAncestorsTestInvocations(test, result);
const fullParentName = (0, utils_1.getCodeRef)(test, test.parent.title);
const parentSuite = this.suites.get(fullParentName);
// if all children of the test parent have already finished, then finish all empty ancestors
if (parentSuite &&
'testInvocationsLeft' in parentSuite &&
parentSuite.testInvocationsLeft < 1) {
this.finishSuites();
}
});
}
// TODO: cover with tests
updateAncestorsTestInvocations(test, result) {
// Decrease by 1 by default as only one test case finished
let decreaseIndex = 1;
const isTestFinishedFromHookOrStaticAnnotation = result.workerIndex === -1;
const testOutcome = test.outcome();
const isTestHasStaticAnnotations =
// @ts-ignore access to private property _staticAnnotations
test._staticAnnotations && Array.isArray(test._staticAnnotations);
const isStaticallyAnnotatedWithSkippedAnnotation = isTestHasStaticAnnotations
? // @ts-ignore access to private property _staticAnnotations
test._staticAnnotations.some((annotation) => annotation.type === constants_1.TEST_ANNOTATION_TYPES.SKIP ||
annotation.type === constants_1.TEST_ANNOTATION_TYPES.FIXME)
: false;
// TODO: post an issue on GitHub for playwright/test to provide clear output for this purpose
const isFinishedFromHook = isTestFinishedFromHookOrStaticAnnotation && !isStaticallyAnnotatedWithSkippedAnnotation; // In case test finished by hook error it will be retried.
const nonRetriedResult = testOutcome === constants_1.TEST_OUTCOME_TYPES.EXPECTED ||
testOutcome === constants_1.TEST_OUTCOME_TYPES.FLAKY ||
// This check broke `decreaseIndex` calculation for tests with .skip()/.fixme() static annotations and enabled retries after error from hook,
// but helps to calculate `decreaseIndex`correctly in other cases.
// Additional info required from Playwright to correctly determine failure from hook.
(testOutcome === constants_1.TEST_OUTCOME_TYPES.SKIPPED && !isFinishedFromHook);
// if test case has retries, and it will not be retried anymore
if (test.retries > 0 && nonRetriedResult) {
const possibleInvocations = test.retries + 1;
const possibleInvocationsLeft = possibleInvocations - test.results.length;
// we need to decrease also all the rest possible invocations as the test case will not be retried anymore
decreaseIndex = decreaseIndex + possibleInvocationsLeft;
}
// @ts-ignore access to private property _parallelMode
const isSerialMode = test.parent._parallelMode === 'serial'; // is test run with "serial" mode
this.suites.forEach((value, key) => {
const { descendants, testInvocationsLeft, executedTestCount } = value;
if (descendants.length && descendants.includes(test.id)) {
// if test will not be retried anymore, we consider it as finally executed
const newExecutedTestCount = executedTestCount + (nonRetriedResult ? 1 : 0);
/* In case one test from serial group will fail, all tests from this group will be retried,
so we need to increase _testInvocationsLeft_ of all its ancestors by already finished test amount, see https://playwright.dev/docs/test-retries#serial-mode
*/
const serialModeIncrement = isSerialMode ? executedTestCount : 0;
const newTestInvocationsLeft = testInvocationsLeft - decreaseIndex + serialModeIncrement;
this.suites.set(key, Object.assign(Object.assign({}, value), { executedTestCount: newExecutedTestCount, testInvocationsLeft: newTestInvocationsLeft }));
}
});
}
onEnd() {
return __awaiter(this, void 0, void 0, function* () {
// Force finish unfinished suites in case of interruptions
if (this.suites.size > 0) {
this.suites.forEach((value, key) => {
this.suites.set(key, Object.assign(Object.assign({}, value), { testInvocationsLeft: 0, descendants: [] }));
});
this.finishSuites();
}
if (!this.config.launchId) {
const { promise } = this.client.finishLaunch(this.launchId, Object.assign({ endTime: helpers_1.default.now() }, (this.customLaunchStatus && { status: this.customLaunchStatus })));
this.addRequestToPromisesQueue(promise, 'Failed to finish launch.');
}
this.isLaunchFinishSend = true;
yield Promise.all(this.promises);
this.launchId = null;
});
}
printsToStdio() {
return false;
}
}
exports.RPReporter = RPReporter;
//# sourceMappingURL=reporter.js.map