forked from Klaveness-Digital/cypress-cucumber-preprocessor
-
-
Notifications
You must be signed in to change notification settings - Fork 147
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This entails creating step- & hook-registries *per test* when running in all-mode and with non-global step definitions. This will become important for the next step.
- Loading branch information
Showing
18 changed files
with
456 additions
and
521 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
const { | ||
CucumberExpression, | ||
RegularExpression, | ||
ParameterTypeRegistry | ||
} = require("cucumber-expressions"); | ||
|
||
const { shouldProceedCurrentStep } = require("./tagsHelper"); | ||
|
||
class StepDefinitionRegistry { | ||
constructor() { | ||
this.definitions = {}; | ||
this.runtime = {}; | ||
this.options = { | ||
parameterTypeRegistry: new ParameterTypeRegistry() | ||
}; | ||
|
||
this.definitions = []; | ||
this.runtime = (matcher, implementation) => { | ||
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 | ||
}); | ||
}; | ||
|
||
this.resolve = text => { | ||
const matchingSteps = this.definitions.filter(({ expression }) => | ||
expression.match(text) | ||
); | ||
|
||
if (matchingSteps.length === 0) { | ||
throw new Error(`Step implementation missing for: ${text}`); | ||
} else if (matchingSteps.length > 1) { | ||
throw new Error(`Multiple implementations exists for: ${text}`); | ||
} else { | ||
return matchingSteps[0]; | ||
} | ||
}; | ||
} | ||
} | ||
|
||
class HookRegistry { | ||
constructor() { | ||
this.definitions = []; | ||
this.runtime = {}; | ||
|
||
this.runtime = (tags, implementation) => { | ||
this.definitions.push({ | ||
tags, | ||
implementation | ||
}); | ||
}; | ||
|
||
this.resolve = scenarioTags => | ||
this.definitions.filter( | ||
({ tags }) => | ||
!tags || | ||
tags.length === 0 || | ||
shouldProceedCurrentStep(scenarioTags, tags) | ||
); | ||
} | ||
} | ||
|
||
function createRegistries() { | ||
const stepDefinitionRegistry = new StepDefinitionRegistry(); | ||
const beforeHookRegistry = new HookRegistry(); | ||
const afterHookRegistry = new HookRegistry(); | ||
|
||
return { | ||
stepDefinitionRegistry, | ||
beforeHookRegistry, | ||
afterHookRegistry | ||
}; | ||
} | ||
|
||
module.exports = createRegistries; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,208 @@ | ||
const DataTable = require("cucumber/lib/models/data_table").default; | ||
const statuses = require("cucumber/lib/status").default; | ||
const { CucumberDataCollector } = require("./cukejson/cucumberDataCollector"); | ||
const { generateCucumberJson } = require("./cukejson/generateCucumberJson"); | ||
const { shouldProceedCurrentStep } = require("./tagsHelper"); | ||
const createRegitries = require("./createRegistries"); | ||
const populateGlobalMethods = require("./populateGlobalMethods"); | ||
|
||
function resolveAndRunHooks(hookRegistry, scenarioTags, featureName) { | ||
return window.Cypress.Promise.each( | ||
hookRegistry.resolve(scenarioTags, featureName), | ||
({ implementation }) => implementation.call(this) | ||
); | ||
} | ||
|
||
function resolveStepArgument(args) { | ||
const [argument] = args; | ||
if (!argument) { | ||
return argument; | ||
} | ||
if (argument.rows) { | ||
return new DataTable(argument); | ||
} | ||
if (argument.content) { | ||
return argument.content; | ||
} | ||
return argument; | ||
} | ||
|
||
function resolveAndRunStepDefinition( | ||
stepDefinitionRegistry, | ||
step, | ||
featureName | ||
) { | ||
const stepText = step.text; | ||
const { expression, implementation } = stepDefinitionRegistry.resolve( | ||
stepText, | ||
featureName | ||
); | ||
const argument = resolveStepArgument(step.arguments); | ||
return implementation.call( | ||
this, | ||
...expression.match(stepText).map(match => match.getValue()), | ||
argument | ||
); | ||
} | ||
|
||
const cleanupFilename = s => s.split(".")[0]; | ||
|
||
const writeCucumberJsonFile = (json, preprocessorConfig) => { | ||
const outputFolder = | ||
preprocessorConfig.cucumberJson.outputFolder || "cypress/cucumber-json"; | ||
const outputPrefix = preprocessorConfig.cucumberJson.filePrefix || ""; | ||
const outputSuffix = | ||
preprocessorConfig.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 }); | ||
}; | ||
|
||
function createTestsFromFeatures(options) { | ||
const { features, preprocessorConfig, globalFilesToRequireFn } = options; | ||
|
||
const tagsUsedInTests = features | ||
.flatMap(feature => feature.pickles) | ||
.flatMap(pickle => pickle.tags) | ||
.map(tag => tag.name); | ||
|
||
const envTags = Cypress.env("TAGS"); | ||
|
||
let tagFilter = null; | ||
|
||
if (tagsUsedInTests.includes("@focus")) { | ||
tagFilter = "@focus"; | ||
} else if (envTags) { | ||
tagFilter = envTags; | ||
} | ||
|
||
let stepDefinitionRegistry; | ||
let beforeHookRegistry; | ||
let afterHookRegistry; | ||
|
||
if (globalFilesToRequireFn) { | ||
({ | ||
stepDefinitionRegistry, | ||
beforeHookRegistry, | ||
afterHookRegistry | ||
} = createRegitries()); | ||
|
||
populateGlobalMethods({ | ||
stepDefinitionRegistry, | ||
beforeHookRegistry, | ||
afterHookRegistry | ||
}); | ||
|
||
globalFilesToRequireFn(); | ||
} | ||
|
||
// eslint-disable-next-line no-restricted-syntax | ||
for (const { | ||
filePath, | ||
source, | ||
feature, | ||
pickles, | ||
localFilesToRequireFn | ||
} of features) { | ||
const testState = new CucumberDataCollector(filePath, source, feature); | ||
|
||
if (localFilesToRequireFn) { | ||
({ | ||
stepDefinitionRegistry, | ||
beforeHookRegistry, | ||
afterHookRegistry | ||
} = createRegitries()); | ||
|
||
populateGlobalMethods({ | ||
stepDefinitionRegistry, | ||
beforeHookRegistry, | ||
afterHookRegistry | ||
}); | ||
|
||
localFilesToRequireFn(); | ||
} | ||
|
||
// eslint-disable-next-line no-loop-func | ||
describe(feature.name, () => { | ||
before(() => { | ||
cy.then(() => testState.onStartTest()); | ||
}); | ||
|
||
beforeEach(() => { | ||
/** | ||
* Left for legacy support, but it's not something we rely on (nor should you). | ||
*/ | ||
window.testState = testState; | ||
|
||
const failHandler = err => { | ||
Cypress.off("fail", failHandler); | ||
testState.onFail(err); | ||
throw err; | ||
}; | ||
|
||
Cypress.on("fail", failHandler); | ||
}); | ||
|
||
const picklesToRun = pickles.filter( | ||
pickle => !tagFilter || shouldProceedCurrentStep(pickle.tags, tagFilter) | ||
); | ||
|
||
picklesToRun.forEach(pickle => { | ||
const indexedSteps = pickle.steps.map((step, index) => | ||
Object.assign({}, step, { index }) | ||
); | ||
|
||
it(pickle.name, function() { | ||
return cy | ||
.then(() => testState.onStartScenario(pickle, indexedSteps)) | ||
.then(() => | ||
resolveAndRunHooks.call( | ||
this, | ||
beforeHookRegistry, | ||
pickle.tags, | ||
feature.name | ||
) | ||
) | ||
.then(() => | ||
indexedSteps.forEach(step => { | ||
cy.then(() => testState.onStartStep(step)) | ||
.then(() => | ||
resolveAndRunStepDefinition.call( | ||
this, | ||
stepDefinitionRegistry, | ||
step, | ||
testState.feature.name | ||
) | ||
) | ||
.then(() => testState.onFinishStep(step, statuses.PASSED)); | ||
}) | ||
) | ||
.then(() => | ||
resolveAndRunHooks.call( | ||
this, | ||
afterHookRegistry, | ||
pickle.tags, | ||
feature.name | ||
) | ||
) | ||
.then(() => testState.onFinishScenario(pickle)); | ||
}); | ||
}); | ||
|
||
after(() => { | ||
cy.then(() => testState.onFinishTest()).then(() => { | ||
if ( | ||
preprocessorConfig && | ||
preprocessorConfig.cucumberJson && | ||
preprocessorConfig.cucumberJson.generate | ||
) { | ||
const json = generateCucumberJson(testState); | ||
writeCucumberJsonFile(json, preprocessorConfig); | ||
} | ||
}); | ||
}); | ||
}); | ||
} | ||
} | ||
|
||
module.exports = createTestsFromFeatures; |
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion
2
lib/cukejson/generateCucumberJson.js → ...-runtime/cukejson/generateCucumberJson.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
const createTestsFromFeatures = require("./createTestsFromFeatures"); | ||
|
||
module.exports = { createTestsFromFeatures }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
const { | ||
defineParameterType | ||
} = require("cucumber/lib/support_code_library_builder/define_helpers"); | ||
|
||
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] | ||
}; | ||
} | ||
|
||
/* eslint-disable no-param-reassign, no-multi-assign */ | ||
function populateGlobalMethods( | ||
{ stepDefinitionRegistry, beforeHookRegistry, afterHookRegistry }, | ||
instance = window | ||
) { | ||
const defineStep = (expression, implementation) => { | ||
stepDefinitionRegistry.runtime(expression, implementation); | ||
}; | ||
|
||
instance.defineStep = defineStep; | ||
instance.Given = instance.given = defineStep; | ||
instance.When = instance.when = defineStep; | ||
instance.Then = instance.then = defineStep; | ||
instance.And = instance.and = defineStep; | ||
instance.But = instance.but = defineStep; | ||
|
||
instance.defineParameterType = defineParameterType(stepDefinitionRegistry); | ||
|
||
instance.Before = (...args) => { | ||
const { tags, implementation } = parseHookArgs(args); | ||
beforeHookRegistry.runtime(tags, implementation); | ||
}; | ||
|
||
instance.After = (...args) => { | ||
const { tags, implementation } = parseHookArgs(args); | ||
afterHookRegistry.runtime(tags, implementation); | ||
}; | ||
} | ||
/* eslint-enable no-param-reassign, no-multi-assign */ | ||
|
||
module.exports = populateGlobalMethods; |
File renamed without changes.
Oops, something went wrong.