diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3e13d..00173fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Added - Add `@cucumber/cucumber` version 10 support. Addressed [155](https://github.com/reportportal/agent-js-cucumber/issues/155). ### Changed +- **Breaking change** Drop support of cucumber <7. Addressed [153](https://github.com/reportportal/agent-js-cucumber/issues/153). - **Breaking change** Drop support of Node.js 10. The version [5.2.3](https://github.com/reportportal/agent-js-cucumber/releases/tag/v5.2.3) is the latest that supports it. - `@reportportal/client-javascript` bumped to version `5.1.0`. diff --git a/README.md b/README.md index 205e574..c7a8931 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ Agent to integrate CucumberJS with ReportPortal. * More about [ReportPortal](http://reportportal.io/) This agent works well with cucumber versions from 7.x to 10.x. -Documentation for legacy cucumber versions from 4.x to 6.x can be found [here](/modules/api/deprecated/README.md) ## Install agent to your project dir diff --git a/jest.config.js b/jest.config.js index 256824a..c2f3506 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,7 +1,7 @@ module.exports = { moduleFileExtensions: ['js'], testRegex: '/tests/.*\\.spec.(js)$', - collectCoverageFrom: ['modules/**.js', '!api/deprecated'], + collectCoverageFrom: ['modules/**.js'], coverageThreshold: { global: { branches: 80, diff --git a/modules/api/current.js b/modules/api/current.js deleted file mode 100644 index b4a41b0..0000000 --- a/modules/api/current.js +++ /dev/null @@ -1,565 +0,0 @@ -/* - * 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. - */ - -const stripAnsi = require('strip-ansi'); -const pjson = require('../../package.json'); -const utils = require('../utils'); -const { - RP_EVENTS, - RP_ENTITY_LAUNCH, - LOG_LEVELS, - STATUSES, - CUCUMBER_MESSAGES, - TEST_ITEM_TYPES, -} = require('../constants'); -const Storage = require('../storage'); - -module.exports = { - init() { - this.storage = new Storage(); - this.customLaunchStatus = null; - this.codeRefIndexesMap = new Map(); - - this.options.eventBroadcaster.on('envelope', (event) => { - const [key] = Object.keys(event); - switch (key) { - case CUCUMBER_MESSAGES.GHERKIN_DOCUMENT: - return this.onGherkinDocumentEvent(event[key]); - case CUCUMBER_MESSAGES.PICKLE: - return this.onPickleEvent(event[key]); - case CUCUMBER_MESSAGES.HOOK: - return this.onHookEvent(event[key]); - case CUCUMBER_MESSAGES.TEST_RUN_STARTED: - return this.onTestRunStartedEvent(); - case CUCUMBER_MESSAGES.TEST_CASE: - return this.onTestCaseEvent(event[key]); - case CUCUMBER_MESSAGES.TEST_CASE_STARTED: - return this.onTestCaseStartedEvent(event[key]); - case CUCUMBER_MESSAGES.TEST_STEP_STARTED: - return this.onTestStepStartedEvent(event[key]); - case CUCUMBER_MESSAGES.ATTACHMENT: - return this.onTestStepAttachmentEvent(event[key]); - case CUCUMBER_MESSAGES.TEST_STEP_FINISHED: - return this.onTestStepFinishedEvent(event[key]); - case CUCUMBER_MESSAGES.TEST_CASE_FINISHED: - return this.onTestCaseFinishedEvent(event[key]); - case CUCUMBER_MESSAGES.TEST_RUN_FINISHED: - return this.onTestRunFinishedEvent(event[key]); - default: - return null; - } - }); - }, - onGherkinDocumentEvent(data) { - this.storage.setDocument(data); - this.storage.setAstNodesData(data, utils.findAstNodesData(data.feature.children)); - }, - onHookEvent(data) { - const { id } = data; - this.storage.setHook(id, data); - }, - onPickleEvent(data) { - this.storage.setPickle(data); - }, - onTestRunStartedEvent() { - const attributes = [ - ...(this.config.attributes || []), - { key: 'agent', value: `${pjson.name}|${pjson.version}`, system: true }, - ]; - if (this.config.skippedIssue === false) { - const skippedIssueAttribute = { key: 'skippedIssue', value: 'false', system: true }; - attributes.push(skippedIssueAttribute); - } - const startLaunchData = { - name: this.config.launch, - startTime: this.reportportal.helpers.now(), - description: this.config.description || '', - attributes, - rerun: this.isRerun, - rerunOf: this.rerunOf, - ...(this.config.mode && { mode: this.config.mode }), - }; - const { tempId } = this.reportportal.startLaunch(startLaunchData); - this.storage.setLaunchTempId(tempId); - }, - onTestCaseEvent(data) { - const { id: testCaseId, pickleId, testSteps } = data; - this.storage.setTestCase({ id: testCaseId, pickleId, testSteps }); - - // prepare steps - const stepsMap = {}; - testSteps.forEach((step, index) => { - const { pickleStepId, id, hookId } = step; - - if (pickleStepId) { - const { steps: stepsData } = this.storage.getPickle(pickleId); - const stepData = stepsData.find((item) => item.id === pickleStepId); - stepsMap[id] = { ...stepData, type: TEST_ITEM_TYPES.STEP }; - } else if (hookId) { - const isBeforeHook = index === 0; - const { name } = this.storage.getHook(hookId); - stepsMap[id] = { - text: name || (isBeforeHook ? 'Before' : 'After'), - type: isBeforeHook ? TEST_ITEM_TYPES.BEFORE_TEST : TEST_ITEM_TYPES.AFTER_TEST, - }; - } - }); - this.storage.setSteps(testCaseId, stepsMap); - }, - onTestCaseStartedEvent(data) { - const { id, testCaseId, attempt } = data; - this.storage.setTestCaseStartedId(id, testCaseId); - const { pickleId, isRetry: isTestCaseRetried } = this.storage.getTestCase(testCaseId); - - const { - uri: pickleFeatureUri, - astNodeIds: [scenarioId, parametersId], - } = this.storage.getPickle(pickleId); - const currentFeatureUri = this.storage.getCurrentFeatureUri(); - const feature = this.storage.getFeature(pickleFeatureUri); - const launchTempId = this.storage.getLaunchTempId(); - const isNeedToStartFeature = currentFeatureUri !== pickleFeatureUri; - - // start FEATURE if no currentFeatureUri or new feature - // else finish old one - const featureCodeRef = utils.formatCodeRef(pickleFeatureUri, feature.name); - if (isNeedToStartFeature) { - const isFirstFeatureInLaunch = currentFeatureUri === null; - const suiteData = { - name: `${feature.keyword}: ${feature.name}`, - startTime: this.reportportal.helpers.now(), - type: this.isScenarioBasedStatistics ? TEST_ITEM_TYPES.TEST : TEST_ITEM_TYPES.SUITE, - description: (feature.description || '').trim(), - attributes: utils.createAttributes(feature.tags), - codeRef: featureCodeRef, - }; - - if (!isFirstFeatureInLaunch) { - const previousFeatureTempId = this.storage.getFeatureTempId(); - this.reportportal.finishTestItem(previousFeatureTempId, { - endTime: this.reportportal.helpers.now(), - }); - } - - this.storage.setCurrentFeatureUri(pickleFeatureUri); - const { tempId } = this.reportportal.startTestItem(suiteData, launchTempId, ''); - this.storage.setFeatureTempId(tempId); - } - - // current feature node rule(this entity is for grouping several - // scenarios in one logical block) || scenario - const currentNode = utils.findNode(feature, scenarioId); - - let scenario; - let ruleTempId; - if (currentNode.rule) { - ruleTempId = this.storage.getRuleTempId(currentNode.rule.id); - - if (!ruleTempId) { - const { rule } = currentNode; - - const { name, description, tags, keyword, children = [], id: ruleId } = rule; - const childrenIds = children.map((child) => child.scenario.id); - const currentNodeCodeRef = utils.formatCodeRef(featureCodeRef, name); - const testData = { - startTime: this.reportportal.helpers.now(), - type: this.isScenarioBasedStatistics ? TEST_ITEM_TYPES.TEST : TEST_ITEM_TYPES.SUITE, - name: `${keyword}: ${name}`, - description, - attributes: utils.createAttributes(tags), - codeRef: currentNodeCodeRef, - }; - const parentId = this.storage.getFeatureTempId(); - const { tempId } = this.reportportal.startTestItem(testData, launchTempId, parentId); - ruleTempId = tempId; - - scenario = utils.findScenario(rule, scenarioId); - - this.storage.setRuleTempId(ruleId, ruleTempId); - this.storage.setRuleTempIdToTestCase(id, ruleTempId); - this.storage.setRuleChildrenIds(ruleTempId, childrenIds); - this.storage.setStartedRuleChildrenIds(ruleTempId, scenarioId); - } else { - this.storage.setRuleTempIdToTestCase(id, ruleTempId); - this.storage.setStartedRuleChildrenIds(ruleTempId, scenarioId); - scenario = utils.findScenario(currentNode.rule, scenarioId); - } - } else { - scenario = currentNode.scenario; - } - - let isRetry = isTestCaseRetried; - if (attempt > 0) { - isRetry = true; - this.storage.updateTestCase(testCaseId, { isRetry }); - - // do not show scenario with retry in RP - if (!this.isScenarioBasedStatistics) return; - } - - const { name: scenarioName } = scenario; - const [keyword] = scenario.keyword.split(' '); - - const currentNodeCodeRef = utils.formatCodeRef( - featureCodeRef, - ruleTempId ? currentNode.rule.name : scenarioName, - ); - const scenarioCodeRefIndexValue = this.codeRefIndexesMap.get(currentNodeCodeRef); - this.codeRefIndexesMap.set(currentNodeCodeRef, (scenarioCodeRefIndexValue || 0) + 1); - const name = - scenarioCodeRefIndexValue && !isRetry - ? `${scenarioName} [${scenarioCodeRefIndexValue}]` - : scenarioName; - const scenarioCodeRef = - scenarioCodeRefIndexValue && !isRetry - ? `${currentNodeCodeRef} [${scenarioCodeRefIndexValue}]` - : currentNodeCodeRef; - - const testData = { - startTime: this.reportportal.helpers.now(), - type: this.isScenarioBasedStatistics ? TEST_ITEM_TYPES.STEP : TEST_ITEM_TYPES.TEST, - name: `${keyword}: ${name}`, - description: scenario.description, - attributes: utils.createAttributes(scenario.tags), - codeRef: scenarioCodeRef, - retry: this.isScenarioBasedStatistics && attempt > 0, - }; - - if (parametersId) { - const [{ tableHeader, tableBody }] = scenario.examples; - const params = utils.collectParams({ tableHeader, tableBody }); - Object.keys(params).forEach((paramKey) => { - this.storage.setParameters(paramKey, params[paramKey]); - }); - testData.parameters = this.storage.getParameters(parametersId); - } - const parentId = ruleTempId || this.storage.getFeatureTempId(); - const { tempId } = this.reportportal.startTestItem(testData, launchTempId, parentId); - this.storage.setScenarioTempId(testCaseId, tempId); - this.storage.updateTestCase(testCaseId, { - codeRef: scenarioCodeRef, - }); - }, - onTestStepStartedEvent(data) { - const { testCaseStartedId, testStepId } = data; - const testCaseId = this.storage.getTestCaseId(testCaseStartedId); - const testCase = this.storage.getTestCase(testCaseId); - const step = this.storage.getStep(testCaseId, testStepId); - - // start step - if (step) { - const currentFeatureUri = this.storage.getCurrentFeatureUri(); - const astNodesData = this.storage.getAstNodesData(currentFeatureUri); - - const { text: stepName, type, astNodeIds } = step; - const keyword = - astNodeIds && (astNodesData.find(({ id }) => astNodeIds.includes(id)) || {}).keyword; - - const codeRef = utils.formatCodeRef(testCase.codeRef, stepName); - const stepCodeRefIndexValue = this.codeRefIndexesMap.get(codeRef); - this.codeRefIndexesMap.set(codeRef, (stepCodeRefIndexValue || 0) + 1); - const name = - stepCodeRefIndexValue && !testCase.isRetry - ? `${stepName} [${stepCodeRefIndexValue}]` - : stepName; - - const stepData = { - name: keyword ? `${keyword} ${name}` : name, - startTime: this.reportportal.helpers.now(), - type, - codeRef, - hasStats: !this.isScenarioBasedStatistics, - retry: !this.isScenarioBasedStatistics && !!testCase.isRetry, - }; - - if (!this.isScenarioBasedStatistics && step.astNodeIds && step.astNodeIds.length > 1) { - const { testSteps } = testCase; - const testStep = testSteps.find((item) => item.id === testStepId); - const argumentsMap = testStep.stepMatchArgumentsLists[0].stepMatchArguments.map((arg) => - arg.group.value.slice(1, -1), - ); - const parametersId = step.astNodeIds[1]; - const params = this.storage.getParameters(parametersId); - stepData.parameters = params.filter((param) => argumentsMap.includes(param.value)); - } - - const launchTempId = this.storage.getLaunchTempId(); - const parentId = this.storage.getScenarioTempId(testCaseId); - const { tempId } = this.reportportal.startTestItem(stepData, launchTempId, parentId); - this.storage.setStepTempId(testStepId, tempId); - } - }, - onTestStepAttachmentEvent(data) { - if (data) { - const { testStepId, testCaseStartedId } = data; - const testCaseId = this.storage.getTestCaseId(testCaseStartedId); - const step = this.storage.getStep(testCaseId, testStepId); - const dataObj = utils.getJSON(data.body); - - switch (data.mediaType) { - case RP_EVENTS.TEST_CASE_ID: { - this.storage.updateStep(testCaseId, testStepId, dataObj); - break; - } - case RP_EVENTS.ATTRIBUTES: { - const savedAttributes = step.attributes || []; - this.storage.updateStep(testCaseId, testStepId, { - attributes: savedAttributes.concat(dataObj.attributes), - }); - break; - } - case RP_EVENTS.DESCRIPTION: { - const savedDescription = step.description || ''; - this.storage.updateStep(testCaseId, testStepId, { - description: savedDescription - ? `${savedDescription}
${dataObj.description}` - : dataObj.description, - }); - break; - } - case RP_EVENTS.STATUS: { - if (dataObj.entity !== RP_ENTITY_LAUNCH) { - this.storage.updateStep(testCaseId, testStepId, dataObj); - } else { - this.customLaunchStatus = dataObj.status; - } - break; - } - case 'text/plain': { - const request = { - time: this.reportportal.helpers.now(), - }; - let tempStepId = this.storage.getStepTempId(testStepId); - - if (dataObj) { - request.level = dataObj.level; - request.message = dataObj.message; - if (dataObj.entity === RP_ENTITY_LAUNCH) { - tempStepId = this.storage.getLaunchTempId(); - } - } else { - request.level = LOG_LEVELS.DEBUG; - request.message = data.body; - } - this.reportportal.sendLog(tempStepId, request); - break; - } - default: { - const fileName = 'file'; // TODO: generate human valuable file name here if possible - const request = { - time: this.reportportal.helpers.now(), - level: LOG_LEVELS.INFO, - message: fileName, - file: { - name: fileName, - }, - }; - let tempStepId = this.storage.getStepTempId(testStepId); - - if (dataObj) { - if (dataObj.level) { - request.level = dataObj.level; - } - request.message = dataObj.message; - request.file.name = dataObj.message; - if (dataObj.entity === RP_ENTITY_LAUNCH) { - tempStepId = this.storage.getLaunchTempId(); - } - } - const fileObj = { - name: fileName, - type: data.mediaType, - content: (dataObj && dataObj.data) || data.body, - }; - this.reportportal.sendLog(tempStepId, request, fileObj); - break; - } - } - } - }, - onTestStepFinishedEvent(data) { - const { testCaseStartedId, testStepId, testStepResult } = data; - const testCaseId = this.storage.getTestCaseId(testCaseStartedId); - const step = this.storage.getStep(testCaseId, testStepId); - const tempStepId = this.storage.getStepTempId(testStepId); - let status; - - switch (testStepResult.status.toLowerCase()) { - case STATUSES.PASSED: { - status = STATUSES.PASSED; - break; - } - case STATUSES.PENDING: { - this.reportportal.sendLog(tempStepId, { - time: this.reportportal.helpers.now(), - level: 'WARN', - message: "This step is marked as 'pending'", - }); - status = STATUSES.FAILED; - break; - } - case STATUSES.UNDEFINED: { - this.reportportal.sendLog(tempStepId, { - time: this.reportportal.helpers.now(), - level: 'ERROR', - message: 'There is no step definition found. Please verify and implement it.', - }); - status = STATUSES.FAILED; - break; - } - case STATUSES.AMBIGUOUS: { - this.reportportal.sendLog(tempStepId, { - time: this.reportportal.helpers.now(), - level: 'ERROR', - message: 'There are more than one step implementation. Please verify and reimplement it.', - }); - status = STATUSES.FAILED; - break; - } - case STATUSES.SKIPPED: { - status = STATUSES.SKIPPED; - break; - } - case STATUSES.FAILED: { - status = STATUSES.FAILED; - this.reportportal.sendLog(tempStepId, { - time: this.reportportal.helpers.now(), - level: 'ERROR', - message: stripAnsi(testStepResult.message), - }); - - const isBrowserAvailable = 'browser' in global; - const isTakeScreenshotOptionProvidedInRPConfig = - this.config.takeScreenshot && this.config.takeScreenshot === 'onFailure'; - - if (isBrowserAvailable && isTakeScreenshotOptionProvidedInRPConfig) { - const currentFeatureUri = this.storage.getCurrentFeatureUri(); - const astNodesData = this.storage.getAstNodesData(currentFeatureUri); - const screenshotName = utils.getScreenshotName(astNodesData, step.astNodeIds); - - const request = { - time: this.reportportal.helpers.now(), - level: 'ERROR', - file: { name: screenshotName }, - message: screenshotName, - }; - - global.browser - .takeScreenshot() - .then((png) => { - const screenshot = { - name: screenshotName, - type: 'image/png', - content: png, - }; - this.reportportal.sendLog(tempStepId, request, screenshot); - }) - .catch((error) => { - console.dir(error); - }); - } - break; - } - default: - break; - } - - if (step) { - const { attributes, description = '', testCaseId: customTestCaseId } = step; - status = step.status || status || testStepResult.status; - const errorMessage = - testStepResult.message && `\`\`\`error\n${stripAnsi(testStepResult.message)}\n\`\`\``; - const descriptionToSend = errorMessage - ? `${description}${description ? '\n' : ''}${errorMessage}` - : description; - const withoutIssue = status === STATUSES.SKIPPED && this.config.skippedIssue === false; - this.reportportal.finishTestItem(tempStepId, { - ...(status && { status }), - ...(attributes && { attributes }), - ...(descriptionToSend && { description: descriptionToSend }), - ...(customTestCaseId && { testCaseId: customTestCaseId }), - ...(withoutIssue && { issue: { issueType: 'NOT_ISSUE' } }), - endTime: this.reportportal.helpers.now(), - }); - } - - if (this.isScenarioBasedStatistics && status !== STATUSES.PASSED) { - this.storage.updateTestCase(testCaseId, { status: STATUSES.FAILED }); - } - - this.storage.removeStepTempId(testStepId); - }, - onTestCaseFinishedEvent({ testCaseStartedId, willBeRetried }) { - const isNeedToFinishTestCase = !this.isScenarioBasedStatistics && willBeRetried; - - if (isNeedToFinishTestCase) { - return; - } - - const testCaseId = this.storage.getTestCaseId(testCaseStartedId); - const testCase = this.storage.getTestCase(testCaseId); - const scenarioTempId = this.storage.getScenarioTempId(testCaseId); - - this.reportportal.finishTestItem(scenarioTempId, { - endTime: this.reportportal.helpers.now(), - ...(this.isScenarioBasedStatistics && { status: testCase.status || STATUSES.PASSED }), - }); - - // finish RULE if it's exist and if it's last scenario - const ruleTempId = this.storage.getRuleTempIdToTestCase(testCaseStartedId); - const ruleChildrenIds = this.storage.getRuleChildrenIds(ruleTempId); - const startedRuleChildrenIds = this.storage.getStartedRuleChildrenIds(ruleTempId); - const isAllRuleChildrenStarted = utils.isAllRuleChildrenStarted( - ruleChildrenIds, - startedRuleChildrenIds, - ); - - if (ruleTempId && isAllRuleChildrenStarted) { - this.reportportal.finishTestItem(ruleTempId, { - endTime: this.reportportal.helpers.now(), - }); - - this.storage.removeRuleTempIdToTestCase(testCaseStartedId); - this.storage.removeStartedRuleChildrenIds(ruleTempId); - this.storage.removeRuleChildrenIds(ruleTempId); - this.codeRefIndexesMap.clear(); - } - - if (!willBeRetried) { - this.storage.removeTestCaseStartedId(testCaseStartedId); - this.storage.removeSteps(testCaseId); - this.storage.removeTestCase(testCaseId); - this.storage.removeScenarioTempId(testCaseStartedId); - } - }, - onTestRunFinishedEvent() { - const featureTempId = this.storage.getFeatureTempId(); - this.reportportal.finishTestItem(featureTempId, { - endTime: this.reportportal.helpers.now(), - }); - - const launchId = this.storage.getLaunchTempId(); - this.reportportal.getPromiseFinishAllItems(launchId).then(() => { - this.reportportal.finishLaunch(launchId, { - ...(this.customLaunchStatus && { status: this.customLaunchStatus }), - }); - this.storage.setLaunchTempId(null); - this.storage.setCurrentFeatureUri(null); - this.storage.setFeatureTempId(null); - this.customLaunchStatus = null; - }); - }, -}; diff --git a/modules/api/deprecated/README.md b/modules/api/deprecated/README.md deleted file mode 100644 index de0c9d0..0000000 --- a/modules/api/deprecated/README.md +++ /dev/null @@ -1,316 +0,0 @@ -## THIS IS DOCUMENTATION FOR DEPRECATED VERSION CUCUMBER FRAMEWORK
PLEASE USE CUCUMBER VERSION FROM 7 OR HIGHER - -# agent-js-cucumber - -Agent for integration CucumberJS with ReportPortal. -* More about [CucumberJS](https://cucumber.io/docs/installation/javascript/) -* More about [ReportPortal](http://reportportal.io/) - -This agent works well with cucumber versions from 4.x to 6.x inclusive. - -## Install agent to your project dir - -```cmd -npm install --save-dev @reportportal/agent-js-cucumber -``` - -1. Make sure that you required glue code correctly. It is important to make Cucumber see support code. - For example: - Let's say you have project structure like this below - - ``` - my-project - L features - L step_definitions - L steps.js - L support - L hooks.js - L world.js - L package.json - ``` - - #### Note - - Protractor and Cucumber have their own **timeouts** . - When protractror start main process that lauches cucumber it would have different timeouts if there not the same they would wait for scripts different time. - If cucumbers's timeout less then protractor's it would through wrong exeption. - For example if page that has been loaded and hasn't got angular, the next error would be thrown : `Error: function timed out after 10000 milliseconds . . .` . Instead of protractor's : - `Error: Error while running testForAngular: asynchronous script timeout: result was not received in 4 seconds . . .` . - So it must be handled manually by setting cucumbers's timeout greater then protractor's is at the hooks.js. For example if you set up protractor's timeout 9000 miliseconds , so cucumber must be at least 1 second greater = 10000 miliseconds. Example : - - ```javascript - var { setDefaultTimeout } = require('cucumber'); - - setDefaultTimeout(10000); - ``` - -2. Create Report Portal configuration file - For example `./rpConfig.json` - - In example below `${text}` - is used as placeholder for your data. This data you must get from ReportPortal profile. - - ```json - { - "token": "${rp.token}", - "endpoint": "${rp.endpoint}/api/v1", - "launch": "${rp.launch}", - "project": "${rp.your_project}", - "takeScreenshot": "onFailure", - "description": "Awesome launch description.", - "attributes": [ - { - "key": "launchAttributeKey", - "value": "launchAttributeValue" - } - ], - "mode": "DEFAULT", - "debug": false, - "restClientConfig": { - "timeout": 0 - } - } - ``` - - `takeScreenshot` - if this option is defined then framework will take screenshot with _protractor or webdriver_ API if step has failed
- `mode` - Launch mode. Allowable values *DEFAULT* (by default) or *DEBUG*.
- `debug` - this flag allows seeing the logs of the `client-javascript`. Useful for debugging. - `restClientConfig` (optional) - The object with `agent` property for configure [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client, may contain other client options eg. `timeout`. - -3. Create Report Portal formatter in a new js file, for example `reportPortalFormatter.js`: - - ```javascript - const { createRPFormatterClass } = require('@reportportal/agent-js-cucumber'); - const config = require('./rpConfig.json'); - - module.exports = createRPFormatterClass(config); - ``` - -4. Import RPWorld (provides API for logging and data attaching) into /features/step_definitions/support/world.js - - ```javascript - let { setWorldConstructor } = require('cucumber'); - let { RPWorld } = require('@reportportal/agent-js-cucumber'); - setWorldConstructor(RPWorld); - ``` - - If you have other world constructors it must be used with the RPWorld as shown below - - ```javascript - class CustomWorld extends RPWorld { - constructor(...args) { - super(...args); - - /* - * any driver container must be named 'browser', because reporter could be used with cucumber - * and protractor. And protractor has global object browser which contains all web-driver methods - */ - global.browser = new seleniumWebdriver.Builder().forBrowser('chrome').build(); - } - } - - setWorldConstructor(CustomWorld); - ``` - - It will allow you send logs and screenshots to RP directly from step definitions. - **All this logs would be attached to test data and could be viewed at the Report Portal**.
- Also you will be able to specify additional info for test items (e.g. description, attributes, testCaseId, status). - See [API](#api) section for more information. - -5. Run cucumber-js - - `cucumber-js -f ./reportPortalFormatter.js` - - More info in the [examples](https://github.com/reportportal/examples-js/tree/master/example-cucumber) repository. - -### TODO parallel launch - -## Rerun - -To report [rerun](https://github.com/reportportal/documentation/blob/master/src/md/src/DevGuides/rerun.md) to the report portal you need to specify the following options to the config file: - -- rerun - to enable rerun -- rerunOf - UUID of launch you want to rerun. If not specified, report portal will update the latest launch with the same name - -Example: - -```json - "rerun": true, - "rerunOf": "f68f39f9-279c-4e8d-ac38-1216dffcc59c" -``` - -## Step reporting configuration - -By default, this agent reports the following structure: - -- feature - SUITE -- scenario - TEST -- step - STEP - -You may change this behavior to report steps to the log level by enabling scenario-based reporting: - -- feature - TEST -- scenario - STEP -- step - log item - -To report your steps as logs, you need to pass an additional parameter to the agent config: `"scenarioBasedStatistics": true` - -```json -{ - "scenarioBasedStatistics": true -} -``` - -This will report your your steps with logs to a log level without creating statistics for every step. - -## Reporting skipped cucumber steps as failed - -By default, cucumber marks steps which follow a failed step as `skipped`. -When `scenarioBasedStatistics` is set to `false` (the default behavior) -Report Portal reports these steps as failures to investigate. - -To change this behavior and instead mark skipped steps which follow a failed step as `cancelled`, -you need to add an additional parameter to the agent config: `"reportSkippedCucumberStepsOnFailedTest": false` - -```json -{ - "reportSkippedCucumberStepsOnFailedTest": false -} -``` - -Steps which are marked as `skipped` that do not follow a failed step will continue to mark the step and the scenario as `skipped`. - -## API - -### Attachments - -Attachments are being reported as logs. You can either just attach a file using cucumber's `this.attach` or specify log level and message: - -```javascript -this.attach( - JSON.stringify({ - message: `Attachment with ${type}`, - level: 'INFO', - data: data.toString('base64'), - }), - type, -); -``` -To send attachment to the launch just specify `entity: 'launch'` property. - -Also `this.screenshot` and `this.launchScreenshot` methods can be used to take screenshots. - -```javascript -Then(/^I should see my new task in the list$/, function(callback) { - this.screenshot('This screenshot') - .then(() => callback()) - .catch((err) => callback(err)); - this.launchScreenshot('This is screenshot for launch') - .then(() => callback()) - .catch((err) => callback(err)); -}); -``` - -`screenshot`/`launchScreenshot` function return promise fulfilled after `screenshot` is taken and image added to attachments. -Handler will parse attachments and send corresponding log to the step item. - -### Logs - -To report logs to the **items** you can use the next methods: - -```javascript -Then(/^I should see my new task in the list$/, function() { - this.info('This is Info Level log'); - this.debug('This is Debug Level log'); - this.error('This is Error Level log'); - this.warn('This is Warn Level log'); - this.trace('This is Trace Level log'); - this.fatal('This is Fatal Level log'); -}); -``` - -To report logs to the **launch** you can use the next methods: - -```javascript -Then(/^I should see my new task in the list$/, function() { - this.launchInfo('This is Info Level log'); - this.launchDebug('This is Debug Level log'); - this.launchError('This is Error Level log'); - this.launchWarn('This is Warn Level log'); - this.launchTrace('This is Trace Level log'); - this.launchFatal('This is Fatal Level log'); -}); -``` - -### Attributes - -Attributes for features and scenarios are parsed from @tags as `@key:value` pair. - -To add attributes to the items you can use the next method: - -```javascript -Then(/^I should see my new task in the list$/, function() { - this.addAttributes([{ key: 'agent', value: 'cucumber' }]); -}); -``` - -The attributes will be concatenated. - -### Description - -Description for features and scenarios are parsed from their definition. - -To add description to the items you can use the following method: - -```javascript -Then(/^I should see my new task in the list$/, function() { - this.addDescription('Test item description.'); -}); -``` - -The description will be concatenated. - -### TestCaseId - -To set test case id to the items you can use the following method: - -```javascript -Then(/^I should see my new task in the list$/, function() { - this.setTestCaseId('itemTestCaseId'); -}); -``` - -### Statuses - -The user can set the status of the item/launch directly depending on some conditions or behavior. -It will take precedence over the actual completed status. - -To set status to the **item** you can use the next methods: - -```javascript -Then(/^I should see my new task in the list$/, function() { - this.setStatusPassed(); - this.setStatusFailed(); - this.setStatusSkipped(); - this.setStatusStopped(); - this.setStatusInterrupted(); - this.setStatusCancelled(); -}); -``` - -To set status to the **item** you can use the next methods: - -```javascript -Then(/^I should see my new task in the list$/, function() { - this.setLaunchStatusPassed(); - this.setLaunchStatusFailed(); - this.setLaunchStatusSkipped(); - this.setLaunchStatusStopped(); - this.setLaunchStatusInterrupted(); - this.setLaunchStatusCancelled(); -}); -``` - -# Copyright Notice - -Licensed under the [Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0.html) -license (see the LICENSE.txt file). diff --git a/modules/api/deprecated/context.js b/modules/api/deprecated/context.js deleted file mode 100644 index 19c8eb1..0000000 --- a/modules/api/deprecated/context.js +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2020 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 { STATUSES } = require('../../constants'); - -class Context { - constructor() { - this.initContext(); - } - - initContext() { - this.outlineRow = 0; - this.scenarioStatus = STATUSES.FAILED; - this.forcedIssue = null; - this.currentFeatureUri = null; - this.scenarioId = null; - this.stepId = null; - this.stepStatus = STATUSES.FAILED; - this.launchId = null; - this.background = null; - this.failedScenarios = {}; - this.lastScenarioDescription = null; - this.scenario = null; - this.step = null; - this.stepSourceLocation = null; - this.stepDefinitions = null; - this.stepDefinition = null; - this.itemsParams = {}; - } - - getFileName() { - const fileName = this.stepDefinition - ? `Failed at step definition line:${this.stepDefinition.line}` - : 'UNDEFINED STEP'; - - return fileName; - } - - findStep(event) { - let stepObj = null; - const stepDefinition = this.stepDefinitions.steps[event.index]; - - if (stepDefinition.hookType) { - stepObj = { keyword: stepDefinition.hookType }; - } else { - this.scenario.steps.forEach((step) => { - if ( - stepDefinition.sourceLocation.uri === event.testCase.sourceLocation.uri && - stepDefinition.sourceLocation.line === step.location.line - ) { - stepObj = step; - } - }); - - if (this.background) { - this.background.steps.forEach((step) => { - if ( - stepDefinition.sourceLocation.uri === event.testCase.sourceLocation.uri && - stepDefinition.sourceLocation.line === step.location.line - ) { - stepObj = step; - } - }); - } - } - return stepObj; - } - - incrementFailedScenariosCount(uri) { - this.failedScenarios[uri] = this.failedScenarios[uri] ? this.failedScenarios[uri] + 1 : 1; - } - - resetContext() { - this.initContext(); - } -} - -module.exports = Context; diff --git a/modules/api/deprecated/cucumber-reportportal-formatter.test.js b/modules/api/deprecated/cucumber-reportportal-formatter.test.js deleted file mode 100644 index be8c477..0000000 --- a/modules/api/deprecated/cucumber-reportportal-formatter.test.js +++ /dev/null @@ -1,888 +0,0 @@ -/* These tests need to be rewritten to reflect the refactoring of the agent. Plans for version 5.0.1. */ -const { createRPFormatterClass } = require('../../index'); -const { - ContextMock, - DocumentsStorageMock, - RPClientMock, - getDefaultConfig, - mockedDate, -} = require('../../../tests/mocks'); -const itemFinders = require('./itemFinders'); -const utils = require('./utils'); -const { AFTER_HOOK_URI_TO_SKIP, STATUSES } = require('../../constants'); - -const featureMock = { - description: 'feature description', - keyword: 'ft', - name: 'feature', - tags: ['@feature:value'], - children: [], -}; - -describe('Create ReportPortal formatter class', function() { - let FormatterClass; - let formatter; - - beforeEach(() => { - const config = getDefaultConfig(); - - FormatterClass = createRPFormatterClass(config); - - formatter = new FormatterClass({ - parsedArgvOptions: {}, - eventBroadcaster: { - on: () => {}, - }, - }); - - formatter.contextState = new ContextMock(); - formatter.documentsStorage = new DocumentsStorageMock(); - formatter.reportportal = new RPClientMock(); - formatter.attributesConf = []; - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - describe('onGherkinDocument', () => { - const documentEvent = { - uri: 'mockUri', - document: 'any', - pickle: null, - }; - - test('should call cacheDocument method from documents storage to cache the document', function() { - const spyCacheDocument = jest.spyOn(formatter.documentsStorage, 'cacheDocument'); - - formatter.onGherkinDocument(documentEvent); - - expect(spyCacheDocument).toHaveBeenCalledWith(documentEvent); - }); - - test('should call startLaunch method from RPClient if not launchId in the config', function() { - const launchStartObj = { - name: 'LauncherName', - startTime: mockedDate, - description: 'Launch description', - attributes: [], - rerun: undefined, - rerunOf: undefined, - }; - - const spyStartLaunch = jest.spyOn(formatter.reportportal, 'startLaunch'); - - formatter.onGherkinDocument(documentEvent); - - expect(spyStartLaunch).toHaveBeenCalledWith(launchStartObj); - expect(formatter.contextState.context.launchId).toBe('tempLaunchId'); - }); - - test('should not call startLaunch method from RPClient if launchId exists in the config', function() { - formatter.contextState.context.launchId = 'tempLaunchId'; - const spyStartLaunch = jest.spyOn(formatter.reportportal, 'startLaunch'); - - formatter.onGherkinDocument(documentEvent); - - expect(spyStartLaunch).toHaveBeenCalledTimes(0); - }); - }); - - describe('onPickleAccepted', () => { - const uriMock = 'featureUri'; - const documentEvent = { - uri: uriMock, - document: 'any', - pickle: null, - }; - - beforeAll(() => { - jest.spyOn(itemFinders, 'findFeature').mockImplementation(() => featureMock); - jest.spyOn(itemFinders, 'findBackground').mockReturnValue(null); - jest.spyOn(utils, 'getUri').mockReturnValue(uriMock); - jest.spyOn(utils, 'createAttribute').mockReturnValue([{ key: 'feature', value: 'value' }]); - }); - - beforeEach(() => { - formatter.documentsStorage.pickleDocuments[uriMock] = {}; - }); - - test('should call isAcceptedPickleCached method from documents storage with event', function() { - const spyIsAcceptedPickleCached = jest.spyOn( - formatter.documentsStorage, - 'isAcceptedPickleCached', - ); - - formatter.onPickleAccepted(documentEvent); - - expect(spyIsAcceptedPickleCached).toHaveBeenCalledWith(documentEvent); - }); - - test('should call cacheAcceptedPickle method from documents storage if pickle not cached', function() { - const spyCacheAcceptedPickle = jest.spyOn(formatter.documentsStorage, 'cacheAcceptedPickle'); - - formatter.onPickleAccepted(documentEvent); - - expect(spyCacheAcceptedPickle).toHaveBeenCalledWith(documentEvent); - }); - - test('should not call cacheAcceptedPickle method from documents storage if pickle cached', function() { - jest.spyOn(formatter.documentsStorage, 'isAcceptedPickleCached').mockReturnValue(true); - - const spyCacheAcceptedPickle = jest.spyOn(formatter.documentsStorage, 'cacheAcceptedPickle'); - - formatter.onPickleAccepted(documentEvent); - - expect(spyCacheAcceptedPickle).toHaveBeenCalledTimes(0); - }); - }); - - describe('onTestCasePrepared', () => { - test('should set stepDefinitions and isBeforeHook for the context', function() { - const event = { - data: 'any', - }; - - formatter.onTestCasePrepared(event); - - expect(formatter.contextState.context.stepDefinitions).toEqual(event); - expect(formatter.contextState.context.isBeforeHook).toBe(true); - }); - }); - - describe('onTestCaseStarted', () => { - const uriMock = 'featureUri'; - const documentEvent = { - uri: uriMock, - document: 'any', - name: 'lol', - keyword: 'loc', - description: 'description', - sourceLocation: { - uri: uriMock, - }, - }; - const pickleTags = [ - { - name: '@feature:value', - location: { - line: 1, - column: 1, - }, - }, - ]; - let spyFindScenario; - let spyGetUri; - let spyCreateAttributes; - let spyCreateTagComparator; - - beforeAll(() => { - spyFindScenario = jest - .spyOn(itemFinders, 'findScenario') - .mockImplementation(() => featureMock); - spyGetUri = jest.spyOn(utils, 'getUri').mockReturnValue(uriMock); - spyCreateAttributes = jest - .spyOn(utils, 'createAttributes') - .mockReturnValue([{ key: 'feature', value: 'value' }]); - spyCreateTagComparator = jest - .spyOn(utils, 'createTagComparator') - .mockReturnValue(() => false); - }); - - beforeEach(() => { - formatter.documentsStorage.pickleDocuments[uriMock] = { - featureId: 'featureId', - tags: pickleTags, - }; - formatter.documentsStorage.gherkinDocuments[uriMock] = { - feature: { - tags: [], - }, - }; - }); - - test('should call findScenario with gherkinDocuments & sourceLocation', function() { - formatter.onTestCaseStarted(documentEvent); - - expect(spyFindScenario).toHaveBeenCalledWith( - formatter.documentsStorage.gherkinDocuments, - documentEvent.sourceLocation, - ); - }); - - test('should call getUri with uri from event sourceLocation', function() { - formatter.onTestCaseStarted(documentEvent); - - expect(spyGetUri).toHaveBeenCalledWith(documentEvent.sourceLocation.uri); - }); - - test('should call createTagComparator with pickle tag value', function() { - formatter.onTestCaseStarted(documentEvent); - - expect(spyCreateTagComparator).toHaveBeenCalledWith(pickleTags[0]); - }); - - test('should call createAttributes with pickleTags', function() { - formatter.onTestCaseStarted(documentEvent); - - expect(spyCreateAttributes).toHaveBeenCalledWith(pickleTags); - }); - - test('should call startTestItem method from RPClient if isScenarioBasedStatistics is true', function() { - formatter.isScenarioBasedStatistics = true; - - const itemStartObj = { - name: 'ft: feature', - type: 'STEP', - startTime: mockedDate, - description: 'feature description', - attributes: [{ key: 'feature', value: 'value' }], - retry: false, - }; - - formatter.contextState.context.launchId = 'tempLaunchId'; - const spyStartTestItem = jest.spyOn(formatter.reportportal, 'startTestItem'); - - formatter.onTestCaseStarted(documentEvent); - - expect(spyStartTestItem).toHaveBeenCalledWith(itemStartObj, 'tempLaunchId', 'featureId'); - }); - - test('should call startTestItem method from RPClient if isScenarioBasedStatistics is false and attemptNumber from event <=2', function() { - formatter.isScenarioBasedStatistics = false; - - const itemStartObj = { - name: 'ft: feature', - type: 'TEST', - startTime: mockedDate, - description: 'feature description', - attributes: [{ key: 'feature', value: 'value' }], - retry: false, - }; - - formatter.contextState.context.launchId = 'tempLaunchId'; - - const spyStartTestItem = jest.spyOn(formatter.reportportal, 'startTestItem'); - - formatter.onTestCaseStarted({ - ...documentEvent, - attemptNumber: 1, - }); - - expect(spyStartTestItem).toHaveBeenCalledWith(itemStartObj, 'tempLaunchId', 'featureId'); - }); - - test('should not call startTestItem method from RPClient if isScenarioBasedStatistics is false and attemptNumber from event >=2', function() { - formatter.isScenarioBasedStatistics = false; - - const spyStartTestItem = jest.spyOn(formatter.reportportal, 'startTestItem'); - - formatter.onTestCaseStarted({ - ...documentEvent, - attemptNumber: 2, - }); - - expect(spyStartTestItem).toHaveBeenCalledTimes(0); - }); - }); - - describe('onTestStepStarted', () => { - const stepMock = { - keyword: 'stepExample', - }; - const stepDefinitionMock = { - name: 'stepDefinition', - }; - const event = { - index: 0, - testCase: { - attemptNumber: 1, - }, - }; - - let spyFindStep; - let spyFindStepDefinition; - let spyGetStepType; - - beforeAll(() => { - spyFindStepDefinition = jest - .spyOn(itemFinders, 'findStepDefinition') - .mockImplementation(() => stepDefinitionMock); - spyGetStepType = jest.spyOn(utils, 'getStepType').mockReturnValue('STEP'); - }); - - beforeEach(() => { - spyFindStep = jest - .spyOn(formatter.contextState, 'findStep') - .mockImplementation(() => stepMock); - formatter.contextState.context.stepDefinitions = { - steps: [ - { - sourceLocation: {}, - }, - ], - }; - }); - - test('should call findStep function to find step for context', function() { - formatter.onTestStepStarted(event); - - expect(spyFindStep).toHaveBeenCalledWith(event); - expect(formatter.contextState.context.step).toEqual(stepMock); - }); - - test('should call findStepDefinition function to find step definition for context', function() { - formatter.onTestStepStarted(event); - - expect(spyFindStepDefinition).toHaveBeenCalledWith(formatter.contextState.context, event); - expect(formatter.contextState.context.stepDefinition).toEqual(stepDefinitionMock); - }); - - test('should call getStepType function to get type for step', function() { - formatter.onTestStepStarted(event); - - expect(spyGetStepType).toHaveBeenCalledWith(stepMock.keyword); - }); - - test('should call startTestItem method from RPClient', function() { - formatter.contextState.context.launchId = 'launchId'; - formatter.contextState.context.scenarioId = 'scenarioId'; - formatter.isScenarioBasedStatistics = false; - - const itemStartObj = { - name: stepMock.keyword, - type: 'STEP', - startTime: mockedDate, - description: '', - hasStats: true, - retry: false, - }; - - const spyStartTestItem = jest.spyOn(formatter.reportportal, 'startTestItem'); - - formatter.onTestStepStarted(event); - - expect(spyStartTestItem).toHaveBeenCalledWith(itemStartObj, 'launchId', 'scenarioId'); - expect(formatter.contextState.context.stepId).toBe('testItemId'); - }); - - test('should not call startTestItem method and stop function execution', function() { - formatter.contextState.context.stepDefinitions = { - steps: [ - { - actionLocation: { - uri: `uri: ${AFTER_HOOK_URI_TO_SKIP}`, - }, - }, - ], - }; - - const spyStartTestItem = jest.spyOn(formatter.reportportal, 'startTestItem'); - - formatter.onTestStepStarted(event); - - expect(spyFindStep).toHaveBeenCalledTimes(0); - expect(spyFindStepDefinition).toHaveBeenCalledTimes(0); - expect(spyGetStepType).toHaveBeenCalledTimes(0); - expect(spyStartTestItem).toHaveBeenCalledTimes(0); - }); - }); - - describe('onTestStepFinished', () => { - const event = { - result: { - status: 'passed', - }, - testCase: { - attemptNumber: 1, - sourceLocation: { uri: 'testCaseUri' }, - }, - }; - - let spyGetFileName; - let spyCountFailedScenarios; - - beforeEach(() => { - spyGetFileName = jest.spyOn(formatter.contextState, 'getFileName'); - spyCountFailedScenarios = jest - .spyOn(formatter.contextState, 'countFailedScenarios') - .mockImplementation(() => {}); - formatter.contextState.context.stepSourceLocation = { sourceLocation: {} }; - }); - - test('should call spyGetFileName to get name for screenshot', function() { - formatter.onTestStepFinished(event); - - expect(spyGetFileName).toHaveBeenCalledTimes(1); - }); - - test('should set passed status for step and scenario in case of passed result status', function() { - formatter.onTestStepFinished(event); - - expect(formatter.contextState.context.stepStatus).toBe(STATUSES.PASSED); - expect(formatter.contextState.context.scenarioStatus).toBe(STATUSES.PASSED); - }); - - test('should call sendLog method from RPClient with WARN level in case of pending result status', function() { - event.result.status = STATUSES.PENDING; - formatter.contextState.context.stepId = 'stepId'; - formatter.onTestStepFinished(event); - - const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); - - expect(spySendLog).toHaveBeenCalledWith('stepId', { - time: mockedDate, - level: 'WARN', - message: "This step is marked as 'pending'", - }); - }); - - test('should set not_implemented status for step and failed for scenario in case of pending result status', function() { - event.result.status = STATUSES.PENDING; - formatter.contextState.context.stepId = 'stepId'; - formatter.onTestStepFinished(event); - - expect(formatter.contextState.context.stepStatus).toBe(STATUSES.NOT_IMPLEMENTED); - expect(formatter.contextState.context.scenarioStatus).toBe(STATUSES.FAILED); - expect(spyCountFailedScenarios).toHaveBeenCalledWith(event.testCase.sourceLocation.uri); - }); - - test('should call sendLog method from RPClient with ERROR level in case of undefined result status', function() { - event.result.status = STATUSES.UNDEFINED; - formatter.contextState.context.stepId = 'stepId'; - formatter.onTestStepFinished(event); - - const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); - - expect(spySendLog).toHaveBeenCalledWith('stepId', { - time: mockedDate, - level: 'ERROR', - message: 'There is no step definition found. Please verify and implement it.', - }); - }); - - test('should set not_found status for step and failed for scenario in case of undefined result status', function() { - event.result.status = STATUSES.UNDEFINED; - formatter.contextState.context.stepId = 'stepId'; - formatter.onTestStepFinished(event); - - expect(formatter.contextState.context.stepStatus).toBe(STATUSES.NOT_FOUND); - expect(formatter.contextState.context.scenarioStatus).toBe(STATUSES.FAILED); - expect(spyCountFailedScenarios).toHaveBeenCalledWith(event.testCase.sourceLocation.uri); - }); - - test('should call sendLog method from RPClient with ERROR level in case of ambiguous result status', function() { - event.result.status = STATUSES.AMBIGUOUS; - formatter.contextState.context.stepId = 'stepId'; - formatter.onTestStepFinished(event); - - const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); - - expect(spySendLog).toHaveBeenCalledWith('stepId', { - time: mockedDate, - level: 'ERROR', - message: 'There are more than one step implementation. Please verify and reimplement it.', - }); - }); - - test('should set not_found status for step and failed for scenario in case of ambiguous result status', function() { - event.result.status = STATUSES.AMBIGUOUS; - formatter.contextState.context.stepId = 'stepId'; - formatter.onTestStepFinished(event); - - expect(formatter.contextState.context.stepStatus).toBe(STATUSES.NOT_FOUND); - expect(formatter.contextState.context.scenarioStatus).toBe(STATUSES.FAILED); - expect(spyCountFailedScenarios).toHaveBeenCalledWith(event.testCase.sourceLocation.uri); - }); - - test('should set skipped status for step in case of skipped result status', function() { - event.result.status = STATUSES.SKIPPED; - formatter.onTestStepFinished(event); - - expect(formatter.contextState.context.stepStatus).toBe(STATUSES.SKIPPED); - }); - - test('should set skipped status for scenario if it was failed in case of skipped result status', function() { - event.result.status = STATUSES.SKIPPED; - formatter.contextState.context.scenarioStatus = STATUSES.FAILED; - formatter.onTestStepFinished(event); - - expect(formatter.contextState.context.scenarioStatus).toBe(STATUSES.SKIPPED); - }); - - test('should call sendLog method from RPClient with ERROR level in case of failed result status', function() { - event.result.status = STATUSES.FAILED; - event.result.exception = 255; - const stepDefinitionMock = { - uri: 'stepDefinition', - }; - - formatter.contextState.context.stepDefinition = stepDefinitionMock; - formatter.contextState.context.stepId = 'stepId'; - formatter.onTestStepFinished(event); - - const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); - - expect(spySendLog).toHaveBeenCalledWith('stepId', { - time: mockedDate, - level: 'ERROR', - message: `${stepDefinitionMock.uri}\n 255`, - }); - }); - - test('should set failed status for step in case of failed result status', function() { - event.result.status = STATUSES.FAILED; - formatter.contextState.context.stepDefinition = { - uri: 'stepDefinition', - }; - - formatter.onTestStepFinished(event); - - expect(formatter.contextState.context.stepStatus).toBe(STATUSES.FAILED); - expect(spyCountFailedScenarios).toHaveBeenCalledWith(event.testCase.sourceLocation.uri); - }); - - test('should call finishTestItem method from RPClient', function() { - event.result.status = STATUSES.PASSED; - formatter.contextState.context.stepId = 'stepId'; - - const itemFinishObj = { - status: STATUSES.PASSED, - endTime: mockedDate, - }; - - const spyStartTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); - - formatter.onTestStepFinished(event); - - expect(spyStartTestItem).toHaveBeenCalledWith('stepId', itemFinishObj); - }); - - test('should call finishTestItem method from RPClient with ab001 issue in case of not_found step status', function() { - event.result.status = STATUSES.UNDEFINED; - formatter.contextState.context.stepId = 'stepId'; - - const itemFinishObj = { - status: STATUSES.FAILED, - endTime: mockedDate, - issue: { - issueType: 'ab001', - comment: 'STEP DEFINITION WAS NOT FOUND', - }, - }; - - const spyStartTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); - - formatter.onTestStepFinished(event); - - expect(spyStartTestItem).toHaveBeenCalledWith('stepId', itemFinishObj); - }); - - test('should call finishTestItem method from RPClient with ti001 issue in case of not_implemented step status', function() { - event.result.status = STATUSES.PENDING; - formatter.contextState.context.stepId = 'stepId'; - - const itemFinishObj = { - status: STATUSES.SKIPPED, - endTime: mockedDate, - issue: { - issueType: 'ti001', - comment: 'STEP IS PENDING IMPLEMENTATION', - }, - }; - - const spyStartTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); - - formatter.onTestStepFinished(event); - - expect(spyStartTestItem).toHaveBeenCalledWith('stepId', itemFinishObj); - }); - - test('should not call finishTestItem method and stop function execution', function() { - formatter.contextState.context.stepSourceLocation = { - actionLocation: { - uri: `uri: ${AFTER_HOOK_URI_TO_SKIP}`, - }, - }; - - const spyFinishTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); - const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); - - formatter.onTestStepFinished(event); - - expect(spyGetFileName).toHaveBeenCalledTimes(0); - expect(spyCountFailedScenarios).toHaveBeenCalledTimes(0); - expect(spySendLog).toHaveBeenCalledTimes(0); - expect(spyFinishTestItem).toHaveBeenCalledTimes(0); - }); - }); - - describe('onTestStepAttachment', () => { - const mockFileObj = { - level: 'INFO', - message: 'file', - data: 'string', - }; - const event = { - data: [ - { - item: 'text', - }, - ], - }; - - const spyGetJSON = jest.spyOn(utils, 'getJSON'); - let spyGetFileName; - - beforeEach(() => { - spyGetFileName = jest - .spyOn(formatter.contextState, 'getFileName') - .mockImplementation(() => 'fileName'); - - formatter.contextState.context.stepStatus = STATUSES.PASSED; - formatter.contextState.context.stepId = 'stepId'; - }); - - test('should call spyGetFileName to get name for file', function() { - event.media = { - type: 'text/plain', - }; - spyGetJSON.mockImplementationOnce(() => mockFileObj); - formatter.onTestStepAttachment(event); - - expect(spyGetFileName).toHaveBeenCalledTimes(1); - }); - - test('should call sendLog method from RPClient to send log with attachment for text media type', function() { - event.media = { - type: 'text/plain', - }; - spyGetJSON.mockImplementationOnce(() => mockFileObj); - - const request = { - level: mockFileObj.level, - message: mockFileObj.message, - time: mockedDate, - }; - - const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); - - formatter.onTestStepAttachment(event); - - expect(spySendLog).toHaveBeenCalledWith('stepId', request); - }); - - test('should call sendLog method from RPClient with DEBUG level log in case of invalid json data for text media type', function() { - event.media = { - type: 'text/plain', - }; - spyGetJSON.mockImplementationOnce(() => false); - - const request = { - level: 'DEBUG', - message: event.data, - time: mockedDate, - }; - - const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); - - formatter.onTestStepAttachment(event); - - expect(spySendLog).toHaveBeenCalledWith('stepId', request); - }); - - test('should call sendLog method from RPClient to send log with attachment for other media type', function() { - event.media = { - type: 'other', - }; - spyGetJSON.mockImplementationOnce(() => mockFileObj); - - const request = { - level: mockFileObj.level, - message: mockFileObj.message, - time: mockedDate, - file: { - name: mockFileObj.message, - }, - }; - const fileObj = { - name: 'fileName', - type: 'other', - content: mockFileObj.data, - }; - - const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); - - formatter.onTestStepAttachment(event); - - expect(spySendLog).toHaveBeenCalledWith('stepId', request, fileObj); - }); - - test('should call sendLog method from RPClient with default parameters in case of invalid json data for other media type', function() { - event.media = { - type: 'other', - }; - spyGetJSON.mockImplementationOnce(() => false); - - const request = { - level: 'DEBUG', - message: 'fileName', - time: mockedDate, - file: { - name: 'fileName', - }, - }; - const fileObj = { - name: 'fileName', - type: 'other', - content: event.data, - }; - - const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); - - formatter.onTestStepAttachment(event); - - expect(spySendLog).toHaveBeenCalledWith('stepId', request, fileObj); - }); - }); - - describe('onTestCaseFinished', () => { - const itemUri = 'itemUri'; - const event = { - sourceLocation: { - uri: itemUri, - }, - result: { - status: STATUSES.PASSED, - }, - }; - - let spyFinishTestItem; - - beforeEach(() => { - spyFinishTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); - - formatter.isScenarioBasedStatistics = false; - formatter.contextState.context.scenarioId = 'scenarioId'; - formatter.contextState.context.scenariosCount[itemUri] = { - done: 0, - total: 2, - }; - - formatter.documentsStorage.pickleDocuments[itemUri] = { - featureId: 'featureId', - }; - }); - - test('should call finishTestItem method from RPClient to finish test item', function() { - formatter.onTestCaseFinished(event); - - const itemFinishObj = { - status: STATUSES.PASSED, - endTime: mockedDate, - }; - - expect(spyFinishTestItem).toHaveBeenCalledWith('scenarioId', itemFinishObj); - expect(formatter.contextState.context.scenarioStatus).toBe(STATUSES.FAILED); - expect(formatter.contextState.context.scenarioId).toBe(null); - }); - - test('should call finishTestItem method from RPClient and finish test item with failed status in case of result status not passed', function() { - event.result.status = STATUSES.SKIPPED; - formatter.onTestCaseFinished(event); - - const itemFinishObj = { - status: STATUSES.FAILED, - endTime: mockedDate, - }; - - expect(spyFinishTestItem).toHaveBeenCalledWith('scenarioId', itemFinishObj); - expect(formatter.contextState.context.scenarioStatus).toBe(STATUSES.FAILED); - expect(formatter.contextState.context.scenarioId).toBe(null); - }); - - test('should increase scenariosCount done in context', function() { - event.result.status = STATUSES.PASSED; - formatter.onTestCaseFinished(event); - - expect(formatter.contextState.context.scenariosCount[itemUri].done).toBe(1); - }); - - test('should call finishTestItem method from RPClient twice in case of finishing the item from suite', function() { - formatter.contextState.context.scenariosCount[itemUri].total = 1; - formatter.contextState.context.failedScenarios[itemUri] = 0; - - event.result.status = STATUSES.PASSED; - formatter.onTestCaseFinished(event); - - const finishItemObj = { - status: STATUSES.PASSED, - endTime: mockedDate, - }; - - expect(spyFinishTestItem).toHaveBeenCalledTimes(2); - expect(spyFinishTestItem).toHaveBeenNthCalledWith(2, 'featureId', finishItemObj); - }); - - test('should call finishTestItem method from RPClient second time with failed status in case of failed scenarios in suite', function() { - formatter.contextState.context.scenariosCount[itemUri].total = 1; - formatter.contextState.context.failedScenarios[itemUri] = 1; - - event.result.status = STATUSES.PASSED; - formatter.onTestCaseFinished(event); - - const finishItemObj = { - status: STATUSES.FAILED, - endTime: mockedDate, - }; - - expect(spyFinishTestItem).toHaveBeenCalledTimes(2); - expect(spyFinishTestItem).toHaveBeenNthCalledWith(2, 'featureId', finishItemObj); - }); - - test('should not call finishTestItem method and stop function execution', function() { - formatter.isScenarioBasedStatistics = false; - event.result.retried = true; - - formatter.onTestCaseFinished(event); - - expect(spyFinishTestItem).toHaveBeenCalledTimes(0); - }); - }); - - describe('onTestRunFinished', () => { - let spyGetPromiseFinishAllItems; - let spyFinishLaunch; - - beforeEach(() => { - spyGetPromiseFinishAllItems = jest.spyOn(formatter.reportportal, 'getPromiseFinishAllItems'); - spyFinishLaunch = jest.spyOn(formatter.reportportal, 'finishLaunch'); - }); - - test('should call getPromiseFinishAllItems method from RPClient & should not call finishLaunch method for empty launch id', function() { - formatter.contextState.context.launchId = null; - - formatter.onTestRunFinished(); - - expect(spyGetPromiseFinishAllItems).toHaveBeenCalledWith(null); - expect(spyFinishLaunch).toHaveBeenCalledTimes(0); - }); - - test('should call getPromiseFinishAllItems method from RPClient and should finish launch with corresponding id', async function() { - formatter.contextState.context.launchId = 'launchId'; - await formatter.onTestRunFinished(); - - expect(spyGetPromiseFinishAllItems).toHaveBeenCalledWith('launchId'); - expect(spyFinishLaunch).toHaveBeenCalledWith('launchId', { endTime: mockedDate }); - }); - - test('should call resetContext method from context after finishing launch', async function() { - formatter.contextState.context.launchId = 'launchId'; - const spyResetContext = jest.spyOn(formatter.contextState, 'resetContext'); - - await formatter.onTestRunFinished(); - - expect(spyResetContext).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/modules/api/deprecated/documents-storage.js b/modules/api/deprecated/documents-storage.js deleted file mode 100644 index 2600bba..0000000 --- a/modules/api/deprecated/documents-storage.js +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2020 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. - */ - -class DocumentsStorage { - constructor() { - this.gherkinDocuments = {}; - this.featureData = {}; - } - - cacheDocument(gherkinDocument) { - this.gherkinDocuments[gherkinDocument.uri] = gherkinDocument.document; - } - - createCachedFeature(uri) { - this.featureData[uri] = {}; - } - - isFeatureDataCached(uri) { - return !!this.featureData[uri]; - } -} - -module.exports = DocumentsStorage; diff --git a/modules/api/deprecated/index.js b/modules/api/deprecated/index.js deleted file mode 100644 index 565a689..0000000 --- a/modules/api/deprecated/index.js +++ /dev/null @@ -1,568 +0,0 @@ -/* - * 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. - */ - -const stripAnsi = require('strip-ansi'); -const Table = require('cli-table3'); -const utils = require('./utils'); -const itemFinders = require('./itemFinders'); -const { - STATUSES, - AFTER_HOOK_URI_TO_SKIP, - TABLE_CONFIG, - RP_EVENTS, - RP_ENTITY_LAUNCH, - LOG_LEVELS, - CUCUMBER_EVENTS, -} = require('../../constants'); -const Context = require('./context'); -const DocumentStorage = require('./documents-storage'); -const pjson = require('../../../package.json'); - -module.exports = { - init() { - this.context = new Context(); - this.documentsStorage = new DocumentStorage(); - - this.options.eventBroadcaster.on(CUCUMBER_EVENTS.GHERKIN_DOCUMENT, (event) => - this.onGherkinDocument(event), - ); - this.options.eventBroadcaster.on(CUCUMBER_EVENTS.PICKLE_ACCEPTED, (event) => - this.onPickleAccepted(event), - ); - this.options.eventBroadcaster.on(CUCUMBER_EVENTS.TEST_CASE_PREPARED, (event) => - this.onTestCasePrepared(event), - ); - this.options.eventBroadcaster.on(CUCUMBER_EVENTS.TEST_CASE_STARTED, (event) => - this.onTestCaseStarted(event), - ); - this.options.eventBroadcaster.on(CUCUMBER_EVENTS.TEST_STEP_STARTED, (event) => - this.onTestStepStarted(event), - ); - this.options.eventBroadcaster.on(CUCUMBER_EVENTS.TEST_STEP_FINISHED, (event) => - this.onTestStepFinished(event), - ); - this.options.eventBroadcaster.on(CUCUMBER_EVENTS.TEST_STEP_ATTACHMENT, (event) => - this.onTestStepAttachment(event), - ); - this.options.eventBroadcaster.on(CUCUMBER_EVENTS.TEST_CASE_FINISHED, (event) => - this.onTestCaseFinished(event), - ); - this.options.eventBroadcaster.on(CUCUMBER_EVENTS.TEST_RUN_FINISHED, (event) => - this.onTestRunFinished(event), - ); - }, - onGherkinDocument(event) { - this.documentsStorage.cacheDocument(event); - - // BeforeFeatures - if (!this.context.launchId) { - const launch = this.reportportal.startLaunch({ - name: this.config.launch, - startTime: this.reportportal.helpers.now(), - description: !this.config.description ? '' : this.config.description, - attributes: [ - ...(this.config.attributes || []), - { key: 'agent', value: `${pjson.name}|${pjson.version}`, system: true }, - ], - rerun: this.isRerun, - rerunOf: this.rerunOf, - }); - this.context.launchId = launch.tempId; - } - }, - onPickleAccepted(event) { - const featureUri = utils.getUri(event.uri); - if (!this.documentsStorage.isFeatureDataCached(featureUri)) { - this.documentsStorage.createCachedFeature(featureUri); - - const feature = this.documentsStorage.featureData[featureUri]; - const featureDocument = itemFinders.findFeature( - this.documentsStorage.gherkinDocuments, - event, - ); - feature.description = featureDocument.description || featureUri; - const { name } = featureDocument; - feature.name = name; - feature.itemAttributes = utils.createAttributes(featureDocument.tags); - } - }, - onTestCasePrepared(event) { - const featureUri = utils.getUri(event.sourceLocation.uri); - const feature = this.documentsStorage.featureData[featureUri]; - // If this is the first scenario in the feature, start the feature in RP - if (!feature.featureId) { - feature.featureId = this.reportportal.startTestItem( - { - name: feature.name, - startTime: this.reportportal.helpers.now(), - type: this.isScenarioBasedStatistics ? 'TEST' : 'SUITE', - codeRef: utils.formatCodeRef(featureUri, feature.name), - description: feature.description, - attributes: feature.itemAttributes, - }, - this.context.launchId, - ).tempId; - } - // If this is the first feature in the run, set the currentFeatureUri - if (!this.context.currentFeatureUri) { - this.context.currentFeatureUri = featureUri; - } - // If this is a new feature, finish the previous feature in RP. - // does not work for the final feature in the run. that is finished in onTestRunFinished - if (this.context.currentFeatureUri !== featureUri) { - const previousFeature = this.documentsStorage.featureData[this.context.currentFeatureUri]; - // If this is a new feature, finish the previous feature - this.reportportal.finishTestItem(previousFeature.featureId, { - status: previousFeature.featureStatus, - endTime: this.reportportal.helpers.now(), - }); - // Now that the previous feature is finished, assign the new current feature - this.context.currentFeatureUri = featureUri; - } - this.context.stepDefinitions = event; - let hookType = 'Before'; - this.context.stepDefinitions.steps.forEach((step) => { - if (step.sourceLocation) { - hookType = 'After'; - return; - } - // eslint-disable-next-line no-param-reassign - step.hookType = hookType; - }); - }, - onTestCaseStarted(event) { - const featureDocument = itemFinders.findFeature( - this.documentsStorage.gherkinDocuments, - event.sourceLocation, - ); - this.context.scenario = itemFinders.findScenario( - this.documentsStorage.gherkinDocuments, - event.sourceLocation, - ); - this.context.scenarioStatus = STATUSES.STARTED; - this.context.background = itemFinders.findBackground(featureDocument); - const featureTags = featureDocument.tags; - const keyword = this.context.scenario.keyword - ? this.context.scenario.keyword - : this.context.scenario.type; - let name = [keyword, this.context.scenario.name].join(': '); - const eventTags = this.context.scenario.tags - ? this.context.scenario.tags.filter( - (tag) => !featureTags.find(utils.createTagComparator(tag)), - ) - : []; - const itemAttributes = utils.createAttributes(eventTags); - const description = - this.context.scenario.description || - [utils.getUri(event.sourceLocation.uri), event.sourceLocation.line].join(':'); - const { featureId } = this.documentsStorage.featureData[event.sourceLocation.uri]; - - if (this.context.lastScenarioDescription !== name) { - this.context.lastScenarioDescription = name; - this.context.outlineRow = 0; - } else if (event.attemptNumber < 2) { - this.context.outlineRow += 1; - name += ` [${this.context.outlineRow}]`; - } - - // BeforeScenario - if (this.isScenarioBasedStatistics || event.attemptNumber < 2) { - this.context.scenarioId = this.reportportal.startTestItem( - { - name, - startTime: this.reportportal.helpers.now(), - type: this.isScenarioBasedStatistics ? 'STEP' : 'TEST', - description, - codeRef: utils.formatCodeRef(event.sourceLocation.uri, name), - parameters: this.context.scenario.parameters, - attributes: itemAttributes, - retry: this.isScenarioBasedStatistics && event.attemptNumber > 1, - }, - this.context.launchId, - featureId, - ).tempId; - } - }, - onTestStepStarted(event) { - this.context.stepStatus = STATUSES.FAILED; - this.context.stepId = null; - - this.context.stepSourceLocation = this.context.stepDefinitions.steps[event.index]; - - // skip After Hook added by protractor-cucumber-framework - if ( - !this.context.stepSourceLocation.sourceLocation && - this.context.stepSourceLocation.actionLocation.uri.includes(AFTER_HOOK_URI_TO_SKIP) - ) - return; - - this.context.step = this.context.findStep(event); - this.context.stepDefinition = itemFinders.findStepDefinition(this.context, event); - - let description; - let name = this.context.step.text - ? `${this.context.step.keyword} ${this.context.step.text}` - : this.context.step.keyword; - - if (this.context.step.argument) { - let stepArguments; - if (this.context.step.argument.content) { - stepArguments = `"""\n${this.context.step.argument.content}\n"""`; - } - - if (this.context.step.argument.rows) { - const rows = this.context.step.argument.rows.map((row) => - row.cells.map((cell) => { - // Added an if statement to only replace step parameters if this is a Scenario Outline - let tempStepValue = cell.value; - if (this.context.scenario.parameters) { - this.context.scenario.parameters.forEach((parameter) => { - if (cell.value.includes(`<${parameter.key}>`)) { - tempStepValue = utils.replaceParameter( - cell.value, - parameter.key, - parameter.value, - ); - } - }); - } - return tempStepValue; - }), - ); - const datatable = new Table(TABLE_CONFIG); - datatable.push(...rows); - stepArguments = datatable.toString(); - } - if (this.isScenarioBasedStatistics) { - name += `\n${stepArguments}`; - } else { - description = stepArguments; - } - } - - let type = 'STEP'; - let isHook = false; - if (this.context.step.keyword === 'Before') { - type = 'BEFORE_TEST'; - isHook = true; - } else if (this.context.step.keyword === 'After') { - type = 'AFTER_TEST'; - isHook = true; - } - - // hooks are described in cucumber's library core - const codeRef = - this.context.stepDefinition && !isHook - ? utils.formatCodeRef(this.context.stepDefinition.uri, name) - : undefined; - - this.context.stepId = this.reportportal.startTestItem( - { - name, - description, - startTime: this.reportportal.helpers.now(), - type, - codeRef, - parameters: this.context.step.parameters, - hasStats: !this.isScenarioBasedStatistics, - retry: !this.isScenarioBasedStatistics && event.testCase.attemptNumber > 1, - }, - this.context.launchId, - this.context.scenarioId, - ).tempId; - }, - onTestStepFinished(event) { - // skip After Hook added by protractor-cucumber-framework - if ( - !this.context.stepSourceLocation.sourceLocation && - this.context.stepSourceLocation.actionLocation.uri.includes(AFTER_HOOK_URI_TO_SKIP) - ) - return; - - // StepResult - const sceenshotName = this.context.getFileName(); - - switch (event.result.status) { - case STATUSES.PASSED: { - this.context.stepStatus = STATUSES.PASSED; - if (this.context.scenarioStatus !== STATUSES.FAILED) { - this.context.scenarioStatus = STATUSES.PASSED; - } - break; - } - case STATUSES.PENDING: { - this.reportportal.sendLog(this.context.stepId, { - time: this.reportportal.helpers.now(), - level: 'WARN', - message: "This step is marked as 'pending'", - }); - this.context.stepStatus = STATUSES.NOT_IMPLEMENTED; - this.context.scenarioStatus = STATUSES.FAILED; - this.context.incrementFailedScenariosCount(event.testCase.sourceLocation.uri); - break; - } - case STATUSES.UNDEFINED: { - this.reportportal.sendLog(this.context.stepId, { - time: this.reportportal.helpers.now(), - level: 'ERROR', - message: 'There is no step definition found. Please verify and implement it.', - }); - this.context.stepStatus = STATUSES.NOT_FOUND; - this.context.scenarioStatus = STATUSES.FAILED; - this.context.incrementFailedScenariosCount(event.testCase.sourceLocation.uri); - break; - } - case STATUSES.AMBIGUOUS: { - this.reportportal.sendLog(this.context.stepId, { - time: this.reportportal.helpers.now(), - level: 'ERROR', - message: 'There are more than one step implementation. Please verify and reimplement it.', - }); - this.context.stepStatus = STATUSES.NOT_FOUND; - this.context.scenarioStatus = STATUSES.FAILED; - this.context.incrementFailedScenariosCount(event.testCase.sourceLocation.uri); - break; - } - case STATUSES.SKIPPED: { - this.context.stepStatus = STATUSES.SKIPPED; - if (this.context.scenarioStatus === STATUSES.FAILED) { - this.context.scenarioStatus = STATUSES.SKIPPED; - } - - if ( - this.context.scenarioStatus === STATUSES.STARTED || - this.context.scenarioStatus === STATUSES.PASSED - ) { - this.context.scenarioStatus = STATUSES.SKIPPED; - } else { - this.context.scenarioStatus = STATUSES.FAILED; - if ( - // eslint-disable-next-line no-prototype-builtins - this.config.hasOwnProperty('reportSkippedCucumberStepsOnFailedTest') && - !this.config.reportSkippedCucumberStepsOnFailedTest - ) { - this.context.stepStatus = STATUSES.CANCELLED; - } - } - - break; - } - case STATUSES.FAILED: { - this.context.stepStatus = STATUSES.FAILED; - this.context.scenarioStatus = STATUSES.FAILED; - this.context.incrementFailedScenariosCount(event.testCase.sourceLocation.uri); - const errorMessage = stripAnsi( - `${this.context.stepDefinition.uri}\n ${event.result.exception.toString()}`, - ); - this.reportportal.sendLog(this.context.stepId, { - time: this.reportportal.helpers.now(), - level: 'ERROR', - message: errorMessage, - }); - if ( - global.browser && - this.config.takeScreenshot && - this.config.takeScreenshot === 'onFailure' - ) { - const request = { - time: this.reportportal.helpers.now(), - level: 'ERROR', - file: { name: sceenshotName }, - message: sceenshotName, - }; - global.browser.takeScreenshot().then((png) => { - const fileObj = { - name: sceenshotName, - type: 'image/png', - content: png, - }; - this.reportportal.sendLog(this.context.stepId, request, fileObj); - }); - } - break; - } - default: - break; - } - - const itemParams = this.context.itemsParams[this.context.stepId]; - - // AfterStep - const request = { - status: this.context.stepStatus, - endTime: this.reportportal.helpers.now(), - ...itemParams, - }; - if (request.status === STATUSES.NOT_FOUND) { - request.status = STATUSES.FAILED; - request.issue = { - issueType: 'ab001', - comment: 'STEP DEFINITION WAS NOT FOUND', - }; - } else if (request.status === STATUSES.NOT_IMPLEMENTED) { - request.status = STATUSES.SKIPPED; - request.issue = { - issueType: 'ti001', - comment: 'STEP IS PENDING IMPLEMENTATION', - }; - } - - this.reportportal.finishTestItem(this.context.stepId, request); - }, - - updateItemParams(id, newParams) { - this.context.itemsParams[id] = { - ...this.context.itemsParams[id], - ...newParams, - }; - }, - getItemParams(id) { - return this.context.itemsParams[id] || {}; - }, - onTestStepAttachment(event) { - const fileName = this.context.getFileName(); - if ( - event.data && - event.data.length && - (this.context.stepStatus === STATUSES.PASSED || this.context.stepStatus === STATUSES.FAILED) - ) { - const dataObj = utils.getJSON(event.data); - let itemId = this.context.stepId; - - switch (event.media.type) { - case RP_EVENTS.TEST_CASE_ID: { - this.updateItemParams(itemId, { testCaseId: dataObj.testCaseId }); - break; - } - case RP_EVENTS.ATTRIBUTES: { - const savedAttributes = this.getItemParams(itemId).attributes || []; - this.updateItemParams(itemId, { - attributes: savedAttributes.concat(dataObj.attributes), - }); - break; - } - case RP_EVENTS.DESCRIPTION: { - const savedDescription = this.getItemParams(itemId).description || ''; - this.updateItemParams(itemId, { - description: savedDescription - ? `${savedDescription}
${dataObj.description}` - : dataObj.description, - }); - break; - } - case RP_EVENTS.STATUS: { - if (dataObj.entity !== RP_ENTITY_LAUNCH) { - this.updateItemParams(itemId, { - status: dataObj.status, - }); - } else { - this.context.launchStatus = dataObj.status; - } - break; - } - case 'text/plain': { - const request = { - time: this.reportportal.helpers.now(), - }; - if (dataObj) { - request.level = dataObj.level; - request.message = dataObj.message; - if (dataObj.entity === RP_ENTITY_LAUNCH) { - itemId = this.context.launchId; - } - } else { - request.level = LOG_LEVELS.DEBUG; - request.message = event.data; - } - this.reportportal.sendLog(itemId, request); - break; - } - default: { - const request = { - time: this.reportportal.helpers.now(), - level: - this.context.stepStatus === STATUSES.PASSED ? LOG_LEVELS.DEBUG : LOG_LEVELS.ERROR, - message: fileName, - file: { - name: fileName, - }, - }; - if (dataObj) { - request.level = dataObj.level; - request.message = dataObj.message; - request.file.name = dataObj.message; - if (dataObj.entity === RP_ENTITY_LAUNCH) { - itemId = this.context.launchId; - } - } - const fileObj = { - name: fileName, - type: event.media.type, - content: (dataObj && dataObj.data) || event.data, - }; - this.reportportal.sendLog(itemId, request, fileObj); - break; - } - } - } - }, - onTestCaseFinished(event) { - if (!this.isScenarioBasedStatistics && event.result.retried) { - return; - } - const isFailed = event.result.status !== STATUSES.PASSED; - // ScenarioResult - this.reportportal.finishTestItem(this.context.scenarioId, { - status: isFailed ? STATUSES.FAILED : STATUSES.PASSED, - endTime: this.reportportal.helpers.now(), - }); - this.context.scenarioId = null; - const featureUri = event.sourceLocation.uri; - - this.documentsStorage.featureData[featureUri].featureStatus = - this.context.failedScenarios[featureUri] > 0 ? STATUSES.FAILED : STATUSES.PASSED; - }, - onTestRunFinished(event) { - // Finish the final feature in the run - const finalFeature = this.documentsStorage.featureData[this.context.currentFeatureUri]; - this.reportportal.finishTestItem(finalFeature.featureId, { - status: finalFeature.featureStatus, - endTime: this.reportportal.helpers.now(), - }); - // AfterFeatures - const promise = this.reportportal.getPromiseFinishAllItems(this.context.launchId); - return promise.then(() => { - if (this.context.launchId) { - const finishLaunchRQ = { - endTime: this.reportportal.helpers.now(), - status: event.result.success ? STATUSES.PASSED : STATUSES.FAILED, - }; - - if (this.context.launchStatus) { - finishLaunchRQ.status = this.context.launchStatus; - } - - const launchFinishPromise = this.reportportal.finishLaunch( - this.context.launchId, - finishLaunchRQ, - ).promise; - launchFinishPromise.then(() => { - this.context.resetContext(); - }); - } - }); - }, -}; diff --git a/modules/api/deprecated/itemFinders.js b/modules/api/deprecated/itemFinders.js deleted file mode 100644 index e7aa021..0000000 --- a/modules/api/deprecated/itemFinders.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2020 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 utils = require('./utils'); - -function createSteps(header, row, steps) { - return steps.map((step) => { - const modified = { ...step, parameters: [] }; - - header.cells.forEach((variable, index) => { - const isParameterPresents = modified.text.indexOf(`<${variable.value}>`) !== -1; - modified.text = utils.replaceParameter(modified.text, variable.value, row.cells[index].value); - - if (isParameterPresents) { - modified.parameters.push({ key: variable.value, value: row.cells[index].value }); - } - }); - - return modified; - }); -} - -function createScenarioFromOutlineExample(outline, example, row) { - const parameters = utils.getParameters(example.tableHeader, row); - let outlineName = outline.name; - - parameters.forEach((param) => { - outlineName = utils.replaceParameter(outlineName, param.key, param.value); - }); - - return { - type: 'Scenario', - tags: example.tags, - location: row.location, - keyword: 'Scenario', - name: outlineName, - steps: createSteps(example.tableHeader, row, outline.steps), - parameters, - description: outline.description, - }; -} - -function createScenarioFromOutline(outline, location) { - let foundRow; - const foundExample = outline.examples.find((example) => { - foundRow = example.tableBody.find((row) => row.location.line === location.line); - - return !!foundRow; - }); - - if (!foundRow) return null; - - return createScenarioFromOutlineExample(outline, foundExample, foundRow); -} - -function findOutlineScenario(outlines, location) { - return outlines - .map((child) => createScenarioFromOutline(child, location)) - .find((outline) => !!outline); -} - -function findBackground(feature) { - return feature.children ? feature.children.find((child) => child.type === 'Background') : null; -} - -function findFeature(documents, location) { - return documents[location.uri].feature; -} - -function findScenario(documents, location) { - const { children } = findFeature(documents, location); - const scenario = children.find( - (child) => child.type === 'Scenario' && child.location.line === location.line, - ); - if (scenario) { - return scenario; - } - - const outlines = children.filter((child) => child.type === 'ScenarioOutline'); - return findOutlineScenario(outlines, location); -} - -function findStepDefinition(context, event) { - return context.stepDefinitions.steps[event.index].actionLocation; -} - -module.exports = { - findBackground, - findFeature, - findScenario, - findStepDefinition, -}; diff --git a/modules/api/deprecated/utils.js b/modules/api/deprecated/utils.js deleted file mode 100644 index a3459a4..0000000 --- a/modules/api/deprecated/utils.js +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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. - */ - -const path = require('path'); -const commonUtils = require('../../utils'); - -const getUri = (uri) => uri.replace(process.cwd() + path.sep, ''); - -const createTagComparator = (tagA) => (tagB) => - tagB.name === tagA.name && - tagB.location.line === tagA.location.line && - tagB.location.column === tagA.location.column; - -const getParameters = (header, body) => { - const keys = header ? header.cells.map((cell) => cell.value) : []; - - if (Array.isArray(body)) { - return body.reduce((acc, item) => { - const params = item.cells.map((cell, index) => ({ - key: keys[index], - value: cell.value, - })); - - return acc.concat(params); - }, []); - } - - return body.cells.map((cell, index) => ({ - key: keys[index], - value: cell.value, - })); -}; - -function replaceParameter(originalString, name, value) { - return originalString.replace(new RegExp(`<${name}>`, 'g'), value); -} - -const getStepType = (keyword) => { - let type; - - switch (keyword) { - case 'Before': - type = 'BEFORE_TEST'; - break; - case 'After': - type = 'AFTER_TEST'; - break; - default: - type = 'STEP'; - break; - } - - return type; -}; - -module.exports = { - createTagComparator, - getUri, - createAttributes: commonUtils.createAttributes, - getJSON: commonUtils.getJSON, - getStepType, - getParameters, - formatCodeRef: commonUtils.formatCodeRef, - replaceParameter, -}; diff --git a/modules/constants.js b/modules/constants.js index 2d7461a..eb8fd5e 100644 --- a/modules/constants.js +++ b/modules/constants.js @@ -14,7 +14,6 @@ * limitations under the License. */ -const AFTER_HOOK_URI_TO_SKIP = 'protractor-cucumber-framework'; const STATUSES = { PASSED: 'passed', FAILED: 'failed', @@ -84,36 +83,19 @@ const RP_EVENTS = { STATUS: 'rp/status', }; -// @see https://github.com/Automattic/cli-table#custom-styles -const TABLE_CONFIG = { - chars: { - top: '', - 'top-left': '', - 'top-mid': '', - 'top-right': '', - mid: '', - 'left-mid': '', - 'mid-mid': '', - 'right-mid': '', - bottom: '', - 'bottom-left': '', - 'bottom-mid': '', - 'bottom-right': '', - }, - style: { - head: [], - border: [], - }, +const TEST_STEP_FINISHED_RP_MESSAGES = { + PENDING: "This step is marked as 'pending'", + UNDEFINED: 'There is no step definition found. Please verify and implement it.', + AMBIGUOUS: 'There are more than one step implementation. Please verify and reimplement it.', }; module.exports = { - AFTER_HOOK_URI_TO_SKIP, RP_ENTITY_LAUNCH, STATUSES, LOG_LEVELS, CUCUMBER_EVENTS, RP_EVENTS, - TABLE_CONFIG, CUCUMBER_MESSAGES, + TEST_STEP_FINISHED_RP_MESSAGES, TEST_ITEM_TYPES, }; diff --git a/modules/cucumber-reportportal-formatter.js b/modules/cucumber-reportportal-formatter.js index d7baeb5..c9cc852 100644 --- a/modules/cucumber-reportportal-formatter.js +++ b/modules/cucumber-reportportal-formatter.js @@ -15,27 +15,26 @@ */ const ReportPortalClient = require('@reportportal/client-javascript'); +const { Formatter } = require('@cucumber/cucumber'); +const stripAnsi = require('strip-ansi'); const utils = require('./utils'); const pjson = require('../package.json'); +const { + RP_EVENTS, + RP_ENTITY_LAUNCH, + LOG_LEVELS, + STATUSES, + CUCUMBER_MESSAGES, + TEST_STEP_FINISHED_RP_MESSAGES, + TEST_ITEM_TYPES, +} = require('./constants'); +const Storage = require('./storage'); -const createRPFormatterClass = (config) => { - let Formatter; - let module; - try { - // eslint-disable-next-line global-require - Formatter = require('@cucumber/cucumber').Formatter; - // eslint-disable-next-line global-require - module = require('./api/current'); - } catch (e) { - // eslint-disable-next-line global-require - Formatter = require('cucumber').Formatter; - // eslint-disable-next-line global-require - module = require('./api/deprecated'); - } - - return class CucumberReportPortalFormatter extends Formatter { +const createRPFormatterClass = (config) => + class CucumberReportPortalFormatter extends Formatter { constructor(options) { super(options); + this.options = options; this.config = config; this.reportportal = new ReportPortalClient(config, { @@ -49,12 +48,552 @@ const createRPFormatterClass = (config) => { typeof this.config.scenarioBasedStatistics === 'boolean' ? this.config.scenarioBasedStatistics : false; + this.storage = new Storage(); + this.customLaunchStatus = null; + this.codeRefIndexesMap = new Map(); + + this.options.eventBroadcaster.on('envelope', this.eventHandler); + } + + eventHandler(event) { + const [key] = Object.keys(event); + switch (key) { + case CUCUMBER_MESSAGES.GHERKIN_DOCUMENT: + return this.onGherkinDocumentEvent(event[key]); + case CUCUMBER_MESSAGES.PICKLE: + return this.onPickleEvent(event[key]); + case CUCUMBER_MESSAGES.HOOK: + return this.onHookEvent(event[key]); + case CUCUMBER_MESSAGES.TEST_RUN_STARTED: + return this.onTestRunStartedEvent(); + case CUCUMBER_MESSAGES.TEST_CASE: + return this.onTestCaseEvent(event[key]); + case CUCUMBER_MESSAGES.TEST_CASE_STARTED: + return this.onTestCaseStartedEvent(event[key]); + case CUCUMBER_MESSAGES.TEST_STEP_STARTED: + return this.onTestStepStartedEvent(event[key]); + case CUCUMBER_MESSAGES.ATTACHMENT: + return this.onTestStepAttachmentEvent(event[key]); + case CUCUMBER_MESSAGES.TEST_STEP_FINISHED: + return this.onTestStepFinishedEvent(event[key]); + case CUCUMBER_MESSAGES.TEST_CASE_FINISHED: + return this.onTestCaseFinishedEvent(event[key]); + case CUCUMBER_MESSAGES.TEST_RUN_FINISHED: + return this.onTestRunFinishedEvent(event[key]); + default: + return null; + } + } + + onGherkinDocumentEvent(data) { + this.storage.setDocument(data); + this.storage.setAstNodesData(data, utils.findAstNodesData(data.feature.children)); + } + + onHookEvent(data) { + const { id } = data; + this.storage.setHook(id, data); + } + + onPickleEvent(data) { + this.storage.setPickle(data); + } + + onTestRunStartedEvent() { + const attributes = [ + ...(this.config.attributes || []), + { key: 'agent', value: `${pjson.name}|${pjson.version}`, system: true }, + ]; + if (this.config.skippedIssue === false) { + const skippedIssueAttribute = { key: 'skippedIssue', value: 'false', system: true }; + attributes.push(skippedIssueAttribute); + } + const startLaunchData = { + name: this.config.launch, + startTime: this.reportportal.helpers.now(), + description: this.config.description || '', + attributes, + rerun: this.isRerun, + rerunOf: this.rerunOf, + ...(this.config.mode && { mode: this.config.mode }), + }; + const { tempId } = this.reportportal.startLaunch(startLaunchData); + this.storage.setLaunchTempId(tempId); + } + + onTestCaseEvent(data) { + const { id: testCaseId, pickleId, testSteps } = data; + this.storage.setTestCase({ id: testCaseId, pickleId, testSteps }); + + // prepare steps + const stepsMap = {}; + testSteps.forEach((step, index) => { + const { pickleStepId, id, hookId } = step; + + if (pickleStepId) { + const { steps: stepsData } = this.storage.getPickle(pickleId); + const stepData = stepsData.find((item) => item.id === pickleStepId); + stepsMap[id] = { ...stepData, type: TEST_ITEM_TYPES.STEP }; + } else if (hookId) { + const isBeforeHook = index === 0; + const { name } = this.storage.getHook(hookId); + stepsMap[id] = { + text: name || (isBeforeHook ? 'Before' : 'After'), + type: isBeforeHook ? TEST_ITEM_TYPES.BEFORE_TEST : TEST_ITEM_TYPES.AFTER_TEST, + }; + } + }); + this.storage.setSteps(testCaseId, stepsMap); + } + + onTestCaseStartedEvent(data) { + const { id, testCaseId, attempt } = data; + this.storage.setTestCaseStartedId(id, testCaseId); + const { pickleId, isRetry: isTestCaseRetried } = this.storage.getTestCase(testCaseId); + + const { + uri: pickleFeatureUri, + astNodeIds: [scenarioId, parametersId], + } = this.storage.getPickle(pickleId); + const currentFeatureUri = this.storage.getCurrentFeatureUri(); + const feature = this.storage.getFeature(pickleFeatureUri); + const launchTempId = this.storage.getLaunchTempId(); + const isNeedToStartFeature = currentFeatureUri !== pickleFeatureUri; + + // start FEATURE if no currentFeatureUri or new feature + // else finish old one + const featureCodeRef = utils.formatCodeRef(pickleFeatureUri, feature.name); + if (isNeedToStartFeature) { + const isFirstFeatureInLaunch = currentFeatureUri === null; + const suiteData = { + name: `${feature.keyword}: ${feature.name}`, + startTime: this.reportportal.helpers.now(), + type: this.isScenarioBasedStatistics ? TEST_ITEM_TYPES.TEST : TEST_ITEM_TYPES.SUITE, + description: (feature.description || '').trim(), + attributes: utils.createAttributes(feature.tags), + codeRef: featureCodeRef, + }; + + if (!isFirstFeatureInLaunch) { + const previousFeatureTempId = this.storage.getFeatureTempId(); + this.reportportal.finishTestItem(previousFeatureTempId, { + endTime: this.reportportal.helpers.now(), + }); + } + + this.storage.setCurrentFeatureUri(pickleFeatureUri); + const { tempId } = this.reportportal.startTestItem(suiteData, launchTempId, ''); + this.storage.setFeatureTempId(tempId); + } + + // current feature node rule(this entity is for grouping several + // scenarios in one logical block) || scenario + const currentNode = utils.findNode(feature, scenarioId); + + let scenario; + let ruleTempId; + if (currentNode.rule) { + ruleTempId = this.storage.getRuleTempId(currentNode.rule.id); + + if (!ruleTempId) { + const { rule } = currentNode; + + const { name, description, tags, keyword, children = [], id: ruleId } = rule; + const childrenIds = children.map((child) => child.scenario.id); + const currentNodeCodeRef = utils.formatCodeRef(featureCodeRef, name); + const testData = { + startTime: this.reportportal.helpers.now(), + type: this.isScenarioBasedStatistics ? TEST_ITEM_TYPES.TEST : TEST_ITEM_TYPES.SUITE, + name: `${keyword}: ${name}`, + description, + attributes: utils.createAttributes(tags), + codeRef: currentNodeCodeRef, + }; + const parentId = this.storage.getFeatureTempId(); + const { tempId } = this.reportportal.startTestItem(testData, launchTempId, parentId); + ruleTempId = tempId; + + scenario = utils.findScenario(rule, scenarioId); + + this.storage.setRuleTempId(ruleId, ruleTempId); + this.storage.setRuleTempIdToTestCase(id, ruleTempId); + this.storage.setRuleChildrenIds(ruleTempId, childrenIds); + this.storage.setStartedRuleChildrenIds(ruleTempId, scenarioId); + } else { + this.storage.setRuleTempIdToTestCase(id, ruleTempId); + this.storage.setStartedRuleChildrenIds(ruleTempId, scenarioId); + scenario = utils.findScenario(currentNode.rule, scenarioId); + } + } else { + scenario = currentNode.scenario; + } + + let isRetry = isTestCaseRetried; + if (attempt > 0) { + isRetry = true; + this.storage.updateTestCase(testCaseId, { isRetry }); + + // do not show scenario with retry in RP + if (!this.isScenarioBasedStatistics) return; + } + + const { name: scenarioName } = scenario; + const [keyword] = scenario.keyword.split(' '); + + const currentNodeCodeRef = utils.formatCodeRef( + featureCodeRef, + ruleTempId ? currentNode.rule.name : scenarioName, + ); + const scenarioCodeRefIndexValue = this.codeRefIndexesMap.get(currentNodeCodeRef); + this.codeRefIndexesMap.set(currentNodeCodeRef, (scenarioCodeRefIndexValue || 0) + 1); + const name = + scenarioCodeRefIndexValue && !isRetry + ? `${scenarioName} [${scenarioCodeRefIndexValue}]` + : scenarioName; + const scenarioCodeRef = + scenarioCodeRefIndexValue && !isRetry + ? `${currentNodeCodeRef} [${scenarioCodeRefIndexValue}]` + : currentNodeCodeRef; + + const testData = { + startTime: this.reportportal.helpers.now(), + type: this.isScenarioBasedStatistics ? TEST_ITEM_TYPES.STEP : TEST_ITEM_TYPES.TEST, + name: `${keyword}: ${name}`, + description: scenario.description, + attributes: utils.createAttributes(scenario.tags), + codeRef: scenarioCodeRef, + retry: this.isScenarioBasedStatistics && attempt > 0, + }; + + if (parametersId) { + const [{ tableHeader, tableBody }] = scenario.examples; + const params = utils.collectParams({ tableHeader, tableBody }); + Object.keys(params).forEach((paramKey) => { + this.storage.setParameters(paramKey, params[paramKey]); + }); + testData.parameters = this.storage.getParameters(parametersId); + } + const parentId = ruleTempId || this.storage.getFeatureTempId(); + const { tempId } = this.reportportal.startTestItem(testData, launchTempId, parentId); + this.storage.setScenarioTempId(testCaseId, tempId); + this.storage.updateTestCase(testCaseId, { + codeRef: scenarioCodeRef, + }); + } + + onTestStepStartedEvent(data) { + const { testCaseStartedId, testStepId } = data; + const testCaseId = this.storage.getTestCaseId(testCaseStartedId); + const testCase = this.storage.getTestCase(testCaseId); + const step = this.storage.getStep(testCaseId, testStepId); + + // start step + if (step) { + const currentFeatureUri = this.storage.getCurrentFeatureUri(); + const astNodesData = this.storage.getAstNodesData(currentFeatureUri); + + const { text: stepName, type, astNodeIds } = step; + const keyword = + astNodeIds && (astNodesData.find(({ id }) => astNodeIds.includes(id)) || {}).keyword; + + const codeRef = utils.formatCodeRef(testCase.codeRef, stepName); + const stepCodeRefIndexValue = this.codeRefIndexesMap.get(codeRef); + this.codeRefIndexesMap.set(codeRef, (stepCodeRefIndexValue || 0) + 1); + const name = + stepCodeRefIndexValue && !testCase.isRetry + ? `${stepName} [${stepCodeRefIndexValue}]` + : stepName; + + const stepData = { + name: keyword ? `${keyword} ${name}` : name, + startTime: this.reportportal.helpers.now(), + type, + codeRef, + hasStats: !this.isScenarioBasedStatistics, + retry: !this.isScenarioBasedStatistics && !!testCase.isRetry, + }; + + if (!this.isScenarioBasedStatistics && step.astNodeIds && step.astNodeIds.length > 1) { + const { testSteps } = testCase; + const testStep = testSteps.find((item) => item.id === testStepId); + const argumentsMap = testStep.stepMatchArgumentsLists[0].stepMatchArguments.map((arg) => + arg.group.value.slice(1, -1), + ); + const parametersId = step.astNodeIds[1]; + const params = this.storage.getParameters(parametersId); + stepData.parameters = params.filter((param) => argumentsMap.includes(param.value)); + } + + const launchTempId = this.storage.getLaunchTempId(); + const parentId = this.storage.getScenarioTempId(testCaseId); + const { tempId } = this.reportportal.startTestItem(stepData, launchTempId, parentId); + this.storage.setStepTempId(testStepId, tempId); + } + } + + onTestStepAttachmentEvent(data) { + if (data) { + const { testStepId, testCaseStartedId } = data; + const testCaseId = this.storage.getTestCaseId(testCaseStartedId); + const step = this.storage.getStep(testCaseId, testStepId); + const dataObj = utils.getJSON(data.body); + + switch (data.mediaType) { + case RP_EVENTS.TEST_CASE_ID: { + this.storage.updateStep(testCaseId, testStepId, dataObj); + break; + } + case RP_EVENTS.ATTRIBUTES: { + const savedAttributes = step.attributes || []; + this.storage.updateStep(testCaseId, testStepId, { + attributes: savedAttributes.concat(dataObj.attributes), + }); + break; + } + case RP_EVENTS.DESCRIPTION: { + const savedDescription = step.description || ''; + this.storage.updateStep(testCaseId, testStepId, { + description: savedDescription + ? `${savedDescription}
${dataObj.description}` + : dataObj.description, + }); + break; + } + case RP_EVENTS.STATUS: { + if (dataObj.entity !== RP_ENTITY_LAUNCH) { + this.storage.updateStep(testCaseId, testStepId, dataObj); + } else { + this.customLaunchStatus = dataObj.status; + } + break; + } + case 'text/plain': { + const request = { + time: this.reportportal.helpers.now(), + }; + let tempStepId = this.storage.getStepTempId(testStepId); + + if (dataObj) { + request.level = dataObj.level; + request.message = dataObj.message; + if (dataObj.entity === RP_ENTITY_LAUNCH) { + tempStepId = this.storage.getLaunchTempId(); + } + } else { + request.level = LOG_LEVELS.DEBUG; + request.message = data.body; + } + this.reportportal.sendLog(tempStepId, request); + break; + } + default: { + const fileName = 'file'; // TODO: generate human valuable file name here if possible + const request = { + time: this.reportportal.helpers.now(), + level: LOG_LEVELS.INFO, + message: fileName, + file: { + name: fileName, + }, + }; + let tempStepId = this.storage.getStepTempId(testStepId); + + if (dataObj) { + if (dataObj.level) { + request.level = dataObj.level; + } + request.message = dataObj.message; + request.file.name = dataObj.message; + if (dataObj.entity === RP_ENTITY_LAUNCH) { + tempStepId = this.storage.getLaunchTempId(); + } + } + const fileObj = { + name: fileName, + type: data.mediaType, + content: (dataObj && dataObj.data) || data.body, + }; + this.reportportal.sendLog(tempStepId, request, fileObj); + break; + } + } + } + } + + onTestStepFinishedEvent(data) { + const { testCaseStartedId, testStepId, testStepResult } = data; + const testCaseId = this.storage.getTestCaseId(testCaseStartedId); + const step = this.storage.getStep(testCaseId, testStepId); + const tempStepId = this.storage.getStepTempId(testStepId); + let status; + + switch (testStepResult.status.toLowerCase()) { + case STATUSES.PASSED: { + status = STATUSES.PASSED; + break; + } + case STATUSES.PENDING: { + this.reportportal.sendLog(tempStepId, { + time: this.reportportal.helpers.now(), + level: LOG_LEVELS.WARN, + message: TEST_STEP_FINISHED_RP_MESSAGES.PENDING, + }); + status = STATUSES.FAILED; + break; + } + case STATUSES.UNDEFINED: { + this.reportportal.sendLog(tempStepId, { + time: this.reportportal.helpers.now(), + level: LOG_LEVELS.ERROR, + message: TEST_STEP_FINISHED_RP_MESSAGES.UNDEFINED, + }); + status = STATUSES.FAILED; + break; + } + case STATUSES.AMBIGUOUS: { + this.reportportal.sendLog(tempStepId, { + time: this.reportportal.helpers.now(), + level: LOG_LEVELS.ERROR, + message: TEST_STEP_FINISHED_RP_MESSAGES.AMBIGUOUS, + }); + status = STATUSES.FAILED; + break; + } + case STATUSES.SKIPPED: { + status = STATUSES.SKIPPED; + break; + } + case STATUSES.FAILED: { + status = STATUSES.FAILED; + this.reportportal.sendLog(tempStepId, { + time: this.reportportal.helpers.now(), + level: LOG_LEVELS.ERROR, + message: stripAnsi(testStepResult.message), + }); + + const isBrowserAvailable = 'browser' in global; + const isTakeScreenshotOptionProvidedInRPConfig = + this.config.takeScreenshot && this.config.takeScreenshot === 'onFailure'; + + if (isBrowserAvailable && isTakeScreenshotOptionProvidedInRPConfig) { + const currentFeatureUri = this.storage.getCurrentFeatureUri(); + const astNodesData = this.storage.getAstNodesData(currentFeatureUri); + const screenshotName = utils.getScreenshotName(astNodesData, step.astNodeIds); - utils.bindToClass(module, this); + const request = { + time: this.reportportal.helpers.now(), + level: LOG_LEVELS.ERROR, + file: { name: screenshotName }, + message: screenshotName, + }; - this.init(); + global.browser + .takeScreenshot() + .then((png) => { + const screenshot = { + name: screenshotName, + type: 'image/png', + content: png, + }; + this.reportportal.sendLog(tempStepId, request, screenshot); + }) + .catch((error) => { + console.dir(error); + }); + } + break; + } + default: + break; + } + + if (step) { + const { attributes, description = '', testCaseId: customTestCaseId } = step; + status = step.status || status || testStepResult.status; + const errorMessage = + testStepResult.message && `\`\`\`error\n${stripAnsi(testStepResult.message)}\n\`\`\``; + const descriptionToSend = errorMessage + ? `${description}${description ? '\n' : ''}${errorMessage}` + : description; + const withoutIssue = status === STATUSES.SKIPPED && this.config.skippedIssue === false; + this.reportportal.finishTestItem(tempStepId, { + ...(status && { status }), + ...(attributes && { attributes }), + ...(descriptionToSend && { description: descriptionToSend }), + ...(customTestCaseId && { testCaseId: customTestCaseId }), + ...(withoutIssue && { issue: { issueType: 'NOT_ISSUE' } }), + endTime: this.reportportal.helpers.now(), + }); + } + + if (this.isScenarioBasedStatistics && status !== STATUSES.PASSED) { + this.storage.updateTestCase(testCaseId, { status: STATUSES.FAILED }); + } + + this.storage.removeStepTempId(testStepId); + } + + onTestCaseFinishedEvent({ testCaseStartedId, willBeRetried }) { + const isNeedToFinishTestCase = !this.isScenarioBasedStatistics && willBeRetried; + + if (isNeedToFinishTestCase) { + return; + } + + const testCaseId = this.storage.getTestCaseId(testCaseStartedId); + const testCase = this.storage.getTestCase(testCaseId); + const scenarioTempId = this.storage.getScenarioTempId(testCaseId); + + this.reportportal.finishTestItem(scenarioTempId, { + endTime: this.reportportal.helpers.now(), + ...(this.isScenarioBasedStatistics && { status: testCase.status || STATUSES.PASSED }), + }); + + // finish RULE if it's exist and if it's last scenario + const ruleTempId = this.storage.getRuleTempIdToTestCase(testCaseStartedId); + const ruleChildrenIds = this.storage.getRuleChildrenIds(ruleTempId); + const startedRuleChildrenIds = this.storage.getStartedRuleChildrenIds(ruleTempId); + const isAllRuleChildrenStarted = utils.isAllRuleChildrenStarted( + ruleChildrenIds, + startedRuleChildrenIds, + ); + + if (ruleTempId && isAllRuleChildrenStarted) { + this.reportportal.finishTestItem(ruleTempId, { + endTime: this.reportportal.helpers.now(), + }); + + this.storage.removeRuleTempIdToTestCase(testCaseStartedId); + this.storage.removeStartedRuleChildrenIds(ruleTempId); + this.storage.removeRuleChildrenIds(ruleTempId); + this.codeRefIndexesMap.clear(); + } + + if (!willBeRetried) { + this.storage.removeTestCaseStartedId(testCaseStartedId); + this.storage.removeSteps(testCaseId); + this.storage.removeTestCase(testCaseId); + this.storage.removeScenarioTempId(testCaseStartedId); + } + } + + onTestRunFinishedEvent() { + const featureTempId = this.storage.getFeatureTempId(); + this.reportportal.finishTestItem(featureTempId, { + endTime: this.reportportal.helpers.now(), + }); + + const launchId = this.storage.getLaunchTempId(); + this.reportportal.getPromiseFinishAllItems(launchId).then(() => { + this.reportportal.finishLaunch(launchId, { + ...(this.customLaunchStatus && { status: this.customLaunchStatus }), + }); + this.storage.setLaunchTempId(null); + this.storage.setCurrentFeatureUri(null); + this.storage.setFeatureTempId(null); + this.customLaunchStatus = null; + }); } }; -}; module.exports = { createRPFormatterClass }; diff --git a/modules/utils.js b/modules/utils.js index d394511..bd755ca 100644 --- a/modules/utils.js +++ b/modules/utils.js @@ -69,23 +69,10 @@ const isAllRuleChildrenStarted = (ruleChildrenIds, startedRuleChildrenIds) => ruleChildrenIds.every((childId) => startedRuleChildrenIds.has(childId)); const findScenario = (node, searchId) => { - const children = node.children.find((child) => { - if (child.scenario) { - return child.scenario.id === searchId; - } - return null; - }); + const children = node.children.find((child) => child.scenario && child.scenario.id === searchId); return children.scenario; }; -const bindToClass = (module, thisClass) => { - const that = thisClass; - Object.entries(module).forEach((method) => { - const [key, value] = method; - that[key] = value.bind(that); - }); -}; - const collectParams = ({ tableHeader, tableBody }) => { const { cells: headerCells } = tableHeader; return tableBody.reduce((map, row) => { @@ -134,7 +121,6 @@ module.exports = { findNode, findScenario, isAllRuleChildrenStarted, - bindToClass, collectParams, findAstNodesData, getScreenshotName, diff --git a/package-lock.json b/package-lock.json index 56c1211..a1d153e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "Apache-2.0", "dependencies": { "@reportportal/client-javascript": "^5.1.0", - "cli-table3": "^0.6.3", "strip-ansi": "^6.0.1" }, "devDependencies": { @@ -30,8 +29,7 @@ "node": ">=12.x" }, "peerDependencies": { - "@cucumber/cucumber": "7.x - 10.x", - "cucumber": "4.x - 6.x" + "@cucumber/cucumber": "7.x - 10.x" } }, "../client-javascript": { @@ -689,19 +687,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.23.8", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.23.8.tgz", - "integrity": "sha512-2ZzmcDugdm0/YQKFVYsXiwUN7USPX8PM7cytpb4PFl87fM+qYPSvTZX//8tyeJB1j0YDmafBJEbl5f8NfLyuKw==", - "peer": true, - "dependencies": { - "core-js-pure": "^3.30.2", - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", @@ -770,6 +755,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, "optional": true, "engines": { "node": ">=0.1.90" @@ -1985,7 +1971,8 @@ "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", - "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true }, "node_modules/anymatch": { "version": "3.1.3", @@ -2095,19 +2082,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "peer": true, - "engines": { - "node": ">=0.8" - } - }, "node_modules/assertion-error-formatter": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/assertion-error-formatter/-/assertion-error-formatter-3.0.0.tgz", "integrity": "sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==", + "dev": true, "dependencies": { "diff": "^4.0.1", "pad-right": "^0.2.2", @@ -2273,18 +2252,6 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, - "node_modules/becke-ch--regex--s0-0-v1--base--pl--lib": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/becke-ch--regex--s0-0-v1--base--pl--lib/-/becke-ch--regex--s0-0-v1--base--pl--lib-1.4.0.tgz", - "integrity": "sha512-FnWonOyaw7Vivg5nIkrUll9HSS5TjFbyuURAiDssuL6VxrBe3ERzudRxOcWRhZYlP89UArMDikz7SapRPQpmZQ==", - "peer": true - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "peer": true - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2484,6 +2451,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "dev": true, "dependencies": { "string-width": "^4.2.0" }, @@ -2542,15 +2510,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "node_modules/colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", - "peer": true, - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -2588,23 +2547,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, - "node_modules/core-js-pure": { - "version": "3.35.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.35.0.tgz", - "integrity": "sha512-f+eRYmkou59uh7BPcyJ8MC76DiGhspj1KMxVIcF24tzP8NA9HVa1uC7BTW2tgx7E1QVCzDzsgp7kArrzhlz8Ew==", - "hasInstallScript": true, - "peer": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "peer": true - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", @@ -2640,149 +2582,6 @@ "node": ">= 8" } }, - "node_modules/cucumber": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/cucumber/-/cucumber-6.0.7.tgz", - "integrity": "sha512-pN3AgWxHx8rOi+wOlqjASNETOjf3TgeyqhMNLQam7nSTXgQzju1oAmXkleRQRcXvpVvejcDHiZBLFSfBkqbYpA==", - "deprecated": "Cucumber is publishing new releases under @cucumber/cucumber", - "peer": true, - "dependencies": { - "assertion-error-formatter": "^3.0.0", - "bluebird": "^3.4.1", - "cli-table3": "^0.5.1", - "colors": "^1.1.2", - "commander": "^3.0.1", - "cucumber-expressions": "^8.1.0", - "cucumber-tag-expressions": "^2.0.2", - "duration": "^0.2.1", - "escape-string-regexp": "^2.0.0", - "figures": "^3.0.0", - "gherkin": "5.0.0", - "glob": "^7.1.3", - "indent-string": "^4.0.0", - "is-generator": "^1.0.2", - "is-stream": "^2.0.0", - "knuth-shuffle-seeded": "^1.0.6", - "lodash": "^4.17.14", - "mz": "^2.4.0", - "progress": "^2.0.0", - "resolve": "^1.3.3", - "serialize-error": "^4.1.0", - "stack-chain": "^2.0.0", - "stacktrace-js": "^2.0.0", - "string-argv": "^0.3.0", - "title-case": "^2.1.1", - "util-arity": "^1.0.2", - "verror": "^1.9.0" - }, - "bin": { - "cucumber-js": "bin/cucumber-js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cucumber-expressions": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/cucumber-expressions/-/cucumber-expressions-8.3.0.tgz", - "integrity": "sha512-cP2ya0EiorwXBC7Ll7Cj7NELYbasNv9Ty42L4u7sso9KruWemWG1ZiTq4PMqir3SNDSrbykoqI5wZgMbLEDjLQ==", - "deprecated": "This package is now published under @cucumber/cucumber-expressions", - "peer": true, - "dependencies": { - "becke-ch--regex--s0-0-v1--base--pl--lib": "^1.4.0", - "xregexp": "^4.2.4" - } - }, - "node_modules/cucumber-tag-expressions": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/cucumber-tag-expressions/-/cucumber-tag-expressions-2.0.3.tgz", - "integrity": "sha512-+x5j1IfZrBtbvYHuoUX0rl4nUGxaey6Do9sM0CABmZfDCcWXuuRm1fQeCaklIYQgOFHQ6xOHvDSdkMHHpni6tQ==", - "peer": true - }, - "node_modules/cucumber/node_modules/ansi-regex": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", - "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/cucumber/node_modules/cli-table3": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.5.1.tgz", - "integrity": "sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==", - "peer": true, - "dependencies": { - "object-assign": "^4.1.0", - "string-width": "^2.1.1" - }, - "engines": { - "node": ">=6" - }, - "optionalDependencies": { - "colors": "^1.1.2" - } - }, - "node_modules/cucumber/node_modules/commander": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", - "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", - "peer": true - }, - "node_modules/cucumber/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cucumber/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/cucumber/node_modules/string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "peer": true, - "dependencies": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cucumber/node_modules/strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", - "peer": true, - "dependencies": { - "ansi-regex": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/d": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", - "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "peer": true, - "dependencies": { - "es5-ext": "^0.10.50", - "type": "^1.0.1" - } - }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -2881,6 +2680,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, "engines": { "node": ">=0.3.1" } @@ -2906,16 +2706,6 @@ "node": ">=6.0.0" } }, - "node_modules/duration": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/duration/-/duration-0.2.2.tgz", - "integrity": "sha512-06kgtea+bGreF5eKYgI/36A6pLXggY7oR4p1pq4SmdFBn1ReOL5D8RhG64VrqfTTKNucqqtBAwEj8aB88mcqrg==", - "peer": true, - "dependencies": { - "d": "1", - "es5-ext": "~0.10.46" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2943,7 +2733,8 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true }, "node_modules/error-ex": { "version": "1.3.2", @@ -2958,6 +2749,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dev": true, "dependencies": { "stackframe": "^1.3.4" } @@ -3055,42 +2847,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es5-ext": { - "version": "0.10.62", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", - "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", - "hasInstallScript": true, - "peer": true, - "dependencies": { - "es6-iterator": "^2.0.3", - "es6-symbol": "^3.1.3", - "next-tick": "^1.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", - "peer": true, - "dependencies": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "node_modules/es6-symbol": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", - "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "peer": true, - "dependencies": { - "d": "^1.0.1", - "ext": "^1.1.2" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -3104,6 +2860,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, "engines": { "node": ">=0.8.0" } @@ -3569,30 +3326,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/ext": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", - "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", - "peer": true, - "dependencies": { - "type": "^2.7.2" - } - }, - "node_modules/ext/node_modules/type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", - "peer": true - }, - "node_modules/extsprintf": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", - "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", - "engines": [ - "node >=0.6.0" - ], - "peer": true - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3639,6 +3372,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, "dependencies": { "escape-string-regexp": "^1.0.5" }, @@ -3801,6 +3535,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3902,16 +3637,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gherkin": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/gherkin/-/gherkin-5.0.0.tgz", - "integrity": "sha512-Y+93z2Nh+TNIKuKEf+6M0FQrX/z0Yv9C2LFfc5NlcGJWRrrTeI/jOg2374y1FOw6ZYQ3RgJBezRkli7CLDubDA==", - "deprecated": "This package is now published under @cucumber/gherkin", - "peer": true, - "bin": { - "gherkin-javascript": "bin/gherkin" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4106,6 +3831,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -4200,6 +3926,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, "engines": { "node": ">=8" } @@ -4304,6 +4031,7 @@ "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "dev": true, "dependencies": { "hasown": "^2.0.0" }, @@ -4339,16 +4067,11 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "engines": { "node": ">=8" } }, - "node_modules/is-generator": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-generator/-/is-generator-1.0.3.tgz", - "integrity": "sha512-G56jBpbJeg7ds83HW1LuShNs8J73Fv3CPz/bmROHOHlnKkN8sWb9ujiagjmxxMUywftgq48HlBZELKKqFLk0oA==", - "peer": true - }, "node_modules/is-generator-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", @@ -4474,6 +4197,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "engines": { "node": ">=8" }, @@ -5308,6 +5032,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/knuth-shuffle-seeded/-/knuth-shuffle-seeded-1.0.6.tgz", "integrity": "sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg==", + "dev": true, "dependencies": { "seed-random": "~2.2.0" } @@ -5355,12 +5080,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "peer": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5528,6 +5247,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -5540,12 +5260,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/next-tick": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", - "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==", - "peer": true - }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -5614,6 +5328,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -5768,6 +5483,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/pad-right/-/pad-right-0.2.2.tgz", "integrity": "sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==", + "dev": true, "dependencies": { "repeat-string": "^1.5.2" }, @@ -5834,7 +5550,8 @@ "node_modules/path-parse": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true }, "node_modules/path-scurry": { "version": "1.10.1", @@ -6018,6 +5735,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -6205,12 +5923,6 @@ "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", "dev": true }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "peer": true - }, "node_modules/regexp-match-indices": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/regexp-match-indices/-/regexp-match-indices-1.0.2.tgz", @@ -6262,6 +5974,7 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, "engines": { "node": ">=0.10" } @@ -6279,6 +5992,7 @@ "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -6416,7 +6130,8 @@ "node_modules/seed-random": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", - "integrity": "sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==" + "integrity": "sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==", + "dev": true }, "node_modules/semver": { "version": "7.5.3", @@ -6433,27 +6148,6 @@ "node": ">=10" } }, - "node_modules/serialize-error": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-4.1.0.tgz", - "integrity": "sha512-5j9GgyGsP9vV9Uj1S0lDCvlsd+gc2LEPVK7HHHte7IyPwOD4lVQFeaX143gx3U5AnoCi+wbcb3mvaxVysjpxEw==", - "peer": true, - "dependencies": { - "type-fest": "^0.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/serialize-error/node_modules/type-fest": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", - "integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==", - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/set-function-length": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", @@ -6596,21 +6290,6 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, - "node_modules/stack-chain": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-2.0.0.tgz", - "integrity": "sha512-GGrHXePi305aW7XQweYZZwiRwR7Js3MWoK/EHzzB9ROdc75nCnjSJVi21rdAGxFl+yCx2L2qdfl5y7NO4lTyqg==", - "peer": true - }, - "node_modules/stack-generator": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", - "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", - "peer": true, - "dependencies": { - "stackframe": "^1.3.4" - } - }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -6635,46 +6314,8 @@ "node_modules/stackframe": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", - "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" - }, - "node_modules/stacktrace-gps": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", - "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", - "peer": true, - "dependencies": { - "source-map": "0.5.6", - "stackframe": "^1.3.4" - } - }, - "node_modules/stacktrace-gps/node_modules/source-map": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stacktrace-js": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", - "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", - "peer": true, - "dependencies": { - "error-stack-parser": "^2.0.6", - "stack-generator": "^2.0.5", - "stacktrace-gps": "^3.0.4" - } - }, - "node_modules/string-argv": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", - "peer": true, - "engines": { - "node": ">=0.6.19" - } + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "dev": true }, "node_modules/string-length": { "version": "4.0.2", @@ -6693,6 +6334,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -6852,6 +6494,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "engines": { "node": ">= 0.4" }, @@ -6883,6 +6526,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "dependencies": { "any-promise": "^1.0.0" } @@ -6891,6 +6535,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -6904,31 +6549,6 @@ "integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==", "dev": true }, - "node_modules/title-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/title-case/-/title-case-2.1.1.tgz", - "integrity": "sha512-EkJoZ2O3zdCz3zJsYCsxyq2OC5hrxR9mfdd5I+w8h/tmFfeOxJ+vvkxsKxdmN0WtS9zLdHEgfgVOiMVgv+Po4Q==", - "peer": true, - "dependencies": { - "no-case": "^2.2.0", - "upper-case": "^1.0.3" - } - }, - "node_modules/title-case/node_modules/lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", - "peer": true - }, - "node_modules/title-case/node_modules/no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", - "peer": true, - "dependencies": { - "lower-case": "^1.1.1" - } - }, "node_modules/tmp": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", @@ -6992,12 +6612,6 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, - "node_modules/type": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", - "peer": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -7152,12 +6766,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", - "peer": true - }, "node_modules/upper-case-first": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", @@ -7179,7 +6787,8 @@ "node_modules/util-arity": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/util-arity/-/util-arity-1.1.0.tgz", - "integrity": "sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA==" + "integrity": "sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA==", + "dev": true }, "node_modules/uuid": { "version": "9.0.0", @@ -7214,20 +6823,6 @@ "spdx-expression-parse": "^3.0.0" } }, - "node_modules/verror": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", - "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", - "peer": true, - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -7349,15 +6944,6 @@ "node": ">=8.0" } }, - "node_modules/xregexp": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.4.1.tgz", - "integrity": "sha512-2u9HwfadaJaY9zHtRRnH6BY6CQVNQKkYm3oLtC9gJXXzfsbACg5X5e4EZZGVAH+YIfa+QA9lsFQTTe3HURF3ag==", - "peer": true, - "dependencies": { - "@babel/runtime-corejs3": "^7.12.1" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 0a4cb17..44b836e 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ }, "dependencies": { "@reportportal/client-javascript": "^5.1.0", - "cli-table3": "^0.6.3", "strip-ansi": "^6.0.1" }, "engines": { @@ -31,8 +30,7 @@ "prettier": "^2.8.8" }, "peerDependencies": { - "@cucumber/cucumber": "7.x - 10.x", - "cucumber": "4.x - 6.x" + "@cucumber/cucumber": "7.x - 10.x" }, "repository": { "type": "git", diff --git a/tests/cucumber-reportportal-formatter.spec.js b/tests/cucumber-reportportal-formatter.spec.js index 464adcf..90648cf 100644 --- a/tests/cucumber-reportportal-formatter.spec.js +++ b/tests/cucumber-reportportal-formatter.spec.js @@ -21,6 +21,11 @@ const { gherkinDocument, uri, pickle, + pickleWithParameters, + testCaseWithParameters, + ruleId, + ruleTempId, + hook, testCase, testCaseStarted, testCaseId, @@ -34,7 +39,15 @@ const { scenario, step, } = require('./data'); -const { STATUSES, TEST_ITEM_TYPES } = require('../modules/constants'); +const { + STATUSES, + TEST_ITEM_TYPES, + CUCUMBER_MESSAGES, + RP_EVENTS, + RP_ENTITY_LAUNCH, + TEST_STEP_FINISHED_RP_MESSAGES, + LOG_LEVELS, +} = require('../modules/constants'); describe('cucumber-reportportal-formatter', () => { const config = getDefaultConfig(); @@ -55,6 +68,113 @@ describe('cucumber-reportportal-formatter', () => { formatter.codeRefIndexesMap.clear(); }); + describe('eventHandler', () => { + it('onGherkinDocumentEvent should be called', () => { + const spyOnGherkinDocumentEvent = jest + .spyOn(formatter, 'onGherkinDocumentEvent') + .mockImplementationOnce(() => {}); + formatter.eventHandler({ [CUCUMBER_MESSAGES.GHERKIN_DOCUMENT]: gherkinDocument }); + + expect(spyOnGherkinDocumentEvent).toBeCalledWith(gherkinDocument); + }); + + it('onPickleEvent should be called', () => { + const spyOnPickleEvent = jest + .spyOn(formatter, 'onPickleEvent') + .mockImplementationOnce(() => {}); + formatter.eventHandler({ [CUCUMBER_MESSAGES.PICKLE]: pickle }); + + expect(spyOnPickleEvent).toBeCalledWith(pickle); + }); + + it('onHookEvent should be called', () => { + const spyOnHookEvent = jest.spyOn(formatter, 'onHookEvent').mockImplementationOnce(() => {}); + formatter.eventHandler({ [CUCUMBER_MESSAGES.HOOK]: hook }); + + expect(spyOnHookEvent).toBeCalledWith(hook); + }); + + it('onTestRunStartedEvent should be called', () => { + const spyOnTestRunStartedEvent = jest + .spyOn(formatter, 'onTestRunStartedEvent') + .mockImplementationOnce(() => {}); + formatter.eventHandler({ [CUCUMBER_MESSAGES.TEST_RUN_STARTED]: {} }); + + expect(spyOnTestRunStartedEvent).toBeCalled(); + }); + + it('onTestCaseEvent should be called', () => { + const spyOnTestCaseEvent = jest + .spyOn(formatter, 'onTestCaseEvent') + .mockImplementationOnce(() => {}); + formatter.eventHandler({ [CUCUMBER_MESSAGES.TEST_CASE]: testCase }); + + expect(spyOnTestCaseEvent).toBeCalledWith(testCase); + }); + + it('onTestCaseStartedEvent should be called', () => { + const spyOnTestCaseStartedEvent = jest + .spyOn(formatter, 'onTestCaseStartedEvent') + .mockImplementationOnce(() => {}); + formatter.eventHandler({ [CUCUMBER_MESSAGES.TEST_CASE_STARTED]: testCaseStarted }); + + expect(spyOnTestCaseStartedEvent).toBeCalledWith(testCaseStarted); + }); + + it('onTestStepStartedEvent should be called', () => { + const spyOnTestStepStartedEvent = jest + .spyOn(formatter, 'onTestStepStartedEvent') + .mockImplementationOnce(() => {}); + formatter.eventHandler({ [CUCUMBER_MESSAGES.TEST_STEP_STARTED]: testStepStarted }); + + expect(spyOnTestStepStartedEvent).toBeCalledWith(testStepStarted); + }); + + it('onTestStepAttachmentEvent should be called', () => { + const testData = { id: 'test' }; + const spyOnTestStepAttachmentEvent = jest + .spyOn(formatter, 'onTestStepAttachmentEvent') + .mockImplementationOnce(() => {}); + formatter.eventHandler({ [CUCUMBER_MESSAGES.ATTACHMENT]: testData }); + + expect(spyOnTestStepAttachmentEvent).toBeCalledWith(testData); + }); + + it('onTestStepFinishedEvent should be called', () => { + const spyOnTestStepFinishedEvent = jest + .spyOn(formatter, 'onTestStepFinishedEvent') + .mockImplementationOnce(() => {}); + formatter.eventHandler({ [CUCUMBER_MESSAGES.TEST_STEP_FINISHED]: testStepFinished }); + + expect(spyOnTestStepFinishedEvent).toBeCalledWith(testStepFinished); + }); + + it('onTestCaseFinishedEvent should be called', () => { + const spyOnTestCaseFinishedEvent = jest + .spyOn(formatter, 'onTestCaseFinishedEvent') + .mockImplementationOnce(() => {}); + formatter.eventHandler({ [CUCUMBER_MESSAGES.TEST_CASE_FINISHED]: testCaseFinished }); + + expect(spyOnTestCaseFinishedEvent).toBeCalledWith(testCaseFinished); + }); + + it('onTestRunFinishedEvent should be called', () => { + const testData = { id: 'test' }; + const spyOnTestRunFinishedEvent = jest + .spyOn(formatter, 'onTestRunFinishedEvent') + .mockImplementationOnce(() => {}); + formatter.eventHandler({ [CUCUMBER_MESSAGES.TEST_RUN_FINISHED]: testData }); + + expect(spyOnTestRunFinishedEvent).toBeCalledWith(testData); + }); + + it('should return null if an unexpected event is received', () => { + const result = formatter.eventHandler({ 'unexpected-event': {} }); + + expect(result).toBeNull(); + }); + }); + describe('onGherkinDocumentEvent', () => { beforeEach(() => { formatter.onGherkinDocumentEvent(gherkinDocument); @@ -68,6 +188,14 @@ describe('cucumber-reportportal-formatter', () => { }); }); + describe('onHookEvent', () => { + it('should set hook to storage', () => { + formatter.onHookEvent(hook); + + expect(formatter.storage.getHook(hook.id)).toBe(hook); + }); + }); + describe('onPickleEvent', () => { it('should set pickle to storage', () => { formatter.onPickleEvent(pickle); @@ -82,6 +210,24 @@ describe('cucumber-reportportal-formatter', () => { expect(formatter.storage.getLaunchTempId()).toBe('tempLaunchId'); }); + + it('startLaunch should be called with skippedIssue attribute', () => { + const tempId = 'test-temp-id'; + const spyStartLaunch = jest.spyOn(formatter.reportportal, 'startLaunch'); + const { skippedIssue } = formatter.config; + + formatter.config.skippedIssue = false; + formatter.onTestRunStartedEvent(); + formatter.config.skippedIssue = skippedIssue; + + expect(spyStartLaunch).toHaveBeenCalledWith( + expect.objectContaining({ + attributes: expect.arrayContaining([ + { key: 'skippedIssue', value: 'false', system: true }, + ]), + }), + ); + }); }); describe('onTestCaseEvent', () => { @@ -97,12 +243,25 @@ describe('cucumber-reportportal-formatter', () => { expect(formatter.storage.getStep(testCase.id, testStepId)).toEqual(expectedRes); }); + + it('should set Before Hook to storage under testCaseId', () => { + const data = { ...testCase, testSteps: [{ hookId: hook.id, id: testStepId }] }; + formatter.onGherkinDocumentEvent(gherkinDocument); + formatter.storage.setHook(hook.id, hook); + + formatter.onTestCaseEvent(data); + + expect(formatter.storage.getStep(testCase.id, testStepId)).toEqual({ + text: hook.name, + type: TEST_ITEM_TYPES.BEFORE_TEST, + }); + }); }); describe('onTestCaseStartedEvent', () => { beforeEach(() => { formatter.onGherkinDocumentEvent(gherkinDocument); - formatter.onPickleEvent(pickle); + formatter.onPickleEvent(pickleWithParameters); formatter.onTestRunStartedEvent(); formatter.onTestCaseEvent(testCase); }); @@ -180,19 +339,33 @@ describe('cucumber-reportportal-formatter', () => { type: 'TEST', codeRef: `${uri}/${feature.name}/${scenario.name}`, retry: false, + parameters: [ + { + key: scenario.examples[0].tableHeader.cells[0].value, + value: scenario.examples[0].tableBody[0].cells[0].value, + }, + ], }, 'tempLaunchId', 'testItemId', ); }); + + it('should set isRetry for test case in storage if attempt > 0', () => { + const spyStartTestItem = jest.spyOn(formatter.reportportal, 'startTestItem'); + + formatter.onTestCaseStartedEvent({ ...testCaseStarted, attempt: 1 }); + + expect(formatter.storage.getTestCase(testCaseId).isRetry).toEqual(true); + }); }); describe('onTestStepStartedEvent', () => { beforeEach(() => { formatter.onGherkinDocumentEvent(gherkinDocument); - formatter.onPickleEvent(pickle); + formatter.onPickleEvent(pickleWithParameters); formatter.onTestRunStartedEvent(); - formatter.onTestCaseEvent(testCase); + formatter.onTestCaseEvent(testCaseWithParameters); formatter.onTestCaseStartedEvent(testCaseStarted); }); @@ -207,6 +380,12 @@ describe('cucumber-reportportal-formatter', () => { startTime: mockedDate, type: 'STEP', codeRef: `${uri}/${feature.name}/${scenario.name}/${step.text}`, + parameters: [ + { + key: scenario.examples[0].tableHeader.cells[0].value, + value: scenario.examples[0].tableBody[0].cells[0].value, + }, + ], hasStats: true, retry: false, }, @@ -216,6 +395,248 @@ describe('cucumber-reportportal-formatter', () => { }); }); + describe('onTestStepAttachmentEvent', () => { + const launchTempId = 'launch-temp-id'; + const stepTempId = 'step-temp-id'; + const body = { + entity: 'test-entity', + status: 'test-status', + description: 'test-description', + level: 'test-level', + message: 'test-message', + data: 'test-data', + attributes: ['attributes1', 'attributes2', 'attributes3'], + }; + + beforeEach(() => { + formatter.storage.setTestCaseStartedId(testCaseStartedId, testCase.id); + formatter.storage.setSteps(testCase.id, { + [testStepId]: {}, + }); + }); + + afterEach(() => { + formatter.storage.removeTestCaseStartedId(testCaseStartedId); + formatter.storage.removeSteps(testCase.id); + }); + + it('should update step in storage', () => { + const data = { + mediaType: RP_EVENTS.TEST_CASE_ID, + testStepId, + testCaseStartedId, + body: JSON.stringify(body), + }; + + formatter.onTestStepAttachmentEvent(data); + + expect(formatter.storage.getStep(testCaseId, testStepId)).toEqual(body); + }); + + it('should update step attributes in storage', () => { + const data = { + mediaType: RP_EVENTS.ATTRIBUTES, + testStepId, + testCaseStartedId, + body: JSON.stringify(body), + }; + + formatter.onTestStepAttachmentEvent(data); + + expect(formatter.storage.getStep(testCaseId, testStepId)).toEqual({ + attributes: body.attributes, + }); + }); + + it('should update step description in storage', () => { + const data = { + mediaType: RP_EVENTS.DESCRIPTION, + testStepId, + testCaseStartedId, + body: JSON.stringify(body), + }; + + formatter.onTestStepAttachmentEvent(data); + + expect(formatter.storage.getStep(testCaseId, testStepId)).toEqual({ + description: body.description, + }); + }); + + it('should update step description in storage', () => { + const data = { + mediaType: RP_EVENTS.DESCRIPTION, + testStepId, + testCaseStartedId, + body: JSON.stringify(body), + }; + + formatter.onTestStepAttachmentEvent(data); + + expect(formatter.storage.getStep(testCaseId, testStepId)).toEqual({ + description: body.description, + }); + }); + + describe('Status event handling', () => { + it('should update step in storage', () => { + const data = { + mediaType: RP_EVENTS.STATUS, + testStepId, + testCaseStartedId, + body: JSON.stringify(body), + }; + + formatter.onTestStepAttachmentEvent(data); + + expect(formatter.storage.getStep(testCaseId, testStepId)).toEqual(body); + }); + + it('should set status in customLaunchStatus field', () => { + const status = 'start'; + const data = { + mediaType: RP_EVENTS.STATUS, + testStepId, + testCaseStartedId, + body: JSON.stringify({ ...body, entity: RP_ENTITY_LAUNCH, status }), + }; + + formatter.onTestStepAttachmentEvent(data); + + expect(formatter.customLaunchStatus).toEqual(status); + }); + }); + + describe('text/plain event handling', () => { + it('sendLog should be called', () => { + jest.spyOn(formatter.storage, 'getStepTempId').mockImplementationOnce(() => stepTempId); + const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); + + const data = { + mediaType: 'text/plain', + testStepId, + testCaseStartedId, + body: JSON.stringify(body), + }; + + formatter.onTestStepAttachmentEvent(data); + + expect(spySendLog).toHaveBeenCalledWith(stepTempId, { + time: mockedDate, + level: body.level, + message: body.message, + }); + }); + + it('sendLog should be called with launch temp id', () => { + jest.spyOn(formatter.storage, 'getLaunchTempId').mockImplementationOnce(() => launchTempId); + const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); + + const data = { + mediaType: 'text/plain', + testStepId, + testCaseStartedId, + body: JSON.stringify({ ...body, entity: RP_ENTITY_LAUNCH }), + }; + + formatter.onTestStepAttachmentEvent(data); + + expect(spySendLog).toHaveBeenCalledWith(launchTempId, { + time: mockedDate, + level: body.level, + message: body.message, + }); + }); + + it('sendLog should be called with body like message and debug level', () => { + jest.spyOn(formatter.storage, 'getStepTempId').mockImplementationOnce(() => stepTempId); + const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); + + const data = { + mediaType: 'text/plain', + testStepId, + testCaseStartedId, + body: 'not-json-body', + }; + + formatter.onTestStepAttachmentEvent(data); + + expect(spySendLog).toHaveBeenCalledWith(stepTempId, { + time: mockedDate, + level: LOG_LEVELS.DEBUG, + message: data.body, + }); + }); + }); + + describe('default handling', () => { + const fileName = 'file'; + const mediaType = 'unexpected-media-type'; + + it('sendLog should be called', () => { + jest.spyOn(formatter.storage, 'getStepTempId').mockImplementationOnce(() => stepTempId); + const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); + + const data = { + mediaType, + testStepId, + testCaseStartedId, + body: JSON.stringify(body), + }; + + formatter.onTestStepAttachmentEvent(data); + + expect(spySendLog).toHaveBeenCalledWith( + stepTempId, + { + time: mockedDate, + level: body.level, + message: body.message, + file: { + name: body.message, + }, + }, + { + name: fileName, + type: mediaType, + content: body.data, + }, + ); + }); + + it('sendLog should be called with launch temp id', () => { + jest.spyOn(formatter.storage, 'getLaunchTempId').mockImplementationOnce(() => launchTempId); + const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); + + const data = { + mediaType, + testStepId, + testCaseStartedId, + body: JSON.stringify({ ...body, entity: RP_ENTITY_LAUNCH }), + }; + + formatter.onTestStepAttachmentEvent(data); + + expect(spySendLog).toHaveBeenCalledWith( + launchTempId, + { + time: mockedDate, + level: body.level, + message: body.message, + file: { + name: body.message, + }, + }, + { + name: fileName, + type: mediaType, + content: body.data, + }, + ); + }); + }); + }); + describe('onTestStepFinishedEvent', () => { beforeEach(() => { formatter.onGherkinDocumentEvent(gherkinDocument); @@ -226,23 +647,153 @@ describe('cucumber-reportportal-formatter', () => { formatter.onTestStepStartedEvent(testStepStarted); }); - it('finishTestItem should be called, clean storage', () => { - const spyFinishTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); + describe('finishsed with failed status', () => { + const originBrowser = global.browser; + const png = 'base64-data'; - formatter.onTestStepFinishedEvent(testStepFinished); + beforeAll(() => { + global.browser = { + takeScreenshot: jest.fn().mockImplementation(() => Promise.resolve(png)), + }; + }); - expect(spyFinishTestItem).toBeCalledWith('testItemId', { - endTime: mockedDate, - status: STATUSES.FAILED, - description: '```error\nerror message\n```', + afterAll(() => { + global.browser = originBrowser; + }); + + it('finishTestItem should be called with failed status, clean storage', () => { + const spyFinishTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); + + formatter.onTestStepFinishedEvent(testStepFinished); + + expect(spyFinishTestItem).toBeCalledWith('testItemId', { + endTime: mockedDate, + status: STATUSES.FAILED, + description: '```error\nerror message\n```', + }); + expect(formatter.storage.getStepTempId(testStepStarted.testStepId)).toBeUndefined(); }); + + it('should make screenshot if takeScreenshot === onFailure', () => { + const originTakeScreenshot = formatter.config.takeScreenshot; + + const spyTakeScreenshot = jest.spyOn(global.browser, 'takeScreenshot'); + + formatter.config.takeScreenshot = 'onFailure'; + formatter.onTestStepFinishedEvent(testStepFinished); + formatter.config.takeScreenshot = originTakeScreenshot; + + expect(spyTakeScreenshot).toBeCalled(); + }); + }); + + it('finishTestItem should be called with passed status, clean storage', () => { + const spyfinishTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); + + formatter.onTestStepFinishedEvent({ + ...testStepFinished, + testStepResult: { ...testStepFinished.testStepResult, status: STATUSES.PASSED }, + }); + + expect(spyfinishTestItem).toBeCalledWith( + 'testItemId', + expect.objectContaining({ + status: STATUSES.PASSED, + }), + ); expect(formatter.storage.getStepTempId(testStepStarted.testStepId)).toBeUndefined(); }); + + it('finishTestItem should be called with unexpected status', () => { + const spyfinishTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); + + formatter.onTestStepFinishedEvent({ + ...testStepFinished, + testStepResult: { ...testStepFinished.testStepResult, status: 'unexpected-status' }, + }); + + expect(spyfinishTestItem).toBeCalledWith( + 'testItemId', + expect.objectContaining({ + status: 'unexpected-status', + }), + ); + }); + + it('finishTestItem should be called with NOT_ISSUE type if SKIPPED status and skippedIssue set to false', () => { + const spyfinishTestItem = jest.spyOn(formatter.reportportal, 'finishTestItem'); + + formatter.config.skippedIssue = false; + formatter.onTestStepFinishedEvent({ + ...testStepFinished, + testStepResult: { ...testStepFinished.testStepResult, status: STATUSES.SKIPPED }, + }); + + expect(spyfinishTestItem).toBeCalledWith( + 'testItemId', + expect.objectContaining({ status: STATUSES.SKIPPED, issue: { issueType: 'NOT_ISSUE' } }), + ); + }); + + it('should set failed status for test case in storage if isScenarioBasedStatistics === true', () => { + const originIsScenarioBasedStatistics = formatter.isScenarioBasedStatistics; + formatter.isScenarioBasedStatistics = true; + + formatter.onTestStepFinishedEvent(testStepFinished); + formatter.isScenarioBasedStatistics = originIsScenarioBasedStatistics; + + expect(formatter.storage.getTestCase(testCaseId).status).toEqual(STATUSES.FAILED); + }); + + it('sendLog should be called with pending message in case of corresponding status received', () => { + const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); + + formatter.onTestStepFinishedEvent({ + ...testStepFinished, + testStepResult: { ...testStepFinished.testStepResult, status: STATUSES.PENDING }, + }); + + expect(spySendLog).toBeCalledWith('testItemId', { + time: mockedDate, + level: LOG_LEVELS.WARN, + message: TEST_STEP_FINISHED_RP_MESSAGES.PENDING, + }); + }); + + it('sendLog should be called with undefined message in case of corresponding status received', () => { + const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); + + formatter.onTestStepFinishedEvent({ + ...testStepFinished, + testStepResult: { ...testStepFinished.testStepResult, status: STATUSES.UNDEFINED }, + }); + + expect(spySendLog).toBeCalledWith('testItemId', { + time: mockedDate, + level: LOG_LEVELS.ERROR, + message: TEST_STEP_FINISHED_RP_MESSAGES.UNDEFINED, + }); + }); + + it('sendLog should be called with ambiguous message in case of corresponding status received', () => { + const spySendLog = jest.spyOn(formatter.reportportal, 'sendLog'); + + formatter.onTestStepFinishedEvent({ + ...testStepFinished, + testStepResult: { ...testStepFinished.testStepResult, status: STATUSES.AMBIGUOUS }, + }); + + expect(spySendLog).toBeCalledWith('testItemId', { + time: mockedDate, + level: LOG_LEVELS.ERROR, + message: TEST_STEP_FINISHED_RP_MESSAGES.AMBIGUOUS, + }); + }); }); describe('onTestCaseFinishedEvent', () => { beforeEach(() => { - formatter.onGherkinDocumentEvent(gherkinDocument); + formatter.onGherkinDocumentEvent(gherkinDocumentWithRule); formatter.onPickleEvent(pickle); formatter.onTestRunStartedEvent(); formatter.onTestCaseEvent(testCase); diff --git a/tests/data.js b/tests/data.js index e19dea1..086312e 100644 --- a/tests/data.js +++ b/tests/data.js @@ -17,12 +17,19 @@ const launchTempId = 'tempId'; const uri = 'features/statuses/statuses.feature'; const scenarioId = '1957ea93-e4de-4895-86e8-acb857b5b069'; +const parameters = 'row-cell-id'; const ruleId = '2034eaf4-f7hv-8234-55l4-njk687k3k423'; const scenario = { id: scenarioId, name: 'scenario name', keyword: 'Scenario', steps: [{ id: 'scenarioStepsId', keyword: 'Then', location: { line: 7, column: 5 } }], + examples: [ + { + tableHeader: { cells: [{ value: 'header-cells-value1' }] }, + tableBody: [{ id: parameters, cells: [{ value: 'row-cell-value1' }] }], + }, + ], }; const feature = { keyword: 'Feature', @@ -51,6 +58,12 @@ const step = { argument: undefined, astNodeIds: [], }; +const stepWithParameters = { + id: stepId, + text: 'I put "true"', + argument: undefined, + astNodeIds: [scenarioId, parameters], +}; const pickleId = 'c544ae8c-f080-41be-a612-f3000ac46565'; const pickle = { id: pickleId, @@ -59,6 +72,13 @@ const pickle = { name: 'Given and expected value are equal', steps: [step], }; +const pickleWithParameters = { + id: pickleId, + uri: 'features/statuses/statuses.feature', + astNodeIds: [scenarioId, parameters], + name: 'Given and expected value are equal', + steps: [stepWithParameters], +}; const hookId = '2b9c9732-acbd-4fa0-8408-42875800d92e'; const hook = { id: hookId, @@ -76,6 +96,24 @@ const testCase = { }, ], }; +const testCaseWithParameters = { + pickleId, + id: testCaseId, + testSteps: [ + { + id: testStepId, + pickleStepId: stepId, + stepMatchArgumentsLists: [ + { + stepMatchArguments: [ + { group: { value: '-header-cells-value1-' } }, + { group: { value: '-row-cell-value1-' } }, + ], + }, + ], + }, + ], +}; const testCaseStartedId = '57e2a70f-a001-4774-97fa-1e6d5fdb293b'; const testCaseStarted = { testCaseId, @@ -119,6 +157,8 @@ module.exports = { pickleId, uri, pickle, + pickleWithParameters, + testCaseWithParameters, hookId, hook, testCase, diff --git a/tests/mocks.js b/tests/mocks.js index 50788e9..8311130 100644 --- a/tests/mocks.js +++ b/tests/mocks.js @@ -36,7 +36,7 @@ class RPClientMock { } const getDefaultConfig = () => ({ - token: '00000000-0000-0000-0000-000000000000', + apiKey: '00000000-0000-0000-0000-000000000000', endpoint: 'https://reportportal.server/api/v1', project: 'ProjectName', launch: 'LauncherName', diff --git a/tests/utils.spec.js b/tests/utils.spec.js index 325e9b9..9bed7d8 100644 --- a/tests/utils.spec.js +++ b/tests/utils.spec.js @@ -98,20 +98,6 @@ describe('utils', () => { expect(utils.findScenario(node, scenarioId)).toBe(expectedRes); }); - it('bindToClass should add method to class', () => { - const module = { - newMethod() {}, - }; - class Test { - constructor() { - utils.bindToClass(module, this); - } - } - const instance = new Test(); - - expect(instance.newMethod).toBeTruthy(); - }); - it('collectParams should create map from tableHeader & tableBody', () => { const tableHeader = { cells: [{ value: 'parameterKey' }],