Skip to content

Commit e0cf9cd

Browse files
bn-jcundilllgandecki
authored andcommitted
added cucumber.json output
updated README for cucumber.json generation
1 parent 2f516de commit e0cf9cd

12 files changed

+725
-152
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ typings/
6161

6262
cypress/videos
6363
cypress/screenshots
64+
cypress/cucumber-json
65+
target
6466

6567
#optional .vscode settings directory
6668
.vscode

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module.exports = (on, config) => {
2525
}
2626
```
2727

28+
2829
### Feature files
2930

3031
Put your feature files in cypress/integration/
@@ -279,6 +280,42 @@ Here is an [example](cypress/support/step_definitions/customParameterTypes.js) w
279280
280281
We use https://docs.cucumber.io/cucumber/cucumber-expressions/ to parse your .feature file, please use that document as your reference
281282
283+
## cucumber.json file generation
284+
285+
The cypress-cucumber-preprocessor can generate cucumber.json file output as it runs the features files. This is separate from, and in addition to, any mocha reporter configured in cypress.
286+
287+
These files are intended to be used with one of the many available cucumber report generator packages.
288+
Seems to work fine with both https://github.com/jenkinsci/cucumber-reports-plugin and https://github.com/wswebcreation/multiple-cucumber-html-reporter
289+
290+
291+
Output, by default, is written to the folder cypress/cucumber-json/, and one file is generated per feature.
292+
293+
294+
This behaviour is configurable. Use cosmiconfig to create a configuration for the plugin, see step definition discussion above,
295+
and add the following to the cypress-cucumber-preprocessor section in package.json to turn it off or change the defaults:
296+
297+
```
298+
"cypress-cucumber-preprocessor": {
299+
"cucumberJson": {
300+
"generate": true,
301+
"outputFolder": "cucumber-json",
302+
"filePrefix": "cucumber-",
303+
"fileSuffix": ""
304+
}
305+
}
306+
```
307+
308+
Here:
309+
310+
outputFolder: The folder to write the files to, defaults to ```./cypress/cucumber-json```
311+
312+
filePrefix: A separate json file is generated for each feature based on the name of the feature file. All generated file names will be prefixed with this option if specified.
313+
314+
fileSuffix: A suffix to add to each generated filename, defaults to '.cucumber'
315+
316+
generate: Flag, output cucumber.json or not, defaults to true.
317+
318+
282319
## Development
283320
284321
Install all dependencies:

fixJson.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
const fs = require("fs");
2+
const path = require("path");
3+
4+
const args = process.argv;
5+
6+
const cucumberJsonDir = args[2] || "./cypress/cucumber-json";
7+
const screenshotsDir = args[3] || "./cypress/screenshots";
8+
const videosDir = args[4] || "./cypress/videos";
9+
10+
const featureToFileMap = {};
11+
const cukeMap = {};
12+
const videosMap = {};
13+
const jsonNames = {};
14+
15+
const jsonPath = path.join(__dirname, "..", cucumberJsonDir);
16+
const screenshotsPath = path.join(__dirname, "..", screenshotsDir);
17+
const videosPath = path.join(__dirname, "..", videosDir);
18+
const files = fs.readdirSync(jsonPath);
19+
20+
const videos = fs.readdirSync(videosPath);
21+
videos.forEach(vid => {
22+
const arr = vid.split(".");
23+
const featureName = `${arr[0]}.${arr[1]}`;
24+
videosMap[featureName] = vid;
25+
});
26+
27+
files.forEach(file => {
28+
const json = JSON.parse(
29+
fs.readFileSync(path.join(jsonPath, file)).toString()
30+
);
31+
const feature = json[0].uri.split("/").reverse()[0];
32+
jsonNames[feature] = file;
33+
cukeMap[feature] = json;
34+
featureToFileMap[feature] = file;
35+
});
36+
37+
const failingFeatures = fs.readdirSync(screenshotsPath);
38+
failingFeatures.forEach(feature => {
39+
const screenshots = fs.readdirSync(path.join(screenshotsPath, feature));
40+
screenshots.forEach(screenshot => {
41+
const scenarioName = screenshot
42+
.match(/[\S]+\sScenario\s([\w|\s]+)\([\S]+/)[1]
43+
.trim();
44+
const myScenario = cukeMap[feature][0].elements.find(
45+
e => e.name === scenarioName
46+
);
47+
const myStep = myScenario.steps.find(
48+
step => step.result.status !== "passed"
49+
);
50+
const data = fs.readFileSync(
51+
path.join(screenshotsPath, feature, screenshot)
52+
);
53+
if (data) {
54+
const base64Image = Buffer.from(data, "binary").toString("base64");
55+
myStep.embeddings.push({ data: base64Image, mime_type: "image/png" });
56+
}
57+
58+
// find my video
59+
const vidData = fs
60+
.readFileSync(path.join(videosPath, videosMap[feature]))
61+
.toString("base64");
62+
if (vidData) {
63+
const html = `<video controls width="500"><source type="video/mp4" src="data:video/mp4;base64,${vidData}"> </video>`;
64+
const encodedHtml = Buffer.from(html, "binary").toString("base64");
65+
myStep.embeddings.push({ data: encodedHtml, mime_type: "text/html" });
66+
}
67+
68+
// write me back out again
69+
fs.writeFileSync(
70+
path.join(jsonPath, jsonNames[feature]),
71+
JSON.stringify(cukeMap[feature], null, 2)
72+
);
73+
});
74+
});

lib/createTestFromScenario.js

Lines changed: 117 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,143 @@
11
/* eslint-disable prefer-template */
2+
const statuses = require("cucumber").Status;
23
const { resolveAndRunStepDefinition } = require("./resolveStepDefinition");
4+
const { generateCucumberJson } = require("./cukejson/generateCucumberJson");
35

46
const replaceParameterTags = (rowData, text) =>
57
Object.keys(rowData).reduce(
68
(value, key) => value.replace(`<${key}>`, rowData[key]),
79
text
810
);
911

10-
const stepTest = function(stepDetails, exampleRowData) {
11-
cy.log(`${stepDetails.keyword} ${stepDetails.text}`);
12-
resolveAndRunStepDefinition.call(
13-
this,
14-
stepDetails,
15-
replaceParameterTags,
16-
exampleRowData
12+
// eslint-disable-next-line func-names
13+
const stepTest = function(state, stepDetails, exampleRowData) {
14+
cy.then(() => state.onStartStep(stepDetails))
15+
.then(() =>
16+
resolveAndRunStepDefinition.call(
17+
this,
18+
stepDetails,
19+
replaceParameterTags,
20+
exampleRowData
21+
)
22+
)
23+
.then(() => state.onFinishStep(stepDetails, statuses.PASSED));
24+
};
25+
26+
const runTest = (scenario, stepsToRun, rowData) => {
27+
const indexedSteps = stepsToRun.map((step, index) =>
28+
Object.assign({}, step, { index })
1729
);
30+
31+
// eslint-disable-next-line func-names
32+
it(`Scenario: ${scenario.name}`, function() {
33+
const state = window.testState;
34+
return cy
35+
.then(() => state.onStartScenario(scenario, indexedSteps))
36+
.then(() =>
37+
indexedSteps.forEach(step => stepTest.call(this, state, step, rowData))
38+
)
39+
.then(() => state.onFinishScenario(scenario));
40+
});
1841
};
1942

20-
const createTestFromScenario = (scenario, backgroundSection) => {
21-
if (scenario.examples) {
22-
scenario.examples.forEach(example => {
23-
const exampleValues = [];
43+
const writeCucumberJsonFile = json => {
44+
const outputFolder =
45+
window.cucumberJson.outputFolder || "cypress/cucumber-json";
46+
const outputPrefix = window.cucumberJson.filePrefix || "";
47+
const outputSuffix = window.cucumberJson.fileSuffix || ".cucumber";
48+
const fileName = json[0] ? json[0].uri : "empty";
49+
const outFile = `${outputFolder}/${outputPrefix}${fileName}${outputSuffix}.json`;
50+
cy.writeFile(outFile, json, { log: false });
51+
};
52+
53+
const createTestFromScenarios = (
54+
scenariosToRun,
55+
backgroundSection,
56+
testState
57+
) => {
58+
// eslint-disable-next-line func-names
59+
describe(`Feature: ${testState.feature.name}`, function() {
60+
before(() => {
61+
cy.then(() => testState.onStartTest());
62+
});
63+
64+
// ctx is cleared between each 'it'
65+
// eslint-disable-next-line func-names, prefer-arrow-callback
66+
beforeEach(function() {
67+
window.testState = testState;
68+
69+
const failHandler = err => {
70+
Cypress.off("fail", failHandler);
71+
testState.onFail(err);
72+
throw err;
73+
};
74+
75+
Cypress.on("fail", failHandler);
76+
});
77+
78+
scenariosToRun.forEach(section => {
79+
if (section.examples) {
80+
section.examples.forEach(example => {
81+
const exampleValues = [];
82+
const exampleLocations = [];
2483

25-
example.tableBody.forEach((row, rowIndex) => {
26-
example.tableHeader.cells.forEach((header, headerIndex) => {
27-
exampleValues[rowIndex] = Object.assign({}, exampleValues[rowIndex], {
28-
[header.value]: row.cells[headerIndex].value
84+
example.tableBody.forEach((row, rowIndex) => {
85+
exampleLocations[rowIndex] = row.location;
86+
example.tableHeader.cells.forEach((header, headerIndex) => {
87+
exampleValues[rowIndex] = Object.assign(
88+
{},
89+
exampleValues[rowIndex],
90+
{
91+
[header.value]: row.cells[headerIndex].value
92+
}
93+
);
94+
});
2995
});
30-
});
31-
});
3296

33-
exampleValues.forEach((rowData, index) => {
34-
// eslint-disable-next-line prefer-arrow-callback
35-
const scenarioName = replaceParameterTags(rowData, scenario.name);
36-
it(`${scenarioName} (example #${index + 1})`, function() {
37-
if (backgroundSection) {
38-
backgroundSection.steps.forEach(step => {
39-
stepTest.call(this, step);
97+
exampleValues.forEach((rowData, index) => {
98+
// eslint-disable-next-line prefer-arrow-callback
99+
const scenarioName = replaceParameterTags(rowData, section.name);
100+
const uniqueScenarioName = `${scenarioName} (example #${index +
101+
1})`;
102+
const exampleSteps = section.steps.map(step => {
103+
const newStep = Object.assign({}, step);
104+
newStep.text = replaceParameterTags(rowData, newStep.text);
105+
return newStep;
40106
});
41-
}
42107

43-
scenario.steps.forEach(step => {
44-
const newStep = Object.assign({}, step);
45-
newStep.text = replaceParameterTags(rowData, newStep.text);
108+
const stepsToRun = backgroundSection
109+
? backgroundSection.steps.concat(exampleSteps)
110+
: exampleSteps;
46111

47-
stepTest.call(this, newStep, rowData);
112+
const scenarioExample = Object.assign({}, section, {
113+
name: uniqueScenarioName,
114+
example: exampleLocations[index]
115+
});
116+
117+
runTest.call(this, scenarioExample, stepsToRun, rowData);
48118
});
49119
});
50-
});
51-
});
52-
} else {
53-
it(scenario.name, function() {
54-
if (backgroundSection) {
55-
backgroundSection.steps.forEach(step => stepTest.call(this, step));
120+
} else {
121+
const stepsToRun = backgroundSection
122+
? backgroundSection.steps.concat(section.steps)
123+
: section.steps;
124+
125+
runTest.call(this, section, stepsToRun);
56126
}
57-
scenario.steps.forEach(step => stepTest.call(this, step));
58127
});
59-
}
128+
129+
// eslint-disable-next-line func-names, prefer-arrow-callback
130+
after(function() {
131+
cy.then(() => testState.onFinishTest()).then(() => {
132+
if (window.cucumberJson.generate) {
133+
const json = generateCucumberJson(testState);
134+
writeCucumberJsonFile(json);
135+
}
136+
});
137+
});
138+
});
60139
};
61140

62141
module.exports = {
63-
createTestFromScenario
142+
createTestFromScenarios
64143
};

lib/createTestsFromFeature.js

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
const { createTestFromScenario } = require("./createTestFromScenario");
1+
const { CucumberDataCollector } = require("./cukejson/cucumberDataCollector");
2+
const { createTestFromScenarios } = require("./createTestFromScenario");
23
const { shouldProceedCurrentStep, getEnvTags } = require("./tagsHelper");
34

4-
const createTestsFromFeature = parsedFeature => {
5-
const featureTags = parsedFeature.feature.tags;
5+
const createTestsFromFeature = (filePath, spec) => {
6+
const testState = new CucumberDataCollector(filePath, spec);
7+
const featureTags = testState.feature.tags;
68
const hasEnvTags = !!getEnvTags();
7-
const sectionsWithTags = parsedFeature.feature.children.filter(
9+
const sectionsWithTags = testState.feature.children.filter(
810
section => section.tags && section.tags.length
911
);
1012

@@ -16,7 +18,7 @@ const createTestsFromFeature = parsedFeature => {
1618
let anyFocused = false;
1719
if (hasEnvTags) {
1820
featureShouldRun = shouldProceedCurrentStep(featureTags);
19-
taggedScenarioShouldRun = parsedFeature.feature.children.some(
21+
taggedScenarioShouldRun = testState.feature.children.some(
2022
section =>
2123
section.tags &&
2224
section.tags.length &&
@@ -36,29 +38,26 @@ const createTestsFromFeature = parsedFeature => {
3638
}
3739

3840
// eslint-disable-next-line prefer-arrow-callback
39-
describe(parsedFeature.feature.name, function() {
40-
if (everythingShouldRun || featureShouldRun || taggedScenarioShouldRun) {
41-
const backgroundSection = parsedFeature.feature.children.find(
42-
section => section.type === "Background"
43-
);
44-
const otherSections = parsedFeature.feature.children.filter(
45-
section => section.type !== "Background"
46-
);
47-
otherSections.forEach(section => {
48-
let shouldRun;
49-
if (anyFocused) {
50-
shouldRun = section.tags.find(t => t.name === "@focus");
51-
} else {
52-
shouldRun =
53-
everythingShouldRun ||
54-
shouldProceedCurrentStep(section.tags.concat(featureTags)); // concat handles inheritance of tags from feature
55-
}
56-
if (shouldRun) {
57-
createTestFromScenario(section, backgroundSection);
58-
}
59-
});
60-
}
61-
});
41+
if (everythingShouldRun || featureShouldRun || taggedScenarioShouldRun) {
42+
const backgroundSection = testState.feature.children.find(
43+
section => section.type === "Background"
44+
);
45+
const otherSections = testState.feature.children.filter(
46+
section => section.type !== "Background"
47+
);
48+
const scenariosToRun = otherSections.filter(section => {
49+
let shouldRun;
50+
if (anyFocused) {
51+
shouldRun = section.tags.find(t => t.name === "@focus");
52+
} else {
53+
shouldRun =
54+
everythingShouldRun ||
55+
shouldProceedCurrentStep(section.tags.concat(featureTags)); // Concat handles inheritance of tags from feature
56+
}
57+
return shouldRun;
58+
});
59+
createTestFromScenarios(scenariosToRun, backgroundSection, testState);
60+
}
6261
};
6362

6463
module.exports = {

0 commit comments

Comments
 (0)