Skip to content

Commit 74b2b40

Browse files
authored
Merge pull request #195 from visusnet/feature/hooks
feat: Added cucumber-js-style Before and After hooks. Related: #25
2 parents b05e31f + 920aeec commit 74b2b40

14 files changed

+7987
-4876
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
Feature: Using Before and After
2+
3+
Scenario: With Untagged Before
4+
Then Untagged Before was called once
5+
And Tagged Before was not called
6+
7+
# After is tested in following scenario by verifying that the After ran at the end of the previous one
8+
Scenario: With Untagged After part 1
9+
Given I executed empty step
10+
11+
Scenario: With Untagged After part 2
12+
Then Flag should be set by untagged After
13+
14+
# After is tested in following scenario by verifying that the After ran at the end of the previous one.
15+
@withTaggedAfter
16+
Scenario: With tagged After part 1
17+
Given I executed empty step
18+
19+
Scenario: With tagged After part 2
20+
Then Flag should be set by tagged After
21+
22+
@withTaggedBefore
23+
Scenario: With tagged Before only
24+
Then Tagged Before was called once
25+
And Untagged Before was called once
26+
27+
@withTaggedBefore
28+
@withAnotherTaggedBefore
29+
Scenario: With multiple tags
30+
Given I executed empty step
31+
Then Tagged Before was called twice
32+
And Untagged Before was called once
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* eslint-env mocha */
2+
/* eslint-disable import/no-extraneous-dependencies */
3+
const {
4+
Before,
5+
After,
6+
Given,
7+
Then
8+
} = require("cypress-cucumber-preprocessor/steps");
9+
10+
let beforeCounter = 0;
11+
let beforeWithTagCounter = 0;
12+
let flagSetByUntaggedAfter = false;
13+
let flagSetByTaggedAfter = false;
14+
15+
Before(() => {
16+
beforeCounter += 1;
17+
beforeWithTagCounter = 0;
18+
});
19+
20+
Before({ tags: "@withTaggedBefore" }, () => {
21+
beforeWithTagCounter += 1;
22+
});
23+
24+
Before({ tags: "@withAnotherTaggedBefore" }, () => {
25+
beforeWithTagCounter += 1;
26+
});
27+
28+
Before({ tags: "@willNeverRun" }, () => {
29+
throw new Error("XXX: before hook unexpectedly called.");
30+
});
31+
32+
After({ tags: "@willNeverRun" }, () => {
33+
throw new Error("XXX: after hook unexpectedly called.");
34+
});
35+
36+
After(() => {
37+
beforeCounter = 0;
38+
flagSetByUntaggedAfter = true;
39+
});
40+
41+
After({ tags: "@withTaggedAfter" }, () => {
42+
flagSetByTaggedAfter = true;
43+
});
44+
45+
Given("I executed empty step", () => {});
46+
47+
Then("Untagged Before was called once", () => {
48+
expect(beforeCounter).to.equal(1);
49+
});
50+
51+
Then("Untagged Before was not called", () => {
52+
expect(beforeCounter).to.equal(0);
53+
});
54+
55+
Then("Tagged Before was called once", () => {
56+
expect(beforeWithTagCounter).to.equal(1);
57+
});
58+
59+
Then("Tagged Before was called twice", () => {
60+
expect(beforeWithTagCounter).to.equal(2);
61+
});
62+
63+
Then("Tagged Before was not called", () => {
64+
expect(beforeWithTagCounter).to.equal(0);
65+
});
66+
67+
Then("Flag should be set by untagged After", () => {
68+
expect(flagSetByUntaggedAfter).to.equal(true);
69+
});
70+
71+
Then("Flag should be set by tagged After", () => {
72+
expect(flagSetByTaggedAfter).to.equal(true);
73+
});

lib/createTestFromScenario.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
/* eslint-disable prefer-template */
22
const statuses = require("cucumber").Status;
3-
const { resolveAndRunStepDefinition } = require("./resolveStepDefinition");
3+
const {
4+
resolveAndRunStepDefinition,
5+
resolveAndRunBeforeHooks,
6+
resolveAndRunAfterHooks
7+
} = require("./resolveStepDefinition");
48
const { generateCucumberJson } = require("./cukejson/generateCucumberJson");
59

610
const replaceParameterTags = (rowData, text) =>
@@ -33,9 +37,11 @@ const runTest = (scenario, stepsToRun, rowData) => {
3337
const state = window.testState;
3438
return cy
3539
.then(() => state.onStartScenario(scenario, indexedSteps))
40+
.then(() => resolveAndRunBeforeHooks.call(this, scenario.tags))
3641
.then(() =>
3742
indexedSteps.forEach(step => stepTest.call(this, state, step, rowData))
3843
)
44+
.then(() => resolveAndRunAfterHooks.call(this, scenario.tags))
3945
.then(() => state.onFinishScenario(scenario));
4046
});
4147
};
@@ -131,7 +137,7 @@ const createTestFromScenarios = (
131137
// eslint-disable-next-line func-names, prefer-arrow-callback
132138
after(function() {
133139
cy.then(() => testState.onFinishTest()).then(() => {
134-
if (window.cucumberJson.generate) {
140+
if (window.cucumberJson && window.cucumberJson.generate) {
135141
const json = generateCucumberJson(testState);
136142
writeCucumberJsonFile(json);
137143
}

lib/cukejson/cucumberDataCollector.test.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ const { generateCucumberJson } = require("./generateCucumberJson");
55

66
window.cucumberJson = { generate: true };
77

8-
window.Cypress = {
9-
log: jest.fn()
10-
};
11-
128
const assertCucumberJson = (json, expectedResults) => {
139
expect(json).to.have.length(1);
1410
expect(json[0].keyword).to.eql("Feature");

lib/loader.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ const { getStepDefinitionsPaths } = require("./getStepDefinitionsPaths");
88
// feature file
99
const createCucumber = (filePath, cucumberJson, spec, toRequire) =>
1010
`
11-
const {resolveAndRunStepDefinition, defineParameterType, given, when, then, and, but, defineStep} = require('cypress-cucumber-preprocessor/lib/resolveStepDefinition');
11+
const {resolveAndRunStepDefinition, defineParameterType, given, when, then, and, but, before, after, defineStep} = require('cypress-cucumber-preprocessor/lib/resolveStepDefinition');
1212
const Given = window.Given = window.given = given;
1313
const When = window.When = window.when = when;
1414
const Then = window.Then = window.then = then;
1515
const And = window.And = window.and = and;
1616
const But = window.But = window.but = but;
17+
const Before = window.Before = window.before = before;
18+
const After = window.After = window.after = after;
1719
window.defineParameterType = defineParameterType;
1820
window.defineStep = defineStep;
1921
const { createTestsFromFeature } = require('cypress-cucumber-preprocessor/lib/createTestsFromFeature');

lib/resolveStepDefinition.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const {
77
RegularExpression,
88
ParameterTypeRegistry
99
} = require("cucumber-expressions");
10+
const { shouldProceedCurrentStep } = require("./tagsHelper");
1011

1112
class StepDefinitionRegistry {
1213
constructor() {
@@ -38,7 +39,28 @@ class StepDefinitionRegistry {
3839
}
3940
}
4041

42+
class HookRegistry {
43+
constructor() {
44+
this.definitions = [];
45+
this.runtime = {};
46+
47+
this.runtime = (tags, implementation) => {
48+
this.definitions.push({ tags, implementation });
49+
};
50+
51+
this.resolve = scenarioTags =>
52+
this.definitions.filter(
53+
({ tags }) =>
54+
!tags ||
55+
tags.length === 0 ||
56+
shouldProceedCurrentStep(scenarioTags, tags)
57+
);
58+
}
59+
}
60+
4161
const stepDefinitionRegistry = new StepDefinitionRegistry();
62+
const beforeHookRegistry = new HookRegistry();
63+
const afterHookRegistry = new HookRegistry();
4264

4365
function resolveStepDefinition(step) {
4466
const stepDefinition = stepDefinitionRegistry.resolve(
@@ -104,7 +126,47 @@ function resolveStepArgument(argument, exampleRowData, replaceParameterTags) {
104126
return argument;
105127
}
106128

129+
function resolveAndRunHooks(hookRegistry, scenarioTags) {
130+
return window.Cypress.Promise.each(
131+
hookRegistry.resolve(scenarioTags),
132+
({ implementation }) => implementation.call(this)
133+
);
134+
}
135+
136+
function parseHookArgs(args) {
137+
if (args.length === 2) {
138+
if (typeof args[0] !== "object" || typeof args[0].tags !== "string") {
139+
throw new Error(
140+
"Hook definitions with two arguments should have an object containing tags (string) as the first argument."
141+
);
142+
}
143+
if (typeof args[1] !== "function") {
144+
throw new Error(
145+
"Hook definitions with two arguments must have a function as the second argument."
146+
);
147+
}
148+
return {
149+
tags: args[0].tags,
150+
implementation: args[1]
151+
};
152+
}
153+
if (typeof args[0] !== "function") {
154+
throw new Error(
155+
"Hook definitions with one argument must have a function as the first argument."
156+
);
157+
}
158+
return {
159+
implementation: args[0]
160+
};
161+
}
162+
107163
module.exports = {
164+
resolveAndRunBeforeHooks(scenarioTags) {
165+
return resolveAndRunHooks(beforeHookRegistry, scenarioTags);
166+
},
167+
resolveAndRunAfterHooks(scenarioTags) {
168+
return resolveAndRunHooks(afterHookRegistry, scenarioTags);
169+
},
108170
// eslint-disable-next-line func-names
109171
resolveAndRunStepDefinition(step, replaceParameterTags, exampleRowData) {
110172
const { expression, implementation } = resolveStepDefinition(step);
@@ -138,6 +200,14 @@ module.exports = {
138200
but: (expression, implementation) => {
139201
stepDefinitionRegistry.runtime(expression, implementation);
140202
},
203+
before: (...args) => {
204+
const { tags, implementation } = parseHookArgs(args);
205+
beforeHookRegistry.runtime(tags, implementation);
206+
},
207+
after: (...args) => {
208+
const { tags, implementation } = parseHookArgs(args);
209+
afterHookRegistry.runtime(tags, implementation);
210+
},
141211
defineStep: (expression, implementation) => {
142212
stepDefinitionRegistry.runtime(expression, implementation);
143213
},

lib/resolveStepDefinition.test.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
/* eslint-disable global-require */
22

3-
const { resolveFeatureFromFile } = require("./setup.js");
3+
const {
4+
resolveFeatureFromFile
5+
} = require("./testHelpers/resolveFeatureFromFile");
46

57
describe("Scenario Outline", () => {
68
require("../cypress/support/step_definitions/scenario_outline_integer");
@@ -46,9 +48,8 @@ describe("Tags implementation", () => {
4648

4749
describe("Tags with env TAGS set", () => {
4850
window.Cypress = {
49-
env: () => "@test-tag and not @ignore-tag",
50-
on: jest.fn(),
51-
off: jest.fn()
51+
...window.Cypress,
52+
env: () => "@test-tag and not @ignore-tag"
5253
};
5354
require("../cypress/support/step_definitions/tags_implementation_with_env_set");
5455
resolveFeatureFromFile(
@@ -62,9 +63,8 @@ describe("Tags with env TAGS set", () => {
6263

6364
describe("Smart tagging", () => {
6465
window.Cypress = {
65-
env: () => "",
66-
on: jest.fn(),
67-
off: jest.fn()
66+
...window.Cypress,
67+
env: () => ""
6868
};
6969
require("../cypress/support/step_definitions/smart_tagging");
7070
resolveFeatureFromFile("./cypress/integration/SmartTagging.feature");
@@ -79,3 +79,8 @@ describe("defineStep", () => {
7979
require("../cypress/support/step_definitions/usingDefineSteps");
8080
resolveFeatureFromFile("./cypress/integration/DefineStep.feature");
8181
});
82+
83+
describe("Before and After", () => {
84+
require("../cypress/support/step_definitions/before_and_after_steps");
85+
resolveFeatureFromFile("./cypress/integration/BeforeAndAfterSteps.feature");
86+
});

lib/setupTestFramework.js

Lines changed: 0 additions & 2 deletions
This file was deleted.

lib/tagsInheritance.test.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/* eslint-disable global-require */
22
/* global jest */
3-
const { resolveFeatureFromFile } = require("./setup");
3+
const {
4+
resolveFeatureFromFile
5+
} = require("./testHelpers/resolveFeatureFromFile");
46

57
describe("Tags inheritance", () => {
68
window.Cypress = {
7-
env: () => "@inherited-tag and @own-tag",
8-
on: jest.fn(),
9-
off: jest.fn()
9+
...window.Cypress,
10+
env: () => "@inherited-tag and @own-tag"
1011
};
1112

1213
require("../cypress/support/step_definitions/tags_implementation_with_env_set");
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/* eslint-disable global-require */
2+
/* global jest */
3+
const fs = require("fs");
4+
5+
const { createTestsFromFeature } = require("../createTestsFromFeature");
6+
7+
const resolveFeatureFromFile = featureFile => {
8+
const spec = fs.readFileSync(featureFile);
9+
createTestsFromFeature(featureFile, spec);
10+
};
11+
12+
module.exports = {
13+
resolveFeatureFromFile
14+
};

0 commit comments

Comments
 (0)