This commit is contained in:
Simon Priet
2021-09-05 22:53:58 +02:00
commit 9e2991e668
17888 changed files with 1263126 additions and 0 deletions

View File

@@ -0,0 +1,191 @@
/* eslint-disable prefer-template */
const statuses = require("cucumber/lib/status").default;
const {
resolveStepDefinition,
resolveAndRunStepDefinition,
resolveAndRunBeforeHooks,
resolveAndRunAfterHooks,
} = require("./resolveStepDefinition");
const { generateCucumberJson } = require("./cukejson/generateCucumberJson");
const { shouldProceedCurrentStep, getEnvTags } = require("./tagsHelper");
const replaceParameterTags = (rowData, text) =>
Object.keys(rowData).reduce(
(value, key) => value.replace(new RegExp(`<${key}>`, "g"), rowData[key]),
text
);
// eslint-disable-next-line func-names
const stepTest = function (state, stepDetails, exampleRowData) {
const step = resolveStepDefinition.call(
this,
stepDetails,
state.feature.name
);
cy.then(() => state.onStartStep(stepDetails))
.then((step && step.config) || {}, () =>
resolveAndRunStepDefinition.call(
this,
stepDetails,
replaceParameterTags,
exampleRowData,
state.feature.name
)
)
.then(() => state.onFinishStep(stepDetails, statuses.PASSED));
};
const runTest = (scenario, stepsToRun, rowData) => {
const indexedSteps = stepsToRun.map((step, index) => ({ ...step, index }));
// should we actually run this scenario
// or just mark it as skipped
if (scenario.shouldRun) {
// eslint-disable-next-line func-names
it(scenario.name, function () {
const state = window.testState;
return cy
.then(() => state.onStartScenario(scenario, indexedSteps))
.then(() =>
resolveAndRunBeforeHooks.call(this, scenario.tags, state.feature.name)
)
.then(() =>
indexedSteps.forEach((step) =>
stepTest.call(this, state, step, rowData)
)
)
.then(() => state.onFinishScenario(scenario));
});
} else {
// eslint-disable-next-line func-names,prefer-arrow-callback
it(scenario.name, function () {
// register this scenario with the cucumber data collector
// but don't run it
// Tell mocha this is a skipped test so it also shows correctly in Cypress
const state = window.testState;
cy.then(() => state.onStartScenario(scenario, indexedSteps))
.then(() => state.onFinishScenario(scenario))
// eslint-disable-next-line func-names
.then(function () {
return this.skip();
});
});
}
};
const cleanupFilename = (s) => s.split(".")[0];
const writeCucumberJsonFile = (json) => {
const outputFolder =
window.cucumberJson.outputFolder || "cypress/cucumber-json";
const outputPrefix = window.cucumberJson.filePrefix || "";
const outputSuffix = window.cucumberJson.fileSuffix || ".cucumber";
const fileName = json[0] ? cleanupFilename(json[0].uri) : "empty";
const outFile = `${outputFolder}/${outputPrefix}${fileName}${outputSuffix}.json`;
cy.writeFile(outFile, json, { log: false });
};
const createTestFromScenarios = (
allScenarios,
backgroundSection,
testState
) => {
// eslint-disable-next-line func-names, prefer-arrow-callback
before(function () {
cy.then(() => testState.onStartTest());
});
// ctx is cleared between each 'it'
// eslint-disable-next-line func-names, prefer-arrow-callback
beforeEach(function () {
window.testState = testState;
const failHandler = (_, err) => {
testState.onFail(err);
};
Cypress.mocha.getRunner().on("fail", failHandler);
});
allScenarios.forEach((section) => {
if (section.examples) {
const hasEnvTags = !!getEnvTags();
section.examples.forEach((example) => {
const exampleValues = [];
const exampleLocations = [];
const shouldRunExamples =
!hasEnvTags ||
shouldProceedCurrentStep(example.tags.concat(section.tags));
example.tableBody.forEach((row, rowIndex) => {
exampleLocations[rowIndex] = row.location;
example.tableHeader.cells.forEach((header, headerIndex) => {
exampleValues[rowIndex] = {
...exampleValues[rowIndex],
[header.value]: row.cells[headerIndex].value,
};
});
});
exampleValues.forEach((rowData, index) => {
// eslint-disable-next-line prefer-arrow-callback
const scenarioName = replaceParameterTags(rowData, section.name);
const uniqueScenarioName = `${scenarioName} (example #${index + 1})`;
const exampleSteps = section.steps.map((step) => {
const newStep = { ...step };
newStep.text = replaceParameterTags(rowData, newStep.text);
return newStep;
});
const stepsToRun = backgroundSection
? backgroundSection.steps.concat(exampleSteps)
: exampleSteps;
const scenarioExample = {
...section,
// tags on scenario's should be inherited by examples (https://cucumber.io/docs/cucumber/api/#tags)
tags: example.tags.concat(section.tags),
shouldRun: shouldRunExamples,
name: uniqueScenarioName,
example: exampleLocations[index],
};
runTest.call(this, scenarioExample, stepsToRun, rowData);
});
});
} else {
const stepsToRun = backgroundSection
? backgroundSection.steps.concat(section.steps)
: section.steps;
runTest.call(this, section, stepsToRun);
}
});
// eslint-disable-next-line func-names, prefer-arrow-callback
after(function () {
cy.then(() => testState.onFinishTest()).then(() => {
if (window.cucumberJson && window.cucumberJson.generate) {
const json = generateCucumberJson(testState);
writeCucumberJsonFile(json);
}
});
});
// eslint-disable-next-line func-names, prefer-arrow-callback
afterEach(function () {
if (testState.currentScenario && testState.currentScenario.shouldRun) {
cy.then(() =>
resolveAndRunAfterHooks.call(
this,
testState.currentScenario.tags,
testState.feature.name
)
);
}
});
};
module.exports = {
createTestFromScenarios,
};

View File

@@ -0,0 +1,48 @@
const { CucumberDataCollector } = require("./cukejson/cucumberDataCollector");
const { createTestFromScenarios } = require("./createTestFromScenario");
const { shouldProceedCurrentStep, getEnvTags } = require("./tagsHelper");
const createTestsFromFeature = (filePath, spec) => {
const testState = new CucumberDataCollector(filePath, spec);
const featureTags = testState.feature.tags;
const hasEnvTags = !!getEnvTags();
const anyFocused =
testState.feature.children.filter(
(section) => section.tags && section.tags.find((t) => t.name === "@focus")
).length > 0;
const backgroundSection = testState.feature.children.find(
(section) => section.type === "Background"
);
const allScenarios = testState.feature.children.filter(
(section) => section.type !== "Background"
);
// tags on features should be inherited by scenario's (https://cucumber.io/docs/cucumber/api/#tags)
allScenarios.forEach((section) => {
// eslint-disable-next-line no-param-reassign
section.tags = section.tags.concat(featureTags);
});
const scenariosToRun = allScenarios.filter((section) => {
let shouldRun;
// only just run focused if no env tags set
// https://github.com/TheBrainFamily/cypress-cucumber-example#smart-tagging
if (!hasEnvTags && anyFocused) {
shouldRun = section.tags.find((t) => t.name === "@focus");
} else {
shouldRun = !hasEnvTags || shouldProceedCurrentStep(section.tags);
}
return shouldRun;
});
// create tests for all the scenarios
// but flag only the ones that should be run
scenariosToRun.forEach((section) => {
// eslint-disable-next-line no-param-reassign
section.shouldRun = true;
});
createTestFromScenarios(allScenarios, backgroundSection, testState);
};
module.exports = {
createTestsFromFeature,
};

View File

@@ -0,0 +1,36 @@
const path = require("path");
const os = require("os");
const getPathFor = (file) => {
if (os.platform() === "win32") {
return path
.join(__dirname.replace(/\\/g, "\\\\"), file)
.replace(/\\/g, "\\\\");
}
return `${__dirname}/${file}`;
};
exports.cucumberTemplate = `
const {
resolveAndRunStepDefinition,
defineParameterType,
Given,
When,
Then,
And,
But,
Before,
After,
defineStep
} = require("${getPathFor("resolveStepDefinition")}");
window.Given = Given;
window.When = When;
window.Then = Then;
window.And = And;
window.But = But;
window.defineParameterType = defineParameterType;
window.defineStep = defineStep;
const {
createTestsFromFeature
} = require("${getPathFor("createTestsFromFeature")}");
`;

View File

@@ -0,0 +1,165 @@
const { Parser } = require("gherkin");
const statuses = require("cucumber/lib/status").default;
class CucumberDataCollector {
constructor(uri, spec) {
this.feature = new Parser().parse(spec.toString()).feature;
this.scenarioSteps = {};
this.runScenarios = {};
this.runTests = {};
this.stepResults = {};
this.testError = null;
this.uri = uri;
this.spec = spec;
this.currentScenario = null;
this.currentStep = 0;
this.timer = Date.now();
this.logStep = (step) => {
Cypress.log({
name: "step",
displayName: step.keyword,
message: `**${step.text}**`,
consoleProps: () => ({ feature: this.uri, step }),
});
};
this.onStartTest = () => {};
this.onFinishTest = () => {
if (this.testError) {
this.attachErrorToFailingStep();
}
};
this.onStartScenario = (scenario, stepsToRun) => {
this.currentScenario = scenario;
this.currentStep = 0;
this.stepResults = {};
this.scenarioSteps[scenario.name] = stepsToRun;
this.testError = null;
stepsToRun.forEach((step) => {
this.stepResults[step.index] = { status: statuses.PENDING };
});
this.runScenarios[scenario.name] = scenario;
};
this.onFinishScenario = (scenario) => {
this.markStillPendingStepsAsSkipped(scenario);
this.recordScenarioResult(scenario);
};
this.onStartStep = (step) => {
this.currentStep = step.index;
this.setStepToPending(step);
this.logStep(step);
};
this.onFinishStep = (step, result) => {
this.recordStepResult(step, result);
};
this.onFail = (err) => {
this.testError = err;
if (
err.message &&
err.message.indexOf("Step implementation missing for") > -1
) {
this.stepResults[this.currentStep] = {
status: statuses.UNDEFINED,
duration: this.timeTaken(),
};
} else if (err.constructor.name === "Pending") {
// cypress marks skipped mocha tests as pending
// https://github.com/cypress-io/cypress/issues/3092
// don't record this error and mark the step as skipped
this.stepResults[this.currentStep] = {
status: statuses.SKIPPED,
duration: this.timeTaken(),
};
} else {
this.stepResults[this.currentStep] = {
status: statuses.FAILED,
duration: this.timeTaken(),
exception: this.testError,
};
}
this.onFinishScenario(this.currentScenario);
};
this.timeTaken = () => {
const now = Date.now();
const duration = now - this.timer;
this.timer = now;
return duration;
};
this.formatTestCase = (scenario) => {
const line = scenario.example
? scenario.example.line
: scenario.location.line;
return {
sourceLocation: { uri, line },
};
};
this.attachErrorToFailingStep = () => {
Object.keys(this.runTests).forEach((test) => {
const stepResults = this.runTests[test];
Object.keys(stepResults).forEach((stepIdx) => {
const stepResult = stepResults[stepIdx];
if (stepResult.result === statuses.FAILED) {
stepResult.exception = this.testError;
}
});
});
};
this.markStillPendingStepsAsSkipped = (scenario) => {
this.runTests[scenario.name] = Object.keys(this.stepResults).map(
(key) => {
const result = this.stepResults[key];
return {
...result,
status:
result.status === statuses.PENDING
? statuses.SKIPPED
: result.status,
};
}
);
};
this.recordScenarioResult = (scenario) => {
const allSkipped = this.areAllStepsSkipped(scenario.name);
const anyFailed = this.anyStepsHaveFailed(scenario.name);
if (allSkipped) this.runTests[scenario.name].result = statuses.SKIPPED;
else
this.runTests[scenario.name].result = anyFailed
? statuses.FAILED
: statuses.PASSED;
};
this.setStepToPending = (step) => {
this.stepResults[step.index] = { status: statuses.PENDING };
};
this.recordStepResult = (step, result) => {
this.stepResults[step.index] = {
status: result,
duration: this.timeTaken(),
};
};
this.areAllStepsSkipped = (name) =>
this.runTests[name].every((e) => e.status === statuses.SKIPPED);
this.anyStepsHaveFailed = (name) =>
this.runTests[name].find((e) => e.status === statuses.FAILED) !==
undefined;
}
}
module.exports = { CucumberDataCollector };

View File

@@ -0,0 +1,160 @@
const fs = require("fs");
const statuses = require("cucumber/lib/status").default;
const { CucumberDataCollector } = require("./cucumberDataCollector");
const { generateCucumberJson } = require("./generateCucumberJson");
window.cucumberJson = { generate: true };
const assertCucumberJson = (json, expectedResults) => {
expect(json).to.have.length(1);
expect(json[0].keyword).to.eql("Feature");
expect(json[0].name).to.eql("Being a plugin");
expect(json[0].elements).to.have.length(1);
expect(json[0].elements[0].keyword).to.eql("Scenario");
expect(json[0].elements[0].name).to.eql("Basic example");
expect(json[0].elements[0].steps).to.have.length(3);
expect(json[0].elements[0].steps[0].keyword).to.equal("Given ");
expect(json[0].elements[0].steps[0].name).to.equal(
"a feature and a matching step definition file"
);
// eslint-disable-next-line no-unused-expressions
expect(json[0].elements[0].steps[0].result).to.be.not.null;
expect(json[0].elements[0].steps[0].result.status).to.eql(expectedResults[0]);
expect(json[0].elements[0].steps[1].keyword).to.equal("When ");
expect(json[0].elements[0].steps[1].name).to.equal("I run cypress tests");
// eslint-disable-next-line no-unused-expressions
expect(json[0].elements[0].steps[1].result).to.be.not.null;
expect(json[0].elements[0].steps[1].result.status).to.eql(expectedResults[1]);
expect(json[0].elements[0].steps[2].keyword).to.equal("Then ");
expect(json[0].elements[0].steps[2].name).to.equal("they run properly");
// eslint-disable-next-line no-unused-expressions
expect(json[0].elements[0].steps[2].result).to.be.not.null;
expect(json[0].elements[0].steps[2].result.status).to.eql(expectedResults[2]);
};
describe("Cucumber Data Collector", () => {
const scenario = {
type: "Scenario",
tags: [],
location: { line: 7, column: 3 },
keyword: "Scenario",
name: "Basic example",
steps: [
{
type: "Step",
location: { line: 8, column: 5 },
keyword: "Given ",
text: "a feature and a matching step definition file",
},
{
type: "Step",
location: { line: 9, column: 5 },
keyword: "When ",
text: "I run cypress tests",
},
{
type: "Step",
location: { line: 10, column: 5 },
keyword: "Then ",
text: "they run properly",
},
],
};
const stepsToRun = [
{
type: "Step",
location: { line: 8, column: 5 },
keyword: "Given ",
text: "a feature and a matching step definition file",
index: 0,
},
{
type: "Step",
location: { line: 9, column: 5 },
keyword: "When ",
text: "I run cypress tests",
index: 1,
},
{
type: "Step",
location: { line: 10, column: 5 },
keyword: "Then ",
text: "they run properly",
index: 2,
},
];
beforeEach(() => {
const filePath = "./cypress/integration/Plugin.feature";
const spec = fs.readFileSync(filePath);
this.testState = new CucumberDataCollector(filePath, spec);
this.testState.onStartTest();
});
it("runs", () => {
this.testState.onFinishTest();
const json = generateCucumberJson(this.testState);
expect(json).to.have.length(0);
});
it("records pending scenarios", () => {
this.testState.onStartScenario(scenario, stepsToRun);
this.testState.onFinishScenario(scenario);
this.testState.onFinishTest();
const json = generateCucumberJson(this.testState);
assertCucumberJson(json, [
statuses.SKIPPED,
statuses.SKIPPED,
statuses.SKIPPED,
]);
});
it("records passed scenarios", () => {
this.testState.onStartScenario(scenario, stepsToRun);
this.testState.onStartStep(stepsToRun[0]);
this.testState.onFinishStep(stepsToRun[0], statuses.PASSED);
this.testState.onStartStep(stepsToRun[1]);
this.testState.onFinishStep(stepsToRun[1], statuses.PASSED);
this.testState.onStartStep(stepsToRun[2]);
this.testState.onFinishStep(stepsToRun[2], statuses.PASSED);
this.testState.onFinishScenario(scenario);
this.testState.onFinishTest();
const json = generateCucumberJson(this.testState);
assertCucumberJson(json, [
statuses.PASSED,
statuses.PASSED,
statuses.PASSED,
]);
});
it("records failed scenarios", () => {
this.testState.onStartScenario(scenario, stepsToRun);
this.testState.onStartStep(stepsToRun[0]);
this.testState.onFinishStep(stepsToRun[0], statuses.PASSED);
this.testState.onStartStep(stepsToRun[1]);
this.testState.onFinishStep(stepsToRun[1], statuses.FAILED);
this.testState.onFinishScenario(scenario);
this.testState.onFinishTest();
const json = generateCucumberJson(this.testState);
assertCucumberJson(json, [
statuses.PASSED,
statuses.FAILED,
statuses.SKIPPED,
]);
});
it("handles missing steps", () => {
this.testState.onStartScenario(scenario, stepsToRun);
this.testState.onStartStep(stepsToRun[0]);
this.testState.onFinishStep(stepsToRun[0], statuses.PASSED);
this.testState.onStartStep(stepsToRun[1]);
this.testState.onFinishStep(stepsToRun[1], statuses.UNDEFINED);
this.testState.onFinishScenario(scenario);
this.testState.onFinishTest();
const json = generateCucumberJson(this.testState);
assertCucumberJson(json, [
statuses.PASSED,
statuses.UNDEFINED,
statuses.SKIPPED,
]);
});
});

View File

@@ -0,0 +1,72 @@
const { EventEmitter } = require("events");
const { generateEvents } = require("gherkin");
const JsonFormatter = require("cucumber/lib/formatter/json_formatter").default;
const formatterHelpers = require("cucumber/lib/formatter/helpers");
function generateCucumberJson(state) {
let output = "";
const logFn = (data) => {
output += data;
};
const eventBroadcaster = new EventEmitter();
function storePickle({ pickle, uri }) {
eventBroadcaster.emit("pickle-accepted", { pickle, uri });
}
eventBroadcaster.on("pickle", storePickle);
// eslint-disable-next-line no-new
new JsonFormatter({
eventBroadcaster,
eventDataCollector: new formatterHelpers.EventDataCollector(
eventBroadcaster
),
log: logFn,
});
// Start feeding the recorded test run into the JsonFormatter
// Feed in the static test structure
generateEvents(state.spec.toString(), state.uri).forEach((event) => {
eventBroadcaster.emit(event.type, event);
});
// Feed in the results from the recorded scenarios and steps
Object.keys(state.runTests).forEach((test) => {
const scenario = state.runScenarios[test];
const stepResults = state.runTests[test];
const stepsToRun = state.scenarioSteps[test];
const steps = stepsToRun.map((step) => ({
sourceLocation: { uri: state.uri, line: step.location.line },
}));
eventBroadcaster.emit("test-case-prepared", {
sourceLocation: state.formatTestCase(scenario).sourceLocation,
steps,
});
stepResults.forEach((stepResult, stepIdx) => {
eventBroadcaster.emit("test-step-prepared", {
index: stepIdx,
testCase: state.formatTestCase(scenario),
});
eventBroadcaster.emit("test-step-finished", {
index: stepIdx,
testCase: state.formatTestCase(scenario),
result: stepResult,
});
if (stepResult.attachment) {
eventBroadcaster.emit("test-step-attachment", stepResult.attachment);
}
});
eventBroadcaster.emit("test-case-finished", {
sourceLocation: state.formatTestCase(scenario).sourceLocation,
result: state.runTests[scenario.name].result,
});
});
eventBroadcaster.emit("test-run-finished", {});
return JSON.parse(output);
}
module.exports = { generateCucumberJson };

View File

@@ -0,0 +1,59 @@
const minimist = require("minimist");
const dargs = require("dargs");
const minimistConfig = {
alias: { g: "glob" },
string: ["g"],
};
function stripCLIArguments(argsToRemove = []) {
const command = process.argv[2];
const userOptions = process.argv.slice(3);
const minimistArgs = minimist(userOptions);
const argsAndAliasesToExclude =
argsToRemove.length > 0
? Object.entries(minimistConfig.alias)
.map(([key, value]) => [key, value].flat())
.filter((aliasedOption) =>
aliasedOption.some((option) => argsToRemove.includes(option))
)
.flat()
: [];
return [
command,
...dargs(minimistArgs, {
excludes: [...new Set([...argsAndAliasesToExclude, ...argsToRemove])],
useEquals: false,
}),
];
}
/**
* Users will be expected to pass args by --glob/-g to avoid issues related to commas in those parameters.
*/
function parseArgsOrDefault(argPrefix, defaultValue) {
const matchedArg = process.argv
.slice(2)
.find((arg) => arg.includes(`${argPrefix}=`));
// Cypress requires env vars to be passed as comma separated list
// otherwise it only accepts the last provided variable,
// the way we replace here accomodates for that.
const argValue = matchedArg
? matchedArg.replace(new RegExp(`.*${argPrefix}=`), "").replace(/,.*/, "")
: "";
return argValue !== "" ? argValue : defaultValue;
}
function getGlobArg() {
const args = minimist(process.argv.slice(2), minimistConfig);
return args.g || parseArgsOrDefault("GLOB", false);
}
module.exports = {
stripCLIArguments,
parseArgsOrDefault,
getGlobArg,
};

View File

@@ -0,0 +1,82 @@
const { stripCLIArguments, getGlobArg } = require("./cypressTagsHelpers");
const processArgv = process.argv;
describe("stripCLIArguments function should return correct args array", () => {
beforeEach(() => {
process.argv = ["path/to/node", "path/to/file", "run"];
});
test("when only target option is present", () => {
process.argv.push(...["-g", "cypress/integration/**/*.feature"]);
expect(stripCLIArguments(["g"])).to.eql(["run"]);
});
test("when target option is a word", () => {
process.argv.push(...["--glob", "cypress/integration/**/*.feature"]);
expect(stripCLIArguments(["glob"])).to.eql(["run"]);
});
test("when option doesn't have a value tied to it", () => {
process.argv.push(...["-g"]);
expect(stripCLIArguments(["g"])).to.eql(["run"]);
});
test("when there are multiple options but only one is target for removal", () => {
process.argv.push(...["-g", "-t"]);
expect(stripCLIArguments(["g"])).to.eql(["run", "-t"]);
});
test("when there are multiple options to remove", () => {
process.argv.push(...["-g", "-t", "cypress tags string"]);
expect(stripCLIArguments(["g", "t"])).to.eql(["run"]);
});
test("when option is coupled with other ones like -tg", () => {
process.argv.push(...["-tg", "cypress/integration/**/*.feature"]);
expect(stripCLIArguments(["t"])).to.eql([
"run",
"-g",
"cypress/integration/**/*.feature",
]);
});
test("when option is coupled with other ones like -gt where t has a value", () => {
process.argv.push(...["-gt", "cypress tags string"]);
expect(stripCLIArguments(["t"])).to.eql(["run", "-g"]);
});
afterEach(() => {
process.argv = processArgv;
});
});
describe("getGlobArg function should return", () => {
beforeEach(() => {
process.argv = ["path/to/node", "path/to/file", "run"];
});
test("glob pattern when using -g or --glob option", () => {
process.argv.push(...["-g", "cypress/integration/**/*.feature"]);
expect(getGlobArg()).to.equal("cypress/integration/**/*.feature");
});
test("glob pattern containing commas when using -g option", () => {
process.argv.push(...["-g", "cypress/integration/**/1,2*.feature"]);
expect(getGlobArg()).to.equal("cypress/integration/**/1,2*.feature");
});
test("glob pattern containing braced sections when using -g option", () => {
process.argv.push(...["-g", "cypress/integration/**/{1,2}*.feature"]);
expect(getGlobArg()).to.equal("cypress/integration/**/{1,2}*.feature");
});
test("glob pattern when using env variables GLOB=", () => {
process.argv.push(...["-e", "GLOB=cypress/integration/**/*.feature"]);
expect(getGlobArg()).to.equal("cypress/integration/**/*.feature");
});
afterEach(() => {
process.argv = processArgv;
});
});

View File

@@ -0,0 +1,123 @@
const glob = require("glob");
const path = require("path");
const fs = require("fs");
const { Parser } = require("gherkin");
const log = require("debug")("cypress:cucumber");
const jsStringEscape = require("js-string-escape");
const { getStepDefinitionsPaths } = require("./getStepDefinitionsPaths");
const { cucumberTemplate } = require("./cucumberTemplate");
const { getCucumberJsonConfig } = require("./getCucumberJsonConfig");
const {
isNonGlobalStepDefinitionsMode,
} = require("./isNonGlobalStepDefinitionsMode");
const createCucumber = (
specs,
globalToRequire,
nonGlobalToRequire,
cucumberJson
) =>
`
${cucumberTemplate}
window.cucumberJson = ${JSON.stringify(cucumberJson)};
var moduleCache = arguments[5];
function clearFromCache(moduleId, instance){
if(isWebpack()){
delete require.cache[moduleId];
} else {
clearFromCacheBrowserify(instance);
}
}
function isWebpack(){
return !!require.cache
}
// Stolen from https://github.com/browserify/browserify/issues/1444
function clearFromCacheBrowserify(instance) {
for(const key in moduleCache)
{
if(moduleCache[key].exports == instance)
{
delete moduleCache[key];
return;
}
}
throw new Error("could not clear instance from browserify module cache");
}
${globalToRequire.join("\n")}
${specs
.map(
({ spec, filePath, name }) => `
describe(\`${name}\`, function() {
window.currentFeatureName = \`${name}\`
${
nonGlobalToRequire &&
nonGlobalToRequire
.find((fileSteps) => fileSteps[filePath])
[filePath].join("\n")
}
createTestsFromFeature('${path.basename(filePath)}', '${jsStringEscape(
spec
)}');
})
`
)
.join("\n")}
`;
// eslint-disable-next-line func-names
module.exports = function (_, filePath = this.resourcePath) {
log("compiling", filePath);
const features = glob.sync(`${path.dirname(filePath)}/**/*.feature`);
let globalStepDefinitionsToRequire = [];
let nonGlobalStepDefinitionsToRequire;
if (isNonGlobalStepDefinitionsMode()) {
nonGlobalStepDefinitionsToRequire = features.map((featurePath) => ({
[featurePath]: getStepDefinitionsPaths(featurePath).map(
(sdPath) =>
`clearFromCache(require.resolve && require.resolve('${sdPath}'), require('${sdPath}'))`
),
}));
} else {
globalStepDefinitionsToRequire = [
...new Set(
features.reduce(
(requires) =>
requires.concat(
getStepDefinitionsPaths(filePath).map(
(sdPath) => `require('${sdPath}')`
)
),
[]
)
),
];
}
const specs = features
.map((featurePath) => ({
spec: fs.readFileSync(featurePath).toString(),
filePath: featurePath,
}))
.map((feature) => ({
...feature,
name: new Parser().parse(feature.spec.toString()).feature.name,
}));
return createCucumber(
specs,
globalStepDefinitionsToRequire,
nonGlobalStepDefinitionsToRequire,
getCucumberJsonConfig()
);
};

View File

@@ -0,0 +1,15 @@
const cosmiconfig = require("cosmiconfig");
let explorer;
exports.getConfig = () => {
if (!explorer) {
explorer = cosmiconfig("cypress-cucumber-preprocessor", {
sync: true,
rcExtensions: true,
});
}
const loaded = explorer.load();
return loaded && loaded.config;
};

View File

@@ -0,0 +1,11 @@
const log = require("debug")("cypress:cucumber");
const { getConfig } = require("./getConfig");
exports.getCucumberJsonConfig = () => {
const config = getConfig();
const cucumberJson =
config && config.cucumberJson ? config.cucumberJson : { generate: false };
log("cucumber.json", JSON.stringify(cucumberJson));
return cucumberJson;
};

View File

@@ -0,0 +1,6 @@
const path = require("path");
module.exports = {
getStepDefinitionPathsFrom: (filePath) =>
filePath.replace(path.extname(filePath), ""),
};

View File

@@ -0,0 +1,7 @@
const { getStepDefinitionPathsFrom } = require("./getStepDefinitionPathsFrom");
test("getStepDefinitionPathsFrom", () => {
expect(
getStepDefinitionPathsFrom("/home/lgandecki/someComplex_.feature")
).equal("/home/lgandecki/someComplex_");
});

View File

@@ -0,0 +1,40 @@
const path = require("path");
const glob = require("glob");
const process = require("process");
const { getConfig } = require("./getConfig");
const stepDefinitionPath = require("./stepDefinitionPath.js");
const { getStepDefinitionPathsFrom } = require("./getStepDefinitionPathsFrom");
const getStepDefinitionsPaths = (filePath) => {
const appRoot = process.cwd();
let paths = [];
const config = getConfig();
if (config && config.nonGlobalStepDefinitions) {
let nonGlobalPath = getStepDefinitionPathsFrom(filePath);
let commonPath = config.commonPath || `${stepDefinitionPath()}/common/`;
if (config.commonPath) {
commonPath = `${path.resolve(appRoot, commonPath)}/`;
}
if (config.nonGlobalStepBaseDir) {
const stepBase = `${appRoot}/${config.nonGlobalStepBaseDir}`;
nonGlobalPath = nonGlobalPath.replace(stepDefinitionPath(), stepBase);
commonPath = `${stepBase}/${config.commonPath || "common/"}`;
}
const nonGlobalPattern = `${nonGlobalPath}/**/*.+(js|ts|tsx)`;
const commonDefinitionsPattern = `${commonPath}**/*.+(js|ts|tsx)`;
paths = paths.concat(
glob.sync(nonGlobalPattern),
glob.sync(commonDefinitionsPattern)
);
} else {
const pattern = `${stepDefinitionPath()}/**/*.+(js|ts|tsx)`;
paths = paths.concat(glob.sync(pattern));
}
return paths;
};
module.exports = { getStepDefinitionsPaths };

View File

@@ -0,0 +1,99 @@
// file.only
/* eslint-disable global-require */
jest.mock("./stepDefinitionPath.js", () => () => "stepDefinitionPath");
jest.mock("glob", () => ({
sync(pattern) {
return pattern;
},
}));
let getConfig;
describe("getStepDefinitionsPaths", () => {
beforeEach(() => {
jest.resetModules();
({ getConfig } = require("./getConfig"));
jest.unmock("path");
jest.mock("./getConfig");
});
it("should return the default common folder", () => {
getConfig.mockReturnValue({
nonGlobalStepDefinitions: true,
});
const { getStepDefinitionsPaths } = require("./getStepDefinitionsPaths");
const actual = getStepDefinitionsPaths("/path");
const expected = "stepDefinitionPath/common/**/*.+(js|ts|tsx)";
expect(actual).to.include(expected);
});
it("should return the common folder defined by the developer", () => {
jest.spyOn(process, "cwd").mockImplementation(() => "/cwd/");
getConfig.mockReturnValue({
nonGlobalStepDefinitions: true,
commonPath: "myPath/",
});
const { getStepDefinitionsPaths } = require("./getStepDefinitionsPaths");
const actual = getStepDefinitionsPaths("/path");
const expected = "/cwd/myPath/**/*.+(js|ts|tsx)";
expect(actual).to.include(expected);
});
it("should return the default non global step definition pattern", () => {
getConfig.mockReturnValue({
nonGlobalStepDefinitions: true,
});
// eslint-disable-next-line global-require
const { getStepDefinitionsPaths } = require("./getStepDefinitionsPaths");
const path = "stepDefinitionPath/test.feature";
const actual = getStepDefinitionsPaths(path);
const expected = "stepDefinitionPath/test/**/*.+(js|ts|tsx)";
expect(actual).to.include(expected);
});
describe("nonGlobalStepBaseDir is defined", () => {
const path = "stepDefinitionPath/test.feature";
const config = {
nonGlobalStepDefinitions: true,
nonGlobalStepBaseDir: "nonGlobalStepBaseDir",
};
beforeEach(() => {
jest.spyOn(process, "cwd").mockImplementation(() => "cwd");
});
it("should return the overriden non global step definition pattern and default common folder", () => {
getConfig.mockReturnValue(config);
const { getStepDefinitionsPaths } = require("./getStepDefinitionsPaths");
const actual = getStepDefinitionsPaths(path);
const expectedNonGlobalDefinitionPattern =
"cwd/nonGlobalStepBaseDir/test/**/*.+(js|ts|tsx)";
const expectedCommonPath =
"cwd/nonGlobalStepBaseDir/common/**/*.+(js|ts|tsx)";
expect(actual).to.include(expectedNonGlobalDefinitionPattern);
expect(actual).to.include(expectedCommonPath);
expect(actual).to.not.include(
"stepDefinitionPath/test/**/*.+(js|ts|tsx)"
);
});
it("should return common folder defined by the dev and based on nonGlobalStepBaseDir", () => {
getConfig.mockReturnValue({ ...config, commonPath: "commonPath/" });
const { getStepDefinitionsPaths } = require("./getStepDefinitionsPaths");
const actual = getStepDefinitionsPaths(path);
const expectedCommonPath =
"cwd/nonGlobalStepBaseDir/commonPath/**/*.+(js|ts|tsx)";
expect(actual).to.include(expectedCommonPath);
});
});
});

View File

@@ -0,0 +1,27 @@
const { shouldProceedCurrentStep } = require("./tagsHelper");
class HookRegistry {
constructor() {
this.definitions = [];
this.runtime = {};
this.runtime = (tags, implementation) => {
this.definitions.push({
tags,
implementation,
featureName: window.currentFeatureName || "___GLOBAL_EXECUTION___",
});
};
this.resolve = (scenarioTags, runningFeatureName) =>
this.definitions.filter(
({ tags, featureName }) =>
(!tags ||
tags.length === 0 ||
shouldProceedCurrentStep(scenarioTags, tags)) &&
(runningFeatureName === featureName ||
featureName === "___GLOBAL_EXECUTION___")
);
}
}
exports.HookRegistry = HookRegistry;

View File

@@ -0,0 +1,97 @@
/* eslint-disable no-eval */
const fs = require("fs");
const through = require("through");
const browserify = require("@cypress/browserify-preprocessor");
const log = require("debug")("cypress:cucumber");
const chokidar = require("chokidar");
const compile = require("./loader.js");
const compileFeatures = require("./featuresLoader.js");
const stepDefinitionPath = require("./stepDefinitionPath.js");
const transform = (file) => {
let data = "";
function write(buf) {
data += buf;
}
function end() {
if (file.match(".features$")) {
log("compiling features ", file);
this.queue(compileFeatures(data, file));
} else if (file.match(".feature$")) {
log("compiling feature ", file);
this.queue(compile(data, file));
} else {
this.queue(data);
}
this.queue(null);
}
return through(write, end);
};
const touch = (filename) => {
fs.utimesSync(filename, new Date(), new Date());
};
let watcher;
// Include our transform in Browserify options
const wrapOptions = (options) => {
let wrappedTransform;
if (
!options.browserifyOptions ||
!Array.isArray(options.browserifyOptions.transform)
) {
wrappedTransform = [transform];
} else if (!options.browserifyOptions.transform.includes(transform)) {
wrappedTransform = [transform, ...options.browserifyOptions.transform];
} else {
wrappedTransform = options.browserifyOptions.transform;
}
return {
...options,
browserifyOptions: {
...(options.browserifyOptions || {}),
transform: wrappedTransform,
},
};
};
const preprocessor = (options = browserify.defaultOptions) => {
if (typeof options !== "object") {
throw new Error("Preprocessor options must be an object");
}
const wrappedOptions = wrapOptions(options);
return async (file) => {
if (file.shouldWatch) {
if (watcher) {
watcher.close();
}
watcher = chokidar
.watch(
[
`${stepDefinitionPath()}*.js`,
`${stepDefinitionPath()}*.ts`,
`${stepDefinitionPath()}*.tsx`,
],
{
ignoreInitial: true,
}
)
.on("all", () => {
touch(file.filePath);
});
}
return browserify(wrappedOptions)(file);
};
};
module.exports = {
default: preprocessor,
transform,
};

View File

@@ -0,0 +1,98 @@
const browserify = require("@cypress/browserify-preprocessor");
const { default: preprocessor, transform } = require(".");
jest.mock("@cypress/browserify-preprocessor");
const { defaultOptions } = jest.requireActual(
"@cypress/browserify-preprocessor"
);
describe("Preprocessor", () => {
beforeEach(() => {
browserify.defaultOptions = defaultOptions;
browserify.mockReturnValue(() => {});
});
afterEach(() => {
browserify.mockClear();
});
it("should use Cypress browserify options by default", async () => {
await preprocessor()({ shouldWatch: false });
global.jestExpect(browserify).toHaveBeenCalledWith({
...defaultOptions,
browserifyOptions: {
...defaultOptions.browserifyOptions,
transform: [transform, ...defaultOptions.browserifyOptions.transform],
},
});
});
it("should add transform when other transforms are defined in options", async () => {
await preprocessor({
browserifyOptions: {
transform: ["babelify"],
},
})({ shouldWatch: false });
global.jestExpect(browserify).toHaveBeenCalledWith({
browserifyOptions: {
transform: [transform, "babelify"],
},
});
});
it("should preserve transforms in options when our transform is already included", async () => {
const options = {
browserifyOptions: {
extensions: ["js", "ts"],
plugins: [["tsify"]],
transform: ["aliasify", transform, "babelify"],
},
};
await preprocessor(options)({ shouldWatch: false });
global.jestExpect(browserify).toHaveBeenCalledWith(options);
});
it("should add our transform when no other transforms are defined in options", async () => {
const options = {
browserifyOptions: {
extensions: ["js", "ts"],
plugins: [["tsify"]],
},
};
await preprocessor(options)({ shouldWatch: false });
global.jestExpect(browserify).toHaveBeenCalledWith({
...options,
browserifyOptions: {
...options.browserifyOptions,
transform: [transform],
},
});
});
it("should add our transform when browserifyOptions property is not passed to options", async () => {
const options = { unsupported: true };
await preprocessor(options)({ shouldWatch: false });
global.jestExpect(browserify).toHaveBeenCalledWith({
...options,
browserifyOptions: {
transform: [transform],
},
});
});
it("should fail gracefully when options is not an object", () => {
const err = new Error("Preprocessor options must be an object");
global.jestExpect(() => preprocessor("options")).toThrow(err);
global.jestExpect(() => preprocessor(1)).toThrow(err);
global.jestExpect(() => preprocessor(false)).toThrow(err);
});
});

View File

@@ -0,0 +1,6 @@
const { getConfig } = require("./getConfig");
exports.isNonGlobalStepDefinitionsMode = () => {
const config = getConfig();
return config && config.nonGlobalStepDefinitions;
};

View File

@@ -0,0 +1,37 @@
const log = require("debug")("cypress:cucumber");
const path = require("path");
const { Parser } = require("gherkin");
const jsStringEscape = require("js-string-escape");
const { getStepDefinitionsPaths } = require("./getStepDefinitionsPaths");
const { cucumberTemplate } = require("./cucumberTemplate");
const { getCucumberJsonConfig } = require("./getCucumberJsonConfig");
// This is the template for the file that we will send back to cypress instead of the text of a
// feature file
const createCucumber = (filePath, cucumberJson, spec, toRequire, name) =>
`
${cucumberTemplate}
window.cucumberJson = ${JSON.stringify(cucumberJson)};
describe(\`${name}\`, function() {
${toRequire.join("\n")}
createTestsFromFeature('${filePath}', '${jsStringEscape(spec)}');
});
`;
// eslint-disable-next-line func-names
module.exports = function (spec, filePath = this.resourcePath) {
log("compiling", spec);
const stepDefinitionsToRequire = getStepDefinitionsPaths(filePath).map(
(sdPath) => `require('${sdPath}')`
);
const { name } = new Parser().parse(spec.toString()).feature;
return createCucumber(
path.basename(filePath),
getCucumberJsonConfig(),
spec,
stepDefinitionsToRequire,
name
);
};

View File

@@ -0,0 +1,228 @@
const DataTable = require("cucumber/lib/models/data_table").default;
const {
defineParameterType,
} = require("cucumber/lib/support_code_library_builder/define_helpers");
const {
CucumberExpression,
RegularExpression,
ParameterTypeRegistry,
} = require("cucumber-expressions");
const { HookRegistry } = require("./hookRegistry");
class StepDefinitionRegistry {
constructor() {
this.definitions = {};
this.runtime = {};
this.options = {
parameterTypeRegistry: new ParameterTypeRegistry(),
};
this.definitions = [];
this.runtime = (...args) => {
let matcher;
let config;
let implementation;
if (args.length > 2) {
[matcher, config, implementation] = args;
} else {
[matcher, implementation] = args;
}
let expression;
if (matcher instanceof RegExp) {
expression = new RegularExpression(
matcher,
this.options.parameterTypeRegistry
);
} else {
expression = new CucumberExpression(
matcher,
this.options.parameterTypeRegistry
);
}
this.definitions.push({
implementation,
expression,
config,
featureName: window.currentFeatureName || "___GLOBAL_EXECUTION___",
});
};
this.resolve = (type, text, runningFeatureName) =>
this.definitions.filter(
({ expression, featureName }) =>
expression.match(text) &&
(runningFeatureName === featureName ||
featureName === "___GLOBAL_EXECUTION___")
)[0];
}
}
const stepDefinitionRegistry = new StepDefinitionRegistry();
const beforeHookRegistry = new HookRegistry();
const afterHookRegistry = new HookRegistry();
function resolveStepDefinition(step, featureName) {
const stepDefinition = stepDefinitionRegistry.resolve(
step.keyword.toLowerCase().trim(),
step.text,
featureName
);
return stepDefinition || {};
}
function storeTemplateRowsOnArgumentIfNotPresent(argument) {
return !argument.templateRows
? { ...argument, templateRows: argument.rows }
: argument;
}
function applyExampleData(argument, exampleRowData, replaceParameterTags) {
const argumentWithTemplateRows = storeTemplateRowsOnArgumentIfNotPresent(
argument
);
const scenarioDataTableRows = argumentWithTemplateRows.templateRows.map(
(tr) => {
if (!(tr && tr.type === "TableRow")) {
return tr;
}
const cells = {
cells: tr.cells.map((c) => {
const value = {
value: replaceParameterTags(exampleRowData, c.value),
};
return { ...c, ...value };
}),
};
return { ...tr, ...cells };
}
);
return { ...argumentWithTemplateRows, rows: scenarioDataTableRows };
}
function resolveStepArgument(argument, exampleRowData, replaceParameterTags) {
if (!argument) {
return argument;
}
if (argument.type === "DataTable") {
if (!exampleRowData) {
return new DataTable(argument);
}
const argumentWithAppliedExampleData = applyExampleData(
argument,
exampleRowData,
replaceParameterTags
);
return new DataTable(argumentWithAppliedExampleData);
}
if (argument.type === "DocString") {
if (exampleRowData) {
return replaceParameterTags(exampleRowData, argument.content);
}
return argument.content;
}
return argument;
}
function resolveAndRunHooks(hookRegistry, scenarioTags, featureName) {
return window.Cypress.Promise.each(
hookRegistry.resolve(scenarioTags, featureName),
({ implementation }) => implementation.call(this)
);
}
function parseHookArgs(args) {
if (args.length === 2) {
if (typeof args[0] !== "object" || typeof args[0].tags !== "string") {
throw new Error(
"Hook definitions with two arguments should have an object containing tags (string) as the first argument."
);
}
if (typeof args[1] !== "function") {
throw new Error(
"Hook definitions with two arguments must have a function as the second argument."
);
}
return {
tags: args[0].tags,
implementation: args[1],
};
}
if (typeof args[0] !== "function") {
throw new Error(
"Hook definitions with one argument must have a function as the first argument."
);
}
return {
implementation: args[0],
};
}
module.exports = {
resolveStepDefinition(step, featureName) {
return resolveStepDefinition(step, featureName);
},
resolveAndRunBeforeHooks(scenarioTags, featureName) {
return resolveAndRunHooks(beforeHookRegistry, scenarioTags, featureName);
},
resolveAndRunAfterHooks(scenarioTags, featureName) {
return resolveAndRunHooks(afterHookRegistry, scenarioTags, featureName);
},
// eslint-disable-next-line func-names
resolveAndRunStepDefinition(
step,
replaceParameterTags,
exampleRowData,
featureName
) {
const { expression, implementation } = resolveStepDefinition(
step,
featureName
);
const stepText = step.text;
if (expression && implementation) {
const argument = resolveStepArgument(
step.argument,
exampleRowData,
replaceParameterTags
);
return implementation.call(
this,
...expression.match(stepText).map((match) => match.getValue()),
argument
);
}
throw new Error(`Step implementation missing for: ${stepText}`);
},
Given: (...args) => {
stepDefinitionRegistry.runtime(...args);
},
When: (...args) => {
stepDefinitionRegistry.runtime(...args);
},
Then: (...args) => {
stepDefinitionRegistry.runtime(...args);
},
And: (...args) => {
stepDefinitionRegistry.runtime(...args);
},
But: (...args) => {
stepDefinitionRegistry.runtime(...args);
},
Before: (...args) => {
const { tags, implementation } = parseHookArgs(args);
beforeHookRegistry.runtime(tags, implementation);
},
After: (...args) => {
const { tags, implementation } = parseHookArgs(args);
afterHookRegistry.runtime(tags, implementation);
},
defineStep: (expression, implementation) => {
stepDefinitionRegistry.runtime(expression, implementation);
},
defineParameterType: defineParameterType(stepDefinitionRegistry),
};

View File

@@ -0,0 +1,87 @@
/* eslint-disable global-require */
const {
resolveFeatureFromFile,
} = require("./testHelpers/resolveFeatureFromFile");
describe("Scenario Outline", () => {
require("../cypress/support/step_definitions/scenario_outline_integer");
require("../cypress/support/step_definitions/scenario_outline_string");
require("../cypress/support/step_definitions/scenario_outline_data_table");
require("../cypress/support/step_definitions/scenario_outline_multiple_vars");
resolveFeatureFromFile("./cypress/integration/ScenarioOutline.feature");
});
describe("DocString", () => {
require("../cypress/support/step_definitions/docString");
resolveFeatureFromFile("./cypress/integration/DocString.feature");
});
describe("Data table", () => {
require("../cypress/support/step_definitions/dataTable");
resolveFeatureFromFile("./cypress/integration/DataTable.feature");
});
describe("Basic example", () => {
require("../cypress/support/step_definitions/basic");
resolveFeatureFromFile("./cypress/integration/Plugin.feature");
});
describe("Background section", () => {
require("../cypress/support/step_definitions/backgroundSection");
resolveFeatureFromFile("./cypress/integration/BackgroundSection.feature");
});
describe("Regexp", () => {
require("../cypress/support/step_definitions/regexp");
resolveFeatureFromFile("./cypress/integration/RegularExpressions.feature");
});
describe("Custom Parameter Types", () => {
require("../cypress/support/step_definitions/customParameterTypes");
resolveFeatureFromFile("./cypress/integration/CustomParameterTypes.feature");
});
describe("Tags implementation", () => {
require("../cypress/support/step_definitions/tags_implementation");
resolveFeatureFromFile("./cypress/integration/TagsImplementation.feature");
});
describe("Tags with env TAGS set", () => {
window.Cypress = {
...window.Cypress,
env: () => "@test-tag and not @ignore-tag",
};
require("../cypress/support/step_definitions/tags_implementation_with_env_set");
resolveFeatureFromFile(
"./cypress/integration/TagsImplementationWithEnvSet.feature"
);
resolveFeatureFromFile(
"./cypress/integration/TagsImplementationWithEnvSetScenarioLevel.feature"
);
});
describe("Smart tagging", () => {
window.Cypress = {
...window.Cypress,
env: () => "",
};
require("../cypress/support/step_definitions/smart_tagging");
resolveFeatureFromFile("./cypress/integration/SmartTagging.feature");
});
describe("And and But", () => {
require("../cypress/support/step_definitions/and_and_but_steps");
resolveFeatureFromFile("./cypress/integration/AndAndButSteps.feature");
});
describe("defineStep", () => {
require("../cypress/support/step_definitions/usingDefineSteps");
resolveFeatureFromFile("./cypress/integration/DefineStep.feature");
});
describe("Before and After", () => {
require("../cypress/support/step_definitions/before_and_after_steps");
resolveFeatureFromFile("./cypress/integration/BeforeAndAfterSteps.feature");
});

View File

@@ -0,0 +1,32 @@
const path = require("path");
const fs = require("fs");
const { getConfig } = require("./getConfig");
module.exports = () => {
const appRoot = process.cwd();
const config = getConfig();
if (config) {
// left for backward compability, but we need the consistency with other configuration options
const confStepDefinitions = config.step_definitions
? config.step_definitions
: config.stepDefinitions;
if (config.nonGlobalStepDefinitions) {
const relativePath =
confStepDefinitions || `cypress${path.sep}integration`;
const stepsPath = path.resolve(appRoot, relativePath);
if (!fs.existsSync(stepsPath)) {
throw new Error(
`We've tried to resolve your step definitions at ${relativePath}, but that doesn't seem to exist. As of version 2.0.0 it's required to set step_definitions in your cypress-cucumber-preprocessor configuration. Look for nonGlobalStepDefinitions and add stepDefinitions right next to it. It should match your cypress configuration has set for integrationFolder. We no longer rely on getting information from that file as it was unreliable and problematic across Linux/MacOS/Windows especially since the config file could have been passed as an argument to cypress.`
);
}
return stepsPath;
}
if (confStepDefinitions) {
return path.resolve(appRoot, confStepDefinitions);
}
}
return `${appRoot}/cypress/support/step_definitions`;
};

View File

@@ -0,0 +1,92 @@
const fs = require("fs");
const { sep } = require("path");
const { getConfig } = require("./getConfig");
const stepDefinitionPath = require("./stepDefinitionPath");
jest.mock("./getConfig");
jest.mock("fs", () => ({
existsSync: jest.fn(),
}));
const defaultNonGlobalStepDefinitionsPath = `cypress${sep}integration`;
describe("load path from step definitions", () => {
beforeEach(() => {
getConfig.mockReset();
fs.existsSync.mockReset();
});
test("Should throw an error if nonGlobalStepDefinitions and stepDefinitions are not set and the default is wrong", () => {
getConfig.mockReturnValue({
nonGlobalStepDefinitions: true,
});
fs.existsSync.mockReturnValue(false);
const errorMessage = `We've tried to resolve your step definitions at ${defaultNonGlobalStepDefinitionsPath}, but that doesn't seem to exist. As of version 2.0.0 it's required to set step_definitions in your cypress-cucumber-preprocessor configuration. Look for nonGlobalStepDefinitions and add stepDefinitions right next to it. It should match your cypress configuration has set for integrationFolder. We no longer rely on getting information from that file as it was unreliable and problematic across Linux/MacOS/Windows especially since the config file could have been passed as an argument to cypress.`;
expect(stepDefinitionPath).throw(errorMessage);
});
test("Should throw an error if nonGlobalStepDefinitions and stepDefinitions are set but the folder doesn't exist", () => {
const stepDefinitions = "cypress/stepDefinitions";
getConfig.mockReturnValue({
nonGlobalStepDefinitions: true,
stepDefinitions,
});
fs.existsSync.mockReturnValue(false);
const errorMessage = `We've tried to resolve your step definitions at ${stepDefinitions}, but that doesn't seem to exist. As of version 2.0.0 it's required to set step_definitions in your cypress-cucumber-preprocessor configuration. Look for nonGlobalStepDefinitions and add stepDefinitions right next to it. It should match your cypress configuration has set for integrationFolder. We no longer rely on getting information from that file as it was unreliable and problematic across Linux/MacOS/Windows especially since the config file could have been passed as an argument to cypress.`;
expect(stepDefinitionPath).throw(errorMessage);
});
test("should use the default stepDefinitions path for nonGlobalStepDefinitions", () => {
const appRoot = process.cwd();
getConfig.mockReturnValue({
nonGlobalStepDefinitions: true,
});
fs.existsSync.mockReturnValue(true);
expect(stepDefinitionPath()).to.equal(
`${appRoot}${sep}${defaultNonGlobalStepDefinitionsPath}`
);
});
test("should use the stepDefinitions path for nonGlobalStepDefinitions", () => {
const appRoot = process.cwd();
getConfig.mockReturnValue({
step_definitions: "./e2e/support/step-definitions",
});
expect(stepDefinitionPath()).to.equal(
`${appRoot}${sep}e2e${sep}support${sep}step-definitions`
);
});
test("should use the stepDefinitions path", () => {
const appRoot = process.cwd();
getConfig.mockReturnValue({
step_definitions: "./e2e/support/step-definitions",
});
expect(stepDefinitionPath()).to.equal(
`${appRoot}${sep}e2e${sep}support${sep}step-definitions`
);
});
test("should return default path if stepDefinition are not configured and nonGlobalStepDefinitions are not set", () => {
const appRoot = process.cwd();
expect(stepDefinitionPath()).to.equal(
`${appRoot}/cypress/support/step_definitions`
);
});
test("should allow the backward compatible use of step_definitions in cosmiconfig", () => {
const appRoot = process.cwd();
getConfig.mockReturnValue({
step_definitions: "./e2e/support/step-definitions",
});
expect(stepDefinitionPath()).to.equal(
`${appRoot}${sep}e2e${sep}support${sep}step-definitions`
);
});
});

View File

@@ -0,0 +1,23 @@
const { TagExpressionParser } = require("cucumber-tag-expressions");
function getEnvTags() {
return Cypress.env("TAGS") || "";
}
function shouldProceedCurrentStep(tags = [], envTags = getEnvTags()) {
const parser = new TagExpressionParser();
try {
const expressionNode = parser.parse(envTags);
const mappedTags = tags.map((tag) => tag.name);
return expressionNode.evaluate(mappedTags);
} catch (e) {
/* eslint-disable-next-line no-console */
console.log(`Error parsing tags: '${envTags}'. Message: ${e.message}`);
return false;
}
}
module.exports = {
shouldProceedCurrentStep,
getEnvTags,
};

View File

@@ -0,0 +1,14 @@
/* eslint-disable global-require */
const {
resolveFeatureFromFile,
} = require("./testHelpers/resolveFeatureFromFile");
describe("Tags inheritance", () => {
window.Cypress = {
...window.Cypress,
env: () => "@inherited-tag and @own-tag",
};
require("../cypress/support/step_definitions/tags_implementation_with_env_set");
resolveFeatureFromFile("./cypress/integration/TagsInheritance.feature");
});

View File

@@ -0,0 +1,13 @@
/* eslint-disable global-require */
const fs = require("fs");
const { createTestsFromFeature } = require("../createTestsFromFeature");
const resolveFeatureFromFile = (featureFile) => {
const spec = fs.readFileSync(featureFile);
createTestsFromFeature(featureFile, spec);
};
module.exports = {
resolveFeatureFromFile,
};

View File

@@ -0,0 +1,68 @@
global.jestExpect = global.expect;
global.expect = require("chai").expect;
global.before = jest.fn();
global.after = jest.fn();
global.skip = jest.fn();
window.Cypress = {
env: jest.fn(),
on: jest.fn(),
off: jest.fn(),
log: jest.fn(),
mocha: {
getRunner: () => {
return {
on: jest.fn(),
};
},
},
Promise: { each: (iterator, iteree) => iterator.map(iteree) },
};
const {
defineParameterType,
defineStep,
When,
Then,
Given,
And,
But,
Before,
After,
} = require("../resolveStepDefinition");
const mockedThen = (funcOrConfig, func) => {
if (typeof funcOrConfig === "object") {
func();
} else {
funcOrConfig();
}
return { then: mockedThen };
};
const mockedPromise = (func) => {
func();
return { then: mockedThen };
};
window.defineParameterType = defineParameterType;
window.When = When;
window.Then = Then;
window.Given = Given;
window.And = And;
window.But = But;
window.Before = Before;
window.After = After;
window.defineStep = defineStep;
window.cy = {
log: jest.fn(),
logStep: mockedPromise,
startScenario: mockedPromise,
finishScenario: mockedPromise,
startStep: mockedPromise,
finishStep: mockedPromise,
finishTest: mockedPromise,
then: mockedThen,
end: mockedPromise,
};