Provides a DRY way of writing cucumber steps for e2e testing
Package provides a DRY way of creating cucumber steps by combining predefined set of actions, targets and areas.
Targets and areas contain xpath selectors while actions contain nightwatch commands.
In gherkin it will read as "action applied to target that is located in area":
Given I click button "Submit" in modal
would be composed out of I click action, button "Submit" target and in modal area.
The power of this tool is in automatic generation of all configured combinations for actions, targets and areas.
npm i -D cucumber-steps-generator
to generate steps create generate.js, config.js, actions.js, targets.js and areas.js and run:
node ./e2e/stepsGenerator/generate.js
//generate.js
const path = require('path')
const { generateSteps } = require('cucumber-steps-generator')
generateSteps({
outputFile: {
path: path.resolve(__dirname, './generatedSteps.js'),
injections: {
helpers: path.resolve(__dirname, '../helpers.js') // possibility to inject custom modules
}
},
configPath: path.resolve(__dirname, './config.js'),
actionsPath: path.resolve(__dirname, './actions.js'),
targetsPath: path.resolve(__dirname, './targets.js'),
areasPath: path.resolve(__dirname, './areas.js')
})
//config.js
const { extendXpath } = require('cucumber-steps-generator')
const testEnv = process.env.env || 'dev'
module.exports = {
variables: {
url: ['qa, stg'].includes(testEnv) ? `https://sitename-${testEnv}.domain.com` : 'localhost:8080',
},
// this section provides all possible set of selectors/selector functions for the website
xpath: {
button: {
// extendXpath adds functions like has-text, has-class and text-is (see utils section for details)
withTextOrClass: text => extendXpath(`//button[has-text("${text}") or has-class("${text}")]`),
},
input: {
withName: name => extendXpath(`//*[(self::input or self::textarea) and @name="${name}"]`),
},
select: {
withName: name => extendXpath(`//select/option[text-is("${name}")]/parent::select`)
},
navbar: {
top: extendXpath(`//nav[has-class("navbar") and has-class("sticky-top")]`),
},
modal: {
content: extendXpath(`//div[has-class("modal-dialog")]`),
},
link: {
withText: text => extendXpath(`//a[has-text("${text}")]`),
}
}
}
//actions.js
module.exports = {
followUrl: {
// {string} is placeholder. In gherkin it may look: Given I redirect to "/home"
gherkin: 'I redirect to {string}',
// empty targets array means that action can not be used with targets
targets: [],
// func takes ctx(read more in context section) as first arg and then arguments for each placeholder in gherkin part so 'path' here is "/home"
func: ({ client, variables }, path) => client.useXpath().url(variables.url + path)
},
type: {
// {target} placeholder is used for inserting defined targets: I type "Alex" in input "name"
gherkin: 'I type {string} in {target}',
// provided targets mean that action can be used only with targets having mentioned key
targets: ['inputWithName'],
// here value is "Alex" and target is `input "name"` which corresponds to key 'inputWithName' in targets.js file
func: ({ client }, value, target) => client.useXpath().setValue(target, value)
},
xpathClick: {
gherkin: 'I click {target}',
// not defining targets here indicates that action could be appliead to any target
func: ({ client }, target) => client.useXpath().click(target)
}
}
//targets.js
module.exports = {
link: {
gherkin: 'link {string}', // {string} is placeholder. In gherkin it may look: And I click link "click here"
// areas work the same way as in actions. Areas specify where target is located to solve multiple DOM matches
areas: ['modal'],
// func takes ctx(read more in context section) as first arg and then arguments for each placeholder in gherkin part so 'text' here is "click here"
// xpath here refers to config.js xpath definitions
func: ({ xpath }, text) => xpath.link.withText(text)
},
buttonWithTextOrClass: {
gherkin: 'button {string}',
func: ({ xpath }, text) => xpath.button.withTextOrClass(text)
},
inputWithName: {
gherkin: 'input {string}',
func: ({ xpath }, name) => xpath.input.withName(name)
}
}
//areas.js
module.exports = {
topNavbar: {
gherkin: 'in top navbar',
// xpath here refers to config.js xpath definitions
func: ({ xpath }) => xpath.navbar.top
},
modal: {
gherkin: 'in modal',
func: ({ xpath }) => xpath.modal.content
},
hamburgerMenu: {
gherkin: 'in hamburger menu',
func: ({ xpath }) => xpath.burger.menuSidebar
}
}
Part of the output file for the given configuration:
// action followUrl
Then(/^I redirect to "([^"]+)"$/, (arg1) => {
return (({ client, variables }, path) => client.useXpath().url(variables.url + path))(context, arg1)
})
// action type on target inputWithName
Then(/^I type "([^"]+)" in input "([^"]+)"$/, (arg1, arg2) => {
const path = (({ xpath }, name) => xpath.input.withName(name))(context, arg2)
return (({ client }, value, target) => client.useXpath().setValue(target, value))(context, arg1, path)
})
// action type on target inputWithName in area topNavbar
Then(/^I type "([^"]+)" in input "([^"]+)" in top navbar$/, (arg1, arg2) => {
const path = (({ xpath }) => xpath.navbar.top)(context) + (({ xpath }, name) => xpath.input.withName(name))(context, arg2)
return (({ client }, value, target) => client.useXpath().setValue(target, value))(context, arg1, path)
})
// action type on target inputWithName in area modal
Then(/^I type "([^"]+)" in input "([^"]+)" in modal$/, (arg1, arg2) => {
const path = (({ xpath }) => xpath.modal.content)(context) + (({ xpath }, name) => xpath.input.withName(name))(context, arg2)
return (({ client }, value, target) => client.useXpath().setValue(target, value))(context, arg1, path)
})
// action type on target inputWithName in area hamburgerMenu
Then(/^I type "([^"]+)" in input "([^"]+)" in hamburger menu$/, (arg1, arg2) => {
const path = (({ xpath }) => xpath.burger.menuSidebar)(context) + (({ xpath }, name) => xpath.input.withName(name))(context, arg2)
return (({ client }, value, target) => client.useXpath().setValue(target, value))(context, arg1, path)
})
// action xpathClick on target link
Then(/^I click link "([^"]+)"$/, (arg1) => {
const path = (({ xpath }, text) => xpath.link.withText(text))(context, arg1)
return (({ client }, target) => client.useXpath().click(target))(context, path)
})
// action xpathClick on target link in area modal
Then(/^I click link "([^"]+)" in modal$/, (arg1) => {
const path = (({ xpath }) => xpath.modal.content)(context) + (({ xpath }, text) => xpath.link.withText(text))(context, arg1)
return (({ client }, target) => client.useXpath().click(target))(context, path)
})
// action xpathClick on target buttonWithTextOrClass
Then(/^I click button "([^"]+)"$/, (arg1) => {
const path = (({ xpath }, text) => xpath.button.withTextOrClass(text))(context, arg1)
return (({ client }, target) => client.useXpath().click(target))(context, path)
})
// action xpathClick on target buttonWithTextOrClass in area topNavbar
Then(/^I click button "([^"]+)" in top navbar$/, (arg1) => {
const path = (({ xpath }) => xpath.navbar.top)(context) + (({ xpath }, text) => xpath.button.withTextOrClass(text))(context, arg1)
return (({ client }, target) => client.useXpath().click(target))(context, path)
})
// action xpathClick on target buttonWithTextOrClass in area modal
Then(/^I click button "([^"]+)" in modal$/, (arg1) => {
const path = (({ xpath }) => xpath.modal.content)(context) + (({ xpath }, text) => xpath.button.withTextOrClass(text))(context, arg1)
return (({ client }, target) => client.useXpath().click(target))(context, path)
})
// action xpathClick on target buttonWithTextOrClass in area hamburgerMenu
Then(/^I click button "([^"]+)" in hamburger menu$/, (arg1) => {
const path = (({ xpath }) => xpath.burger.menuSidebar)(context) + (({ xpath }, text) => xpath.button.withTextOrClass(text))(context, arg1)
return (({ client }, target) => client.useXpath().click(target))(context, path)
})
// action xpathClick on target inputWithName
Then(/^I click input "([^"]+)"$/, (arg1) => {
const path = (({ xpath }, name) => xpath.input.withName(name))(context, arg1)
return (({ client }, target) => client.useXpath().click(target))(context, path)
})
// action xpathClick on target inputWithName in area topNavbar
Then(/^I click input "([^"]+)" in top navbar$/, (arg1) => {
const path = (({ xpath }) => xpath.navbar.top)(context) + (({ xpath }, name) => xpath.input.withName(name))(context, arg1)
return (({ client }, target) => client.useXpath().click(target))(context, path)
})
// action xpathClick on target inputWithName in area modal
Then(/^I click input "([^"]+)" in modal$/, (arg1) => {
const path = (({ xpath }) => xpath.modal.content)(context) + (({ xpath }, name) => xpath.input.withName(name))(context, arg1)
return (({ client }, target) => client.useXpath().click(target))(context, path)
})
// action xpathClick on target inputWithName in area hamburgerMenu
Then(/^I click input "([^"]+)" in hamburger menu$/, (arg1) => {
const path = (({ xpath }) => xpath.burger.menuSidebar)(context) + (({ xpath }, name) => xpath.input.withName(name))(context, arg1)
return (({ client }, target) => client.useXpath().click(target))(context, path)
})
A context object is passed as first argument to action, target and area functions. Context can contain:
//*[contains(concat(" ", normalize-space(@class), " "), " Active ")]
converts to el_with_class_active
target = target1 | target2
//area//target1 | target2
instead of //area//target1 | //area//target2
//div[@target="1" or @target="2"]