Skip to content

Commit

Permalink
EPMRPP-90382 || Implement reporting Gherkin scenario step as nested s…
Browse files Browse the repository at this point in the history
…tep (#189)
  • Loading branch information
AliakseiLiasnitski authored May 6, 2024
1 parent d94e51c commit 31badf2
Show file tree
Hide file tree
Showing 12 changed files with 325 additions and 7 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
### Added
- `cucumberStepStart` and `cucumberStepEnd` commands for reporting `cypress-cucumber-preprocessor` scenario steps as nested steps in RP.
### Security
- Updated versions of vulnerable packages (@reportportal/client-javascript, glob).

Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,64 @@ jobs:

**Note:** The example provided for Cypress version <= 9. For Cypress version >= 10 usage of `cypress-io/github-action` may be changed.

## Cypress-cucumber-preprocessor execution

### Configuration:
Specify the options in the cypress.config.js:

```javascript
const { defineConfig } = require('cypress');
const createBundler = require('@bahmutov/cypress-esbuild-preprocessor');
const preprocessor = require('@badeball/cypress-cucumber-preprocessor');
const createEsbuildPlugin = require('@badeball/cypress-cucumber-preprocessor/esbuild').default;
const registerReportPortalPlugin = require('@reportportal/agent-js-cypress/lib/plugin');

module.exports = defineConfig({
reporter: '@reportportal/agent-js-cypress',
reporterOptions: {
endpoint: 'http://your-instance.com:8080/api/v1',
apiKey: 'reportportalApiKey',
launch: 'LAUNCH_NAME',
project: 'PROJECT_NAME',
description: 'LAUNCH_DESCRIPTION',
},
e2e: {
async setupNodeEvents(on, config) {
await preprocessor.addCucumberPreprocessorPlugin(on, config);
on(
'file:preprocessor',
createBundler({
plugins: [createEsbuildPlugin(config)],
}),
);
registerReportPortalPlugin(on, config);

return config;
},
specPattern: 'cypress/e2e/**/*.feature',
supportFile: 'cypress/support/e2e.js',
},
});
```

### Scenario steps
At the moment it is not possible to subscribe to start and end of scenario steps events. To solve the problem with displaying steps in the ReportPortal, the agent provides special commands: `cucumberStepStart`, `cucumberStepEnd`.
To work correctly, these commands must be called in the `BeforeStep`/`AfterStep` hooks.

```javascript
import { BeforeStep, AfterStep } from '@badeball/cypress-cucumber-preprocessor';

BeforeStep((step) => {
cy.cucumberStepStart(step);
});

AfterStep((step) => {
cy.cucumberStepEnd(step);
});
```

You can avoid duplicating this logic in each step definitions. Instead, add it to the `cypress/support/step_definitions.js` file and include the path to this file in the `stepDefinitions` array (if necessary). These hooks will be used for all step definitions.

# Copyright Notice

Licensed under the [Apache License v2.0](LICENSE)
5 changes: 5 additions & 0 deletions lib/commands/reportPortalCommands.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ declare global {
launchError(message: string, file?: RP_FILE): Chainable<any>;

launchFatal(message: string, file?: RP_FILE): Chainable<any>;
// Waiting for migrate to TypeScript
// Expected step: IStepHookParameter (https://github.com/badeball/cypress-cucumber-preprocessor/blob/055d8df6a62009c94057b0d894a30e142cb87b94/lib/public-member-types.ts#L39)
cucumberStepStart(step: any): Chainable<any>;

cucumberStepEnd(step: any): Chainable<any>;

setStatus(status: RP_STATUS, suiteTitle?: string): Chainable<void>;

Expand Down
11 changes: 11 additions & 0 deletions lib/commands/reportPortalCommands.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,17 @@ Cypress.Commands.add('launchFatal', (message, file) => {
});
});

/**
* Cucumber Scenario's steps commands
*/
Cypress.Commands.add('cucumberStepStart', (step) => {
cy.task('rp_cucumberStepStart', step);
});

Cypress.Commands.add('cucumberStepEnd', (step) => {
cy.task('rp_cucumberStepEnd', step);
});

/**
* Attributes command
*/
Expand Down
9 changes: 9 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,21 @@ const reporterEvents = {
SCREENSHOT: 'screenshot',
SET_STATUS: 'setStatus',
SET_LAUNCH_STATUS: 'setLaunchStatus',
CUCUMBER_STEP_START: 'cucumberStepStart',
CUCUMBER_STEP_END: 'cucumberStepEnd',
};

const cucumberKeywordMap = {
Outcome: 'Then',
Action: 'When',
Context: 'Given',
};

module.exports = {
testItemStatuses,
logLevels,
entityType,
hookTypesMap,
cucumberKeywordMap,
reporterEvents,
};
8 changes: 8 additions & 0 deletions lib/cypressReporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ class CypressReporter extends Mocha.reporters.Base {
this.worker.send({ event: reporterEvents.SET_STATUS, statusInfo });
const setLaunchStatusListener = (statusInfo) =>
this.worker.send({ event: reporterEvents.SET_LAUNCH_STATUS, statusInfo });
const cucumberStepStartListener = (step) =>
this.worker.send({ event: reporterEvents.CUCUMBER_STEP_START, step });
const cucumberStepEndListener = (step) =>
this.worker.send({ event: reporterEvents.CUCUMBER_STEP_END, step });

startIPCServer(
(server) => {
Expand All @@ -93,6 +97,8 @@ class CypressReporter extends Mocha.reporters.Base {
server.on(IPC_EVENTS.SCREENSHOT, screenshotListener);
server.on(IPC_EVENTS.SET_STATUS, setStatusListener);
server.on(IPC_EVENTS.SET_LAUNCH_STATUS, setLaunchStatusListener);
server.on(IPC_EVENTS.CUCUMBER_STEP_START, cucumberStepStartListener);
server.on(IPC_EVENTS.CUCUMBER_STEP_END, cucumberStepEndListener);
},
(server) => {
server.off(IPC_EVENTS.CONFIG, '*');
Expand All @@ -104,6 +110,8 @@ class CypressReporter extends Mocha.reporters.Base {
server.off(IPC_EVENTS.SCREENSHOT, '*');
server.off(IPC_EVENTS.SET_STATUS, '*');
server.off(IPC_EVENTS.SET_LAUNCH_STATUS, '*');
server.off(IPC_EVENTS.CUCUMBER_STEP_START, '*');
server.off(IPC_EVENTS.CUCUMBER_STEP_END, '*');
},
);
CypressReporter.worker = this.worker;
Expand Down
2 changes: 2 additions & 0 deletions lib/ipcEvents.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ const IPC_EVENTS = {
SCREENSHOT: 'screenshot',
SET_STATUS: 'setStatus',
SET_LAUNCH_STATUS: 'setLaunchStatus',
CUCUMBER_STEP_START: 'cucumberStepStart',
CUCUMBER_STEP_END: 'cucumberStepEnd',
};

module.exports = { IPC_EVENTS };
8 changes: 8 additions & 0 deletions lib/plugin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ const registerReportPortalPlugin = (on, config, callbacks) => {
ipc.of.reportportal.emit(IPC_EVENTS.SET_LAUNCH_STATUS, statusInfo);
return null;
},
rp_cucumberStepStart(step) {
ipc.of.reportportal.emit(IPC_EVENTS.CUCUMBER_STEP_START, step);
return null;
},
rp_cucumberStepEnd(step) {
ipc.of.reportportal.emit(IPC_EVENTS.CUCUMBER_STEP_END, step);
return null;
},
});

on('after:screenshot', (screenshotInfo) => {
Expand Down
103 changes: 100 additions & 3 deletions lib/reporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@

const RPClient = require('@reportportal/client-javascript');

const { entityType, logLevels, testItemStatuses } = require('./constants');
const { entityType, logLevels, testItemStatuses, cucumberKeywordMap } = require('./constants');
const {
getScreenshotAttachment,
getTestStartObject,
getTestEndObject,
getHookStartObject,
getAgentInfo,
getCodeRef,
} = require('./utils');

const { createMergeLaunchLockFile, deleteMergeLaunchLockFile } = require('./mergeLaunchesUtils');
Expand Down Expand Up @@ -55,6 +56,7 @@ class Reporter {
this.suiteTestCaseIds = new Map();
this.pendingTestsIds = [];
this.suiteStatuses = new Map();
this.cucumberSteps = new Map();
}

resetCurrentTestFinishParams() {
Expand Down Expand Up @@ -137,7 +139,12 @@ class Reporter {
);
promiseErrorHandler(promise, 'Fail to start test');
this.testItemIds.set(test.id, tempId);
this.currentTestTempInfo = { tempId, startTime: startTestObj.startTime };
this.currentTestTempInfo = {
tempId,
codeRef: test.codeRef,
startTime: startTestObj.startTime,
cucumberStepIds: new Set(),
};
if (this.pendingTestsIds.includes(test.id)) {
this.testEnd(test);
}
Expand All @@ -161,6 +168,7 @@ class Reporter {
testId = this.testItemIds.get(test.id);
}
this.sendLogOnFinishFailedItem(test, testId);
this.finishFailedStep(test);
const testInfo = Object.assign({}, test, this.currentTestFinishParams);
const finishTestItemPromise = this.client.finishTestItem(
testId,
Expand All @@ -181,6 +189,75 @@ class Reporter {
}
}

cucumberStepStart(data) {
const { testStepId, pickleStep } = data;
const parent = this.currentTestTempInfo;

if (!parent) return;

const keyword = cucumberKeywordMap[pickleStep.type];
const stepName = pickleStep.text;
const codeRef = getCodeRef([stepName], parent.codeRef);

const stepData = {
name: keyword ? `${keyword} ${stepName}` : stepName,
startTime: this.client.helpers.now(),
type: entityType.STEP,
codeRef,
hasStats: false,
};

const { tempId, promise } = this.client.startTestItem(
stepData,
this.tempLaunchId,
parent.tempId,
);
promiseErrorHandler(promise, 'Fail to start step');
this.cucumberSteps.set(testStepId, { tempId, tempParentId: parent.tempId, testStepId });
parent.cucumberStepIds.add(testStepId);
}

finishFailedStep(test) {
if (test.status === FAILED) {
const step = this.getCurrentCucumberStep();

if (!step) return;

this.cucumberStepEnd({
testStepId: step.testStepId,
testStepResult: {
status: testItemStatuses.FAILED,
message: test.err.stack,
},
});
}
}

cucumberStepEnd(data) {
const { testStepId, testStepResult = { status: testItemStatuses.PASSED } } = data;
const step = this.cucumberSteps.get(testStepId);

if (!step) return;

if (testStepResult.status === testItemStatuses.FAILED) {
this.sendLog(step.tempId, {
time: this.client.helpers.now(),
level: logLevels.ERROR,
message: testStepResult.message,
});
}

this.client.finishTestItem(step.tempId, {
status: testStepResult.status,
endTime: this.client.helpers.now(),
});

this.cucumberSteps.delete(testStepId);
if (this.currentTestTempInfo) {
this.currentTestTempInfo.cucumberStepIds.delete(testStepId);
}
}

hookStart(hook) {
const hookStartObject = getHookStartObject(hook);
switch (hookStartObject.type) {
Expand Down Expand Up @@ -227,6 +304,24 @@ class Reporter {
return currentSuiteInfo && currentSuiteInfo.tempId;
}

getCurrentCucumberStep() {
if (this.currentTestTempInfo && this.currentTestTempInfo.cucumberStepIds.size > 0) {
const testStepId = Array.from(this.currentTestTempInfo.cucumberStepIds.values())[
this.currentTestTempInfo.cucumberStepIds.size - 1
];

return this.cucumberSteps.get(testStepId);
}

return null;
}

getCurrentCucumberStepId() {
const step = this.getCurrentCucumberStep();

return step && step.tempId;
}

sendLog(tempId, { level, message = '', file }) {
return this.client.sendLog(
tempId,
Expand All @@ -241,7 +336,9 @@ class Reporter {

sendLogToCurrentItem(log) {
const tempItemId =
(this.currentTestTempInfo && this.currentTestTempInfo.tempId) || this.getCurrentSuiteId();
this.getCurrentCucumberStepId() ||
(this.currentTestTempInfo && this.currentTestTempInfo.tempId) ||
this.getCurrentSuiteId();
if (tempItemId) {
const promise = this.sendLog(tempItemId, log);
promiseErrorHandler(promise, 'Fail to send log to current item');
Expand Down
6 changes: 6 additions & 0 deletions lib/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ process.on('message', (message) => {
case reporterEvents.SET_LAUNCH_STATUS:
reporter.setLaunchStatus(message.statusInfo);
break;
case reporterEvents.CUCUMBER_STEP_START:
reporter.cucumberStepStart(message.step);
break;
case reporterEvents.CUCUMBER_STEP_END:
reporter.cucumberStepEnd(message.step);
break;
default:
break;
}
Expand Down
7 changes: 6 additions & 1 deletion test/mock/mock.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const currentDate = new Date().valueOf();

class RPClient {
constructor(config) {
this.config = config;
Expand Down Expand Up @@ -25,6 +27,10 @@ class RPClient {
this.sendLog = jest.fn().mockReturnValue({
promise: Promise.resolve('ok'),
});

this.helpers = {
now: jest.fn().mockReturnValue(currentDate),
};
}
}

Expand All @@ -40,7 +46,6 @@ const getDefaultConfig = () => ({
},
});

const currentDate = new Date().valueOf();
const RealDate = Date;

const MockedDate = (...attrs) =>
Expand Down
Loading

0 comments on commit 31badf2

Please sign in to comment.