Skip to content

Commit ddf5883

Browse files
committed
Merge branch 'master' of https://github.com/TheBrainFamily/cypress-cucumber-preprocessor into configurable-loc-non-global-steps
2 parents 1a7b57e + 8a5f032 commit ddf5883

20 files changed

+1787
-1600
lines changed

README.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ You can follow the documentation below, or if you prefer to hack on a working ex
1818
* [Step definitions](#step-definitions)
1919
* [Step definitions creation](#step-definitions-creation)
2020
* [Reusable step definitions](#reusable-step-definitions)
21-
* [How to write tests](#excluding-tests)
21+
* [How to write tests](#how-to-write-tests)
2222
* [Cucumber Expressions](#cucumber-expressions)
2323
* [Given/When/Then functions](#cucumber-functions)
2424
* [Custom Parameter Type Resolves](#custom-parameter-type-resolves)
@@ -28,6 +28,7 @@ You can follow the documentation below, or if you prefer to hack on a working ex
2828
* [Smart tagging](#smart-tagging)
2929
* [How to run the tests](#excluding-tests)
3030
* [Running tagged tests](#running-tagged-tests)
31+
* [Ignoring specific scenarios using tags when executing test runner](#ignoring-specific-scenarios-using-tags-when-executing-test-runner)
3132
* [Output](#output)
3233
* [IDE support](#ide-support)
3334
* [Webstorm](#webstorm)
@@ -328,8 +329,28 @@ Example:
328329
```
329330
330331
Please note - we use our own cypress-tags wrapper to speed things up.
332+
This wrapper calls the cypress executable from local modules and if not found it falls back to the globally installed one.
331333
For more details and examples please take a look to the [example repo](https://github.com/TheBrainFamily/cypress-cucumber-example).
332334
335+
### Ignoring specific scenarios using tags when executing test runner
336+
You can also use tags to skip or ignore specific tests/scenarios when running cypress test runner (where you don't have the abilitiy to pass parameters like in the examples above for the execution)
337+
338+
The trick consists in adding the "env" property with the "TAGS" subproperty in the cypress.json configuration file. It would look something like this:
339+
340+
```javascript
341+
{
342+
"env": {
343+
"TAGS": "not @ignore"
344+
},
345+
//rest of configuration options
346+
"baseUrl": "yourBaseUrl",
347+
"ignoreTestFiles": "*.js",
348+
//etc
349+
}
350+
```
351+
352+
Then, any scenarios tagged with @ignore will be skipped when running the tests using the cypress test runner
353+
333354
### Limiting to a subset of feature files
334355
You can use a glob expression to select which feature files should be included.
335356
@@ -389,6 +410,14 @@ Note, that unlike WebStorm which will correctly identify multiple implementation
389410
390411
## TypeScript Support
391412
413+
### Install
414+
415+
Install the plug-in type definitions:
416+
417+
```shell
418+
npm install --save-dev @types/cypress-cucumber-preprocessor
419+
```
420+
392421
### With Webpack
393422
You can also use a Webpack loader to process feature files (TypeScript supported). To see how it is done please take
394423
a look here: [cypress-cucumber-webpack-typescript-example](https://github.com/TheBrainFamily/cypress-cucumber-webpack-typescript-example)

cypress-tags.js

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,44 @@ const debug = (message, ...rest) =>
1212
? console.log(`DEBUG: ${message}`, rest.length ? rest : "")
1313
: null;
1414

15+
function parseArgsOrDefault(argPrefix, defaultValue) {
16+
const matchedArg = process.argv
17+
.slice(2)
18+
.find(arg => arg.includes(`${argPrefix}=`));
19+
20+
// Cypress requires env vars to be passed as comma separated list
21+
// otherwise it only accepts the last provided variable,
22+
// the way we replace here accomodates for that.
23+
const argValue = matchedArg
24+
? matchedArg.replace(new RegExp(`.*${argPrefix}=`), "").replace(/,.*/, "")
25+
: "";
26+
27+
return argValue !== "" ? argValue : defaultValue;
28+
}
29+
1530
// TODO currently we only work with feature files in cypress/integration folder.
1631
// It should be easy to base this on the cypress.json configuration - we are happy to take a PR
1732
// here if you need this functionality!
1833
const defaultGlob = "cypress/integration/**/*.feature";
1934

20-
const specArg = process.argv.slice(2).find(arg => arg.indexOf("GLOB=") === 0);
21-
22-
const specGlob = specArg ? specArg.replace(/.*=/, "") : defaultGlob;
23-
24-
if (specArg) {
25-
debug("Found glob", specGlob);
26-
}
35+
const specGlob = parseArgsOrDefault("GLOB", defaultGlob);
36+
debug("Found glob", specGlob);
37+
const envTags = parseArgsOrDefault("TAGS", "");
38+
debug("Found tag expression", envTags);
2739

2840
const paths = glob.sync(specGlob);
2941

3042
const featuresToRun = [];
3143

32-
const found = process.argv.slice(2).find(arg => arg.indexOf("TAGS=") === 0);
33-
34-
const envTags = found.replace(/.*=/, "");
35-
debug("Found tag expression", envTags);
36-
3744
paths.forEach(featurePath => {
3845
const spec = `${fs.readFileSync(featurePath)}`;
3946
const parsedFeature = new Parser().parse(spec);
4047

48+
if (!parsedFeature.feature) {
49+
debug(`Feature: ${featurePath} is empty`);
50+
return;
51+
}
52+
4153
const featureTags = parsedFeature.feature.tags;
4254
const featureShouldRun = shouldProceedCurrentStep(featureTags, envTags);
4355
const taggedScenarioShouldRun = parsedFeature.feature.children.some(
@@ -54,12 +66,20 @@ paths.forEach(featurePath => {
5466
}
5567
});
5668

69+
function getOsSpecificExecutable(command) {
70+
return process.platform === "win32" ? `${command}.cmd` : command;
71+
}
72+
73+
function getCypressExecutable() {
74+
const command = getOsSpecificExecutable(`${__dirname}/../.bin/cypress`);
75+
// fallback to the globally installed cypress instead
76+
return fs.existsSync(command) ? command : getOsSpecificExecutable("cypress");
77+
}
78+
5779
try {
5880
if (featuresToRun.length || envTags === "") {
5981
execFileSync(
60-
process.platform === "win32"
61-
? `${__dirname}/../.bin/cypress.cmd`
62-
: `${__dirname}/../.bin/cypress`,
82+
getCypressExecutable(),
6383
[...process.argv.slice(2), "--spec", featuresToRun.join(",")],
6484
{
6585
stdio: [process.stdin, process.stdout, process.stderr]

lib/createTestFromScenario.js

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable prefer-template */
2-
const statuses = require("cucumber").Status;
2+
const statuses = require("cucumber/lib/status").default;
33
const {
4+
resolveStepDefinition,
45
resolveAndRunStepDefinition,
56
resolveAndRunBeforeHooks,
67
resolveAndRunAfterHooks
@@ -15,8 +16,13 @@ const replaceParameterTags = (rowData, text) =>
1516

1617
// eslint-disable-next-line func-names
1718
const stepTest = function(state, stepDetails, exampleRowData) {
19+
const step = resolveStepDefinition.call(
20+
this,
21+
stepDetails,
22+
state.feature.name
23+
);
1824
cy.then(() => state.onStartStep(stepDetails))
19-
.then(() =>
25+
.then((step && step.config) || {}, () =>
2026
resolveAndRunStepDefinition.call(
2127
this,
2228
stepDetails,
@@ -33,22 +39,42 @@ const runTest = (scenario, stepsToRun, rowData) => {
3339
Object.assign({}, step, { index })
3440
);
3541

36-
// eslint-disable-next-line func-names
37-
it(scenario.name, function() {
38-
const state = window.testState;
39-
return cy
40-
.then(() => state.onStartScenario(scenario, indexedSteps))
41-
.then(() =>
42-
resolveAndRunBeforeHooks.call(this, scenario.tags, state.feature.name)
43-
)
44-
.then(() =>
45-
indexedSteps.forEach(step => stepTest.call(this, state, step, rowData))
46-
)
47-
.then(() =>
48-
resolveAndRunAfterHooks.call(this, scenario.tags, state.feature.name)
49-
)
50-
.then(() => state.onFinishScenario(scenario));
51-
});
42+
// should we actually run this scenario
43+
// or just mark it as skipped
44+
if (scenario.shouldRun) {
45+
// eslint-disable-next-line func-names
46+
it(scenario.name, function() {
47+
const state = window.testState;
48+
return cy
49+
.then(() => state.onStartScenario(scenario, indexedSteps))
50+
.then(() =>
51+
resolveAndRunBeforeHooks.call(this, scenario.tags, state.feature.name)
52+
)
53+
.then(() =>
54+
indexedSteps.forEach(step =>
55+
stepTest.call(this, state, step, rowData)
56+
)
57+
)
58+
.then(() =>
59+
resolveAndRunAfterHooks.call(this, scenario.tags, state.feature.name)
60+
)
61+
.then(() => state.onFinishScenario(scenario));
62+
});
63+
} else {
64+
// eslint-disable-next-line func-names,prefer-arrow-callback
65+
it(scenario.name, function() {
66+
// register this scenario with the cucumber data collector
67+
// but don't run it
68+
// Tell mocha this is a skipped test so it also shows correctly in Cypress
69+
const state = window.testState;
70+
cy.then(() => state.onStartScenario(scenario, indexedSteps))
71+
.then(() => state.onFinishScenario(scenario))
72+
// eslint-disable-next-line func-names
73+
.then(function() {
74+
return this.skip();
75+
});
76+
});
77+
}
5278
};
5379

5480
const cleanupFilename = s => s.split(".")[0];
@@ -64,7 +90,7 @@ const writeCucumberJsonFile = json => {
6490
};
6591

6692
const createTestFromScenarios = (
67-
scenariosToRun,
93+
allScenarios,
6894
backgroundSection,
6995
testState
7096
) => {
@@ -87,7 +113,7 @@ const createTestFromScenarios = (
87113
Cypress.on("fail", failHandler);
88114
});
89115

90-
scenariosToRun.forEach(section => {
116+
allScenarios.forEach(section => {
91117
if (section.examples) {
92118
section.examples.forEach(example => {
93119
const exampleValues = [];

lib/createTestsFromFeature.js

Lines changed: 27 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,58 +6,37 @@ const createTestsFromFeature = (filePath, spec) => {
66
const testState = new CucumberDataCollector(filePath, spec);
77
const featureTags = testState.feature.tags;
88
const hasEnvTags = !!getEnvTags();
9-
const sectionsWithTags = testState.feature.children.filter(
10-
section => section.tags && section.tags.length
9+
const anyFocused =
10+
testState.feature.children.filter(
11+
section => section.tags && section.tags.find(t => t.name === "@focus")
12+
).length > 0;
13+
const backgroundSection = testState.feature.children.find(
14+
section => section.type === "Background"
15+
);
16+
const allScenarios = testState.feature.children.filter(
17+
section => section.type !== "Background"
1118
);
1219

13-
const sectionsWithTagsExist = sectionsWithTags.length > 0;
14-
15-
let everythingShouldRun = false;
16-
let featureShouldRun = false;
17-
let taggedScenarioShouldRun = false;
18-
let anyFocused = false;
19-
if (hasEnvTags) {
20-
featureShouldRun = shouldProceedCurrentStep(featureTags);
21-
taggedScenarioShouldRun = testState.feature.children.some(
22-
section =>
23-
section.tags &&
24-
section.tags.length &&
25-
shouldProceedCurrentStep(section.tags.concat(featureTags))
26-
);
27-
} else if (!sectionsWithTagsExist) {
28-
everythingShouldRun = true;
29-
} else {
30-
anyFocused = sectionsWithTags.some(section =>
31-
section.tags.find(t => t.name === "@focus")
32-
);
33-
if (anyFocused) {
34-
taggedScenarioShouldRun = true;
20+
const scenariosToRun = allScenarios.filter(section => {
21+
let shouldRun;
22+
// only just run focused if no env tags set
23+
// https://github.com/TheBrainFamily/cypress-cucumber-example#smart-tagging
24+
if (!hasEnvTags && anyFocused) {
25+
shouldRun = section.tags.find(t => t.name === "@focus");
3526
} else {
36-
everythingShouldRun = true;
27+
shouldRun =
28+
!hasEnvTags ||
29+
shouldProceedCurrentStep(section.tags.concat(featureTags)); // Concat handles inheritance of tags from feature
3730
}
38-
}
39-
40-
// eslint-disable-next-line prefer-arrow-callback
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-
}
31+
return shouldRun;
32+
});
33+
// create tests for all the scenarios
34+
// but flag only the ones that should be run
35+
scenariosToRun.forEach(section => {
36+
// eslint-disable-next-line no-param-reassign
37+
section.shouldRun = true;
38+
});
39+
createTestFromScenarios(allScenarios, backgroundSection, testState);
6140
};
6241

6342
module.exports = {

lib/cukejson/cucumberDataCollector.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const { Parser } = require("gherkin");
2-
const statuses = require("cucumber").Status;
2+
const statuses = require("cucumber/lib/status").default;
33

44
class CucumberDataCollector {
55
constructor(uri, spec) {
@@ -72,6 +72,14 @@ class CucumberDataCollector {
7272
status: statuses.UNDEFINED,
7373
duration: this.timeTaken()
7474
};
75+
} else if (err.constructor.name === "Pending") {
76+
// cypress marks skipped mocha tests as pending
77+
// https://github.com/cypress-io/cypress/issues/3092
78+
// don't record this error and mark the step as skipped
79+
this.stepResults[this.currentStep] = {
80+
status: statuses.SKIPPED,
81+
duration: this.timeTaken()
82+
};
7583
} else {
7684
this.stepResults[this.currentStep] = {
7785
status: statuses.FAILED,
@@ -122,9 +130,13 @@ class CucumberDataCollector {
122130
});
123131
};
124132
this.recordScenarioResult = scenario => {
125-
this.runTests[scenario.name].result = this.anyStepsHaveFailed(scenario)
126-
? statuses.FAILED
127-
: statuses.PASSED;
133+
const allSkipped = this.areAllStepsSkipped(scenario.name);
134+
const anyFailed = this.anyStepsHaveFailed(scenario.name);
135+
if (allSkipped) this.runTests[scenario.name].result = statuses.SKIPPED;
136+
else
137+
this.runTests[scenario.name].result = anyFailed
138+
? statuses.FAILED
139+
: statuses.PASSED;
128140
};
129141

130142
this.setStepToPending = step => {
@@ -138,8 +150,11 @@ class CucumberDataCollector {
138150
};
139151
};
140152

141-
this.anyStepsHaveFailed = () =>
142-
Object.values(this.stepResults).find(e => e.status !== statuses.PASSED);
153+
this.areAllStepsSkipped = name =>
154+
this.runTests[name].every(e => e.status === statuses.SKIPPED);
155+
156+
this.anyStepsHaveFailed = name =>
157+
this.runTests[name].find(e => e.status === statuses.FAILED) !== undefined;
143158
}
144159
}
145160

lib/cukejson/cucumberDataCollector.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const fs = require("fs");
2-
const statuses = require("cucumber").Status;
2+
const statuses = require("cucumber/lib/status").default;
33
const { CucumberDataCollector } = require("./cucumberDataCollector");
44
const { generateCucumberJson } = require("./generateCucumberJson");
55

lib/cukejson/generateCucumberJson.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const { EventEmitter } = require("events");
22
const { generateEvents } = require("gherkin");
3-
const { JsonFormatter, formatterHelpers } = require("cucumber");
3+
const JsonFormatter = require("cucumber/lib/formatter/json_formatter").default;
4+
const formatterHelpers = require("cucumber/lib/formatter/helpers");
45

56
function generateCucumberJson(state) {
67
let output = "";

0 commit comments

Comments
 (0)