Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EPMRPP-90382 || Implement reporting Gherkin scenario step as nested step #189

Merged
merged 4 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

## [5.2.0] - 2024-03-21
### Fixed
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.
AmsterGet marked this conversation as resolved.
Show resolved Hide resolved

```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