eslint-plugin-cucumber
Advanced tools
Comparing version 1.3.0 to 1.4.0
@@ -1,8 +0,8 @@ | ||
# ensures Then steps are asynchronous (async-then) | ||
# Ensures step/hook implementations are asynchronous (async-then) | ||
When using an async heavy tool like protractor you want to ensure that your steps wait for async actions to complete before completing the scenario otherwise you can run through all the steps without ever checking anything. | ||
When using an async heavy tool like protractor you want to ensure that your steps and/or hooks wait for async actions to complete before completing the scenario otherwise you can run through all the steps without ever checking anything. | ||
## Rule Details | ||
This rule aims to force you to either return something (presumably a promise) or provide a callback function in your Then steps. | ||
This rule aims to force you to either return something (presumably a promise) or provide a callback function in your steps and/or hooks (by default, just `Then`s) - or use an [async function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function). | ||
@@ -13,3 +13,3 @@ The following patterns are considered warnings: | ||
this.Then(/some step/, function () { | ||
Then(/some step/, function () { | ||
expect(foo).to.eventually.eql('doesn\'t matter because it will never be checked'); | ||
@@ -24,18 +24,79 @@ }); | ||
this.Then(/some step/, function () { | ||
Then(/some step/, function () { | ||
return expect(foo).to.eventually.eql('bar'); | ||
}); | ||
this.Then(/some step/, function (done) { | ||
Then(/some step/, function (done) { | ||
expect(foo).to.eventually.eql('bar').notify(done); | ||
}); | ||
this.When(/some step/, function () { | ||
Then(/some step/, async function () { | ||
// anything, since async functions implicitly return a promise | ||
}); | ||
When(/some step/, function () { | ||
// do anything you want in Whens, we'll wait in the Thens | ||
} | ||
}); | ||
``` | ||
## Options | ||
By default, only calls to `Then` are checked. | ||
The rule has an object option where you can override this: | ||
- `"all"` - a boolean, if `true` then all supported step/hook words will be checked (`Given`, `When`, `Then`, `Before`, `BeforeAll`, `After`, `AfterAll`) | ||
- `"words"`- an array of strings, denoting which step/hook words to check | ||
Examples of additional **incorrect** code for this rule with a sample `{ "all": true }` option: | ||
```js | ||
/*eslint cucumber/async-then: ["error", { "all": true }] */ | ||
Given(/some step/, function () { | ||
expect(foo).to.eventually.eql('doesn\'t matter because it will never be checked'); | ||
}); | ||
When(/some step/, function () { | ||
expect(foo).to.eventually.eql('doesn\'t matter because it will never be checked'); | ||
}); | ||
Before(/some step/, function () { | ||
expect(foo).to.eventually.eql('doesn\'t matter because it will never be checked'); | ||
}); | ||
BeforeAll(/some step/, function () { | ||
expect(foo).to.eventually.eql('doesn\'t matter because it will never be checked'); | ||
}); | ||
After(/some step/, function () { | ||
expect(foo).to.eventually.eql('doesn\'t matter because it will never be checked'); | ||
}); | ||
AfterAll(/some step/, function () { | ||
expect(foo).to.eventually.eql('doesn\'t matter because it will never be checked'); | ||
}); | ||
``` | ||
Examples of additional **incorrect** code for this rule with a sample `{ "words": [ "Given", "When", "Then" ] }` option: | ||
```js | ||
/*eslint cucumber/async-then: ["error", { "words": [ "Given", "When", "Then" ] }] */ | ||
Given(/some step/, function () { | ||
expect(foo).to.eventually.eql('doesn\'t matter because it will never be checked'); | ||
}); | ||
When(/some step/, function () { | ||
expect(foo).to.eventually.eql('doesn\'t matter because it will never be checked'); | ||
}); | ||
Then(/some step/, function () { | ||
expect(foo).to.eventually.eql('doesn\'t matter because it will never be checked'); | ||
}); | ||
``` | ||
## When Not To Use It | ||
When you're not dealing with asynchronous operations |
@@ -6,2 +6,3 @@ const _ = require('lodash'); | ||
'async-then': require('./rules/async-then'), | ||
'expression-type': require('./rules/expression-type'), | ||
'no-restricted-tags': require('./rules/no-restricted-tags'), | ||
@@ -8,0 +9,0 @@ 'no-arrow-functions': require('./rules/no-arrow-functions') |
@@ -1,63 +0,122 @@ | ||
module.exports = function(context) { | ||
const CALLBACK_NAMES = /^(next|done|callback)$/; | ||
const CALLBACK_NAMES = /^(next|done|callback)$/; | ||
const ALL_WORDS = [ | ||
'Given', | ||
'When', | ||
'Then', | ||
'Before', | ||
'BeforeAll', | ||
'After', | ||
'AfterAll' | ||
]; | ||
function isThenStep(node) { | ||
return isCucumberOneThenStep(node) || isCucumberTwoPlusThenStep(node); | ||
} | ||
function getMatchedStepOrHook(node, targetWords) { | ||
return ( | ||
getCucumberOneMatchedStepOrHook(node, targetWords) || | ||
getCucumberTwoPlusMatchedStepOrHook(node, targetWords) | ||
); | ||
} | ||
function isCucumberOneThenStep(node) { | ||
return ( | ||
node.callee && | ||
node.callee.object && | ||
node.callee.object.type === 'ThisExpression' && | ||
node.callee.property && | ||
node.callee.property.name === 'Then' | ||
); | ||
function getCucumberOneMatchedStepOrHook(node, targetWords) { | ||
if ( | ||
node.callee && | ||
node.callee.object && | ||
node.callee.object.type === 'ThisExpression' && | ||
node.callee.property && | ||
targetWords.indexOf(node.callee.property.name) !== -1 | ||
) { | ||
return node.callee.property.name; | ||
} else { | ||
return false; | ||
} | ||
} | ||
function isCucumberTwoPlusThenStep(node) { | ||
return ( | ||
node.type === 'CallExpression' && | ||
node.callee && | ||
node.callee.type === 'Identifier' && | ||
node.callee.name === 'Then' | ||
); | ||
function getCucumberTwoPlusMatchedStepOrHook(node, targetWords) { | ||
if ( | ||
node.type === 'CallExpression' && | ||
node.callee && | ||
node.callee.type === 'Identifier' && | ||
targetWords.indexOf(node.callee.name) !== -1 | ||
) { | ||
return node.callee.name; | ||
} else { | ||
return false; | ||
} | ||
} | ||
function didNotReturnAnythingIn(func) { | ||
const statements = func.body.body; | ||
return ( | ||
!statements.length || | ||
statements[statements.length - 1].type !== 'ReturnStatement' | ||
); | ||
} | ||
function didNotReturnAnythingIn(func) { | ||
const statements = func.body.body; | ||
return ( | ||
!statements.length || | ||
statements[statements.length - 1].type !== 'ReturnStatement' | ||
); | ||
} | ||
function doesNotHaveCallback(func) { | ||
return ( | ||
!func.params.length || | ||
!CALLBACK_NAMES.exec(func.params[func.params.length - 1].name) | ||
); | ||
} | ||
function doesNotHaveCallback(func) { | ||
return ( | ||
!func.params.length || | ||
!CALLBACK_NAMES.exec(func.params[func.params.length - 1].name) | ||
); | ||
} | ||
function isAsyncFunction(func) { | ||
return !!func.async; | ||
function isAsyncFunction(func) { | ||
return !!func.async; | ||
} | ||
function getTargetWords(context) { | ||
if (context.options.length) { | ||
const optionsObj = context.options[0]; | ||
if (optionsObj.all) { | ||
return ALL_WORDS; | ||
} | ||
if (optionsObj.words) { | ||
return optionsObj.words; | ||
} | ||
} | ||
return ['Then']; | ||
} | ||
return { | ||
CallExpression: function(node) { | ||
if (isThenStep(node)) { | ||
const stepBody = node.arguments[node.arguments.length - 1]; | ||
module.exports = { | ||
meta: { | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
all: { | ||
type: 'boolean' | ||
}, | ||
words: { | ||
type: 'array', | ||
items: { | ||
type: 'string' | ||
}, | ||
uniqueItems: true | ||
} | ||
} | ||
} | ||
] | ||
}, | ||
if (isAsyncFunction(stepBody)) { | ||
return; | ||
create: function(context) { | ||
const targetWords = getTargetWords(context); | ||
return { | ||
CallExpression: function(node) { | ||
const matchedStepOrHook = getMatchedStepOrHook(node, targetWords); | ||
if (matchedStepOrHook) { | ||
const stepBody = node.arguments[node.arguments.length - 1]; | ||
if (isAsyncFunction(stepBody)) { | ||
return; | ||
} | ||
if ( | ||
doesNotHaveCallback(stepBody) && | ||
didNotReturnAnythingIn(stepBody) | ||
) { | ||
context.report( | ||
node.callee.property || node.callee, | ||
`${matchedStepOrHook} implementation should be async; use an async function, return a promise, or have a callback` | ||
); | ||
} | ||
} | ||
if (doesNotHaveCallback(stepBody) && didNotReturnAnythingIn(stepBody)) { | ||
context.report( | ||
node.callee.property || node.callee, | ||
"Then step didn't return a promise or have a callback." | ||
); | ||
} | ||
} | ||
} | ||
}; | ||
}; | ||
} | ||
}; |
{ | ||
"name": "eslint-plugin-cucumber", | ||
"version": "1.3.0", | ||
"version": "1.4.0", | ||
"description": "eslint rules for cucumber steps", | ||
@@ -29,3 +29,4 @@ "keywords": [ | ||
"format": "prettier --loglevel warn --write \"**/*.js\"", | ||
"test": "npm run --silent format && script/test.sh" | ||
"test": "mocha tests/**/*.js", | ||
"verify": "npm run format && npm run test" | ||
}, | ||
@@ -37,2 +38,3 @@ "dependencies": { | ||
"eslint": "^4.0.0", | ||
"mocha": "^6.2.1", | ||
"prettier": "^1.14.2" | ||
@@ -39,0 +41,0 @@ }, |
@@ -40,2 +40,3 @@ # eslint-plugin-cucumber | ||
"cucumber/async-then": 2, | ||
"cucumber/expression-type": 2, | ||
"cucumber/no-restricted-tags": [2, "wip", "broken", "foo"], | ||
@@ -51,5 +52,6 @@ "cucumber/no-arrow-functions": 2 | ||
| ------------- | ------------- | | ||
| async-then | If you assume asynchronous steps then your Then steps should either be an async function, return a promise or provide a callback function | | ||
| no-restricted-tags | Restrict usage of specified tags | | ||
| no-arrow-functions | Restrict usage of arrow functions on step definitions | | ||
| [async-then](docs/rules/async-then.md) | If you assume asynchronous steps/hooks then your implementation should either be an async function, return a promise or provide a callback function| | ||
| [expression-type](docs/rules/expression-type.md) | Restrict steps to either Cucumber Expressions or Regular Expressions | | ||
| [no-restricted-tags](docs/rules/no-restricted-tags.md) | Restrict usage of specified tags | | ||
| [no-arrow-functions](docs/rules/no-arrow-functions.md) | Restrict usage of arrow functions on step definitions | | ||
@@ -56,0 +58,0 @@ ## For Maintainers |
@@ -8,2 +8,3 @@ const rule = require('../../../lib/rules/async-then'); | ||
valid: [ | ||
// no options provided, just check `Then`s | ||
'this.Then(/step/, async function () {})', | ||
@@ -22,6 +23,54 @@ 'Then(/step/, async function () {})', | ||
'this.When(/step/, function () {})', | ||
'When(/step/, function () {})' | ||
'When(/step/, function () {})', | ||
'this.Before(/step/, function () {})', | ||
'Before(/step/, function () {})', | ||
'this.BeforeAll(/step/, function () {})', | ||
'BeforeAll(/step/, function () {})', | ||
'this.After(/step/, function () {})', | ||
'After(/step/, function () {})', | ||
'this.AfterAll(/step/, function () {})', | ||
'AfterAll(/step/, function () {})', | ||
// words specified in options | ||
{ | ||
code: 'Given(/step/, async function () {})', | ||
options: [{words: ['Given', 'When']}] | ||
}, | ||
{ | ||
code: 'When(/step/, async function () {})', | ||
options: [{words: ['Given', 'When']}] | ||
}, | ||
{ | ||
code: 'Then(/step/, function () {})', | ||
options: [{words: ['Given', 'When']}] | ||
}, | ||
{ | ||
code: 'Before(/step/, function () {})', | ||
options: [{words: ['Given', 'When']}] | ||
}, | ||
{ | ||
code: 'BeforeAll(/step/, function () {})', | ||
options: [{words: ['Given', 'When']}] | ||
}, | ||
{ | ||
code: 'After(/step/, function () {})', | ||
options: [{words: ['Given', 'When']}] | ||
}, | ||
{ | ||
code: 'AfterAll(/step/, function () {})', | ||
options: [{words: ['Given', 'When']}] | ||
}, | ||
// all words option | ||
{code: 'Given(/step/, async function () {})', options: [{all: true}]}, | ||
{code: 'When(/step/, async function () {})', options: [{all: true}]}, | ||
{code: 'Then(/step/, async function () {})', options: [{all: true}]}, | ||
{code: 'Before(/step/, async function () {})', options: [{all: true}]}, | ||
{code: 'BeforeAll(/step/, async function () {})', options: [{all: true}]}, | ||
{code: 'After(/step/, async function () {})', options: [{all: true}]}, | ||
{code: 'AfterAll(/step/, async function () {})', options: [{all: true}]} | ||
], | ||
invalid: [ | ||
// no options provided, just check `Then`s | ||
{ | ||
@@ -31,3 +80,4 @@ code: 'this.Then(/step/, function () {})', | ||
{ | ||
message: "Then step didn't return a promise or have a callback." | ||
message: | ||
'Then implementation should be async; use an async function, return a promise, or have a callback' | ||
} | ||
@@ -40,7 +90,102 @@ ] | ||
{ | ||
message: "Then step didn't return a promise or have a callback." | ||
message: | ||
'Then implementation should be async; use an async function, return a promise, or have a callback' | ||
} | ||
] | ||
}, | ||
// words specified in options | ||
{ | ||
code: 'Given(/step/, function () {})', | ||
errors: [ | ||
{ | ||
message: | ||
'Given implementation should be async; use an async function, return a promise, or have a callback' | ||
} | ||
], | ||
options: [{words: ['Given', 'When']}] | ||
}, | ||
{ | ||
code: 'When(/step/, function () {})', | ||
errors: [ | ||
{ | ||
message: | ||
'When implementation should be async; use an async function, return a promise, or have a callback' | ||
} | ||
], | ||
options: [{words: ['Given', 'When']}] | ||
}, | ||
// all words option | ||
{ | ||
code: 'Given(/step/, function () {})', | ||
errors: [ | ||
{ | ||
message: | ||
'Given implementation should be async; use an async function, return a promise, or have a callback' | ||
} | ||
], | ||
options: [{all: true}] | ||
}, | ||
{ | ||
code: 'When(/step/, function () {})', | ||
errors: [ | ||
{ | ||
message: | ||
'When implementation should be async; use an async function, return a promise, or have a callback' | ||
} | ||
], | ||
options: [{all: true}] | ||
}, | ||
{ | ||
code: 'Then(/step/, function () {})', | ||
errors: [ | ||
{ | ||
message: | ||
'Then implementation should be async; use an async function, return a promise, or have a callback' | ||
} | ||
], | ||
options: [{all: true}] | ||
}, | ||
{ | ||
code: 'Before(/step/, function () {})', | ||
errors: [ | ||
{ | ||
message: | ||
'Before implementation should be async; use an async function, return a promise, or have a callback' | ||
} | ||
], | ||
options: [{all: true}] | ||
}, | ||
{ | ||
code: 'BeforeAll(/step/, function () {})', | ||
errors: [ | ||
{ | ||
message: | ||
'BeforeAll implementation should be async; use an async function, return a promise, or have a callback' | ||
} | ||
], | ||
options: [{all: true}] | ||
}, | ||
{ | ||
code: 'After(/step/, function () {})', | ||
errors: [ | ||
{ | ||
message: | ||
'After implementation should be async; use an async function, return a promise, or have a callback' | ||
} | ||
], | ||
options: [{all: true}] | ||
}, | ||
{ | ||
code: 'AfterAll(/step/, function () {})', | ||
errors: [ | ||
{ | ||
message: | ||
'AfterAll implementation should be async; use an async function, return a promise, or have a callback' | ||
} | ||
], | ||
options: [{all: true}] | ||
} | ||
] | ||
}); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
35295
18
942
64
3