UNPKG

@reportportal/agent-js-playwright

Version:
489 lines 24.5 kB
"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