diff --git a/cypress-tags.js b/cypress-tags.js index 0846d12b..e2148178 100755 --- a/cypress-tags.js +++ b/cypress-tags.js @@ -5,7 +5,9 @@ const glob = require("glob"); const fs = require("fs"); const { execFileSync } = require("child_process"); -const { shouldProceedCurrentStep } = require("./lib/tagsHelper"); +const { + shouldProceedCurrentStep +} = require("./lib/browser-runtime/tagsHelper"); const debug = (message, ...rest) => process.env.DEBUG diff --git a/lib/browser-runtime/createRegistries.js b/lib/browser-runtime/createRegistries.js new file mode 100644 index 00000000..a4c6cdae --- /dev/null +++ b/lib/browser-runtime/createRegistries.js @@ -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; diff --git a/lib/browser-runtime/createTestsFromFeatures.js b/lib/browser-runtime/createTestsFromFeatures.js new file mode 100644 index 00000000..a112fb03 --- /dev/null +++ b/lib/browser-runtime/createTestsFromFeatures.js @@ -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; diff --git a/lib/cukejson/cucumberDataCollector.js b/lib/browser-runtime/cukejson/cucumberDataCollector.js similarity index 100% rename from lib/cukejson/cucumberDataCollector.js rename to lib/browser-runtime/cukejson/cucumberDataCollector.js diff --git a/lib/cukejson/cucumberDataCollector.test.js b/lib/browser-runtime/cukejson/cucumberDataCollector.test.js similarity index 100% rename from lib/cukejson/cucumberDataCollector.test.js rename to lib/browser-runtime/cukejson/cucumberDataCollector.test.js diff --git a/lib/cukejson/generateCucumberJson.js b/lib/browser-runtime/cukejson/generateCucumberJson.js similarity index 97% rename from lib/cukejson/generateCucumberJson.js rename to lib/browser-runtime/cukejson/generateCucumberJson.js index 0e48a486..f1c22190 100644 --- a/lib/cukejson/generateCucumberJson.js +++ b/lib/browser-runtime/cukejson/generateCucumberJson.js @@ -1,7 +1,7 @@ const { EventEmitter } = require("events"); -const { generateEvents } = require("../parserHelpers"); const JsonFormatter = require("cucumber/lib/formatter/json_formatter").default; const formatterHelpers = require("cucumber/lib/formatter/helpers"); +const { generateEvents } = require("../../parserHelpers"); function last(collection) { return collection[collection.length - 1]; diff --git a/lib/browser-runtime/index.js b/lib/browser-runtime/index.js new file mode 100644 index 00000000..170b0904 --- /dev/null +++ b/lib/browser-runtime/index.js @@ -0,0 +1,3 @@ +const createTestsFromFeatures = require("./createTestsFromFeatures"); + +module.exports = { createTestsFromFeatures }; diff --git a/lib/browser-runtime/populateGlobalMethods.js b/lib/browser-runtime/populateGlobalMethods.js new file mode 100644 index 00000000..9657e6d5 --- /dev/null +++ b/lib/browser-runtime/populateGlobalMethods.js @@ -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; diff --git a/lib/tagsHelper.js b/lib/browser-runtime/tagsHelper.js similarity index 100% rename from lib/tagsHelper.js rename to lib/browser-runtime/tagsHelper.js diff --git a/lib/createTestFromScenario.js b/lib/createTestFromScenario.js deleted file mode 100644 index 33221e60..00000000 --- a/lib/createTestFromScenario.js +++ /dev/null @@ -1,91 +0,0 @@ -/* eslint-disable prefer-template */ -const statuses = require("cucumber/lib/status").default; -const { - resolveAndRunStepDefinition, - resolveAndRunBeforeHooks, - resolveAndRunAfterHooks -} = require("./resolveStepDefinition"); -const { generateCucumberJson } = require("./cukejson/generateCucumberJson"); - -// eslint-disable-next-line func-names -const stepTest = function(state, stepDetails) { - cy.then(() => state.onStartStep(stepDetails)) - .then(() => - resolveAndRunStepDefinition.call(this, stepDetails, state.feature.name) - ) - .then(() => state.onFinishStep(stepDetails, statuses.PASSED)); -}; - -const runTest = pickle => { - const indexedSteps = pickle.steps.map((step, index) => - Object.assign({}, step, { index }) - ); - - // eslint-disable-next-line func-names - it(pickle.name, function() { - const state = window.testState; - return cy - .then(() => state.onStartScenario(pickle, indexedSteps)) - .then(() => - resolveAndRunBeforeHooks.call(this, pickle.tags, state.feature.name) - ) - .then(() => - indexedSteps.forEach(step => stepTest.call(this, state, step)) - ) - .then(() => - resolveAndRunAfterHooks.call(this, pickle.tags, state.feature.name) - ) - .then(() => state.onFinishScenario(pickle)); - }); -}; - -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 createTestFromPickles = (pickles, 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 => { - Cypress.off("fail", failHandler); - testState.onFail(err); - throw err; - }; - - Cypress.on("fail", failHandler); - }); - - pickles.forEach(pickle => { - runTest.call(this, pickle); - }); - - // 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); - } - }); - }); -}; - -module.exports = { - createTestFromScenarios: createTestFromPickles -}; diff --git a/lib/createTestsFromFeature.js b/lib/createTestsFromFeature.js deleted file mode 100644 index e1c55387..00000000 --- a/lib/createTestsFromFeature.js +++ /dev/null @@ -1,33 +0,0 @@ -const { CucumberDataCollector } = require("./cukejson/cucumberDataCollector"); -const { createTestFromScenarios } = require("./createTestFromScenario"); -const { shouldProceedCurrentStep, getEnvTags } = require("./tagsHelper"); - -const flatten = collection => - collection.reduce((acum, element) => [].concat(acum).concat(element)); - -const createTestsFromFeature = (filePath, source, feature, pickles) => { - const testState = new CucumberDataCollector(filePath, source, feature); - const envTags = getEnvTags(); - - let tagFilter = null; - - const tagsUsedInTests = flatten(pickles.map(pickle => pickle.tags)).map( - tag => tag.name - ); - - if (tagsUsedInTests.includes("@focus")) { - tagFilter = "@focus"; - } else if (envTags) { - tagFilter = envTags; - } - - const picklesToRun = pickles.filter( - pickle => !tagFilter || shouldProceedCurrentStep(pickle.tags, tagFilter) - ); - - createTestFromScenarios(picklesToRun, testState); -}; - -module.exports = { - createTestsFromFeature -}; diff --git a/lib/cucumberTemplate.js b/lib/cucumberTemplate.js deleted file mode 100644 index 879f67e6..00000000 --- a/lib/cucumberTemplate.js +++ /dev/null @@ -1,36 +0,0 @@ -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")}"); -const Given = (window.Given = window.given = given); -const When = (window.When = window.when = when); -const Then = (window.Then = window.then = then); -const And = (window.And = window.and = and); -const But = (window.But = window.but = but); -window.defineParameterType = defineParameterType; -window.defineStep = defineStep; -const { - createTestsFromFeature -} = require("${getPathFor("createTestsFromFeature")}"); -`; diff --git a/lib/featuresLoader.js b/lib/featuresLoader.js index 18217c2d..a7bd8203 100644 --- a/lib/featuresLoader.js +++ b/lib/featuresLoader.js @@ -6,8 +6,7 @@ const jsStringEscape = require("js-string-escape"); const { parse } = require("./parserHelpers"); const { getStepDefinitionsPaths } = require("./getStepDefinitionsPaths"); -const { cucumberTemplate } = require("./cucumberTemplate"); -const { getCucumberJsonConfig } = require("./getCucumberJsonConfig"); +const { getConfig } = require("./getConfig"); const { isNonGlobalStepDefinitionsMode } = require("./isNonGlobalStepDefinitionsMode"); @@ -17,48 +16,51 @@ const createCucumber = ( features, picklesCol, globalToRequire, - nonGlobalToRequire, - cucumberJson + nonGlobalToRequire ) => ` - ${cucumberTemplate} - window.cucumberJson = ${JSON.stringify(cucumberJson)}; - - var moduleCache = arguments[5]; - - // Stolen from https://github.com/browserify/browserify/issues/1444 - function clearFromCache(instance) - { - for(var key in moduleCache) - { - if(moduleCache[key].exports == instance) - { - delete moduleCache[key]; - return; - } + var moduleCache = arguments[5]; + + // Stolen from https://github.com/browserify/browserify/issues/1444. + function clearFromCache(instance) { + for (var key in moduleCache) { + if (moduleCache[key].exports == instance) { + delete moduleCache[key]; + return; + } } - throw "could not clear instance from module cache"; - } - - ${globalToRequire.join("\n")} + throw "Could not clear instance from module cache."; + } - ${sources - .map( - ({ source, filePath }, i) => ` - describe(\`${features[i].name}\`, function() { - window.currentFeatureName = \`${features[i].name}\` - ${nonGlobalToRequire && - nonGlobalToRequire - .find(fileSteps => fileSteps[filePath]) - [filePath].join("\n")} - - createTestsFromFeature('${path.basename(filePath)}', \`${jsStringEscape( - source - )}\`, ${JSON.stringify(features[i])}, ${JSON.stringify(picklesCol[i])}); - }) - ` - ) - .join("\n")} + const { + createTestsFromFeatures + } = require("${path.join(__dirname, "browser-runtime")}"); + + createTestsFromFeatures({ + globalFilesToRequireFn: ${ + globalToRequire ? `() => {${globalToRequire.join(";")}}` : "null" + }, + features: [ + ${sources + .map( + ({ source, filePath }, i) => ` + { + filePath: ${JSON.stringify(filePath)}, + source: '${jsStringEscape(source)}', + feature: ${JSON.stringify(features[i])}, + pickles: ${JSON.stringify(picklesCol[i])}, + localFilesToRequireFn: ${ + globalToRequire + ? "null" + : `() => {${nonGlobalToRequire[i].join(";")}}` + } + } + ` + ) + .join(",")} + ], + preprocessorConfig: ${JSON.stringify(getConfig())} + }); `; module.exports = function(_, filePath = this.resourcePath) { @@ -66,15 +68,15 @@ module.exports = function(_, filePath = this.resourcePath) { const featuresPaths = glob.sync(`${path.dirname(filePath)}/**/*.feature`); - let globalStepDefinitionsToRequire = []; + let globalStepDefinitionsToRequire; let nonGlobalStepDefinitionsToRequire; if (isNonGlobalStepDefinitionsMode()) { - nonGlobalStepDefinitionsToRequire = featuresPaths.map(featurePath => ({ - [featurePath]: getStepDefinitionsPaths(featurePath).map( + nonGlobalStepDefinitionsToRequire = featuresPaths.map(featurePath => + getStepDefinitionsPaths(featurePath).map( sdPath => `clearFromCache(require('${sdPath}'))` ) - })); + ); } else { globalStepDefinitionsToRequire = [ ...new Set( @@ -122,7 +124,6 @@ module.exports = function(_, filePath = this.resourcePath) { features, picklesCol, globalStepDefinitionsToRequire, - nonGlobalStepDefinitionsToRequire, - getCucumberJsonConfig() + nonGlobalStepDefinitionsToRequire ); }; diff --git a/lib/loader.js b/lib/loader.js index 9317248e..d6064e64 100644 --- a/lib/loader.js +++ b/lib/loader.js @@ -3,29 +3,28 @@ const path = require("path"); const jsStringEscape = require("js-string-escape"); const { parse } = require("./parserHelpers"); const { getStepDefinitionsPaths } = require("./getStepDefinitionsPaths"); -const { cucumberTemplate } = require("./cucumberTemplate"); -const { getCucumberJsonConfig } = require("./getCucumberJsonConfig"); +const { getConfig } = require("./getConfig"); // 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, - source, - feature, - pickles, - toRequire -) => +const createCucumber = (filePath, source, feature, pickles, toRequire) => ` - ${cucumberTemplate} - - window.cucumberJson = ${JSON.stringify(cucumberJson)}; - describe(\`${feature.name}\`, function() { - ${toRequire.join("\n")} - createTestsFromFeature('${filePath}', \`${jsStringEscape( - source - )}\`, ${JSON.stringify(feature)}, ${JSON.stringify(pickles)}); - }); + const { + createTestsFromFeatures + } = require("${path.join(__dirname, "browser-runtime")}"); + + createTestsFromFeatures({ + features: [{ + filePath: ${JSON.stringify(filePath)}, + source: '${jsStringEscape(source)}', + feature: ${JSON.stringify(feature)}, + pickles: ${JSON.stringify(pickles)}, + localFilesToRequireFn() { + ${toRequire.join("\n")} + } + }], + preprocessorConfig: ${JSON.stringify(getConfig())} + }); `; // eslint-disable-next-line func-names @@ -39,7 +38,6 @@ module.exports = function(source, filePath = this.resourcePath) { return createCucumber( path.basename(filePath), - getCucumberJsonConfig(), source, feature, pickles, diff --git a/lib/resolveStepDefinition.js b/lib/resolveStepDefinition.js deleted file mode 100644 index e3362ebf..00000000 --- a/lib/resolveStepDefinition.js +++ /dev/null @@ -1,201 +0,0 @@ -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 { 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, - featureName: window.currentFeatureName || "___GLOBAL_EXECUTION___" - }); - }; - - this.resolve = (text, runningFeatureName) => { - const matchingSteps = this.definitions.filter( - ({ expression, featureName }) => - expression.match(text) && - (runningFeatureName === featureName || - featureName === "___GLOBAL_EXECUTION___") - ); - - 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, - 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___") - ); - } -} - -const stepDefinitionRegistry = new StepDefinitionRegistry(); -const beforeHookRegistry = new HookRegistry(); -const afterHookRegistry = new HookRegistry(); - -function resolveStepDefinition(step, featureName) { - const stepDefinition = stepDefinitionRegistry.resolve(step.text, featureName); - return stepDefinition || {}; -} - -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 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 = { - resolveAndRunBeforeHooks(scenarioTags, featureName) { - return resolveAndRunHooks.call( - this, - beforeHookRegistry, - scenarioTags, - featureName - ); - }, - resolveAndRunAfterHooks(scenarioTags, featureName) { - return resolveAndRunHooks.call( - this, - afterHookRegistry, - scenarioTags, - featureName - ); - }, - // eslint-disable-next-line func-names - resolveAndRunStepDefinition(step, featureName) { - const { expression, implementation } = resolveStepDefinition( - step, - featureName - ); - const stepText = step.text; - const argument = resolveStepArgument(step.arguments); - return implementation.call( - this, - ...expression.match(stepText).map(match => match.getValue()), - argument - ); - }, - given: (expression, implementation) => { - stepDefinitionRegistry.runtime(expression, implementation); - }, - when: (expression, implementation) => { - stepDefinitionRegistry.runtime(expression, implementation); - }, - then: (expression, implementation) => { - stepDefinitionRegistry.runtime(expression, implementation); - }, - and: (expression, implementation) => { - stepDefinitionRegistry.runtime(expression, implementation); - }, - but: (expression, implementation) => { - stepDefinitionRegistry.runtime(expression, implementation); - }, - 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) -}; diff --git a/lib/testHelpers/setupTestFramework.js b/lib/testHelpers/setupTestFramework.js index a636dbbf..089faf37 100644 --- a/lib/testHelpers/setupTestFramework.js +++ b/lib/testHelpers/setupTestFramework.js @@ -1,9 +1,6 @@ global.jestExpect = global.expect; global.expect = require("chai").expect; -global.before = jest.fn(); -global.after = jest.fn(); - window.Cypress = { env: jest.fn(), on: jest.fn(), @@ -11,41 +8,3 @@ window.Cypress = { log: jest.fn(), Promise: { each: (iterator, iteree) => iterator.map(iteree) } }; - -const { - defineParameterType, - defineStep, - when, - then, - given, - and, - but, - Before, - After -} = require("../resolveStepDefinition"); - -const mockedPromise = func => { - func(); - return { then: mockedPromise }; -}; - -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: mockedPromise, - end: mockedPromise -}; diff --git a/resolveStepDefinition.js b/resolveStepDefinition.js index aaa3a852..ce2dbf05 100644 --- a/resolveStepDefinition.js +++ b/resolveStepDefinition.js @@ -1,25 +1,5 @@ -// reexporting here for backward compability -const { - given, - when, - then, - and, - but, - defineParameterType -} = require("./lib/resolveStepDefinition"); - console.warn( "DEPRECATION WARNING! Please change your imports from cypress-cucumber-preprocessor/resolveStepDefinition to cypress-cucumber-preprocessor/steps" ); -module.exports = { - given, - when, - then, - Given: given, - When: when, - Then: then, - And: and, - But: but, - defineParameterType -}; +module.exports = require("./steps"); diff --git a/steps.js b/steps.js index 9ff27fb7..cb50c2e8 100644 --- a/steps.js +++ b/steps.js @@ -1,31 +1,26 @@ -// We know this is a duplicate of ./resolveStepDefinition. -// We will remove that one soon and leave only this one in a future version. +const methods = [ + "given", + "when", + "then", + "and", + "but", + "Given", + "When", + "Then", + "And", + "But", + "Before", + "After", + "defineParameterType", + "defineStep" +]; -const { - given, - when, - then, - and, - but, - Before, - After, - defineParameterType, - defineStep -} = require("./lib/resolveStepDefinition"); - -module.exports = { - given, - when, - then, - and, - but, - Given: given, - When: when, - Then: then, - And: and, - But: but, - Before, - After, - defineParameterType, - defineStep -}; +module.exports = methods.reduce( + (acum, method) => ({ + ...acum, + [method](...args) { + return window[method](...args); + } + }), + {} +);