In Protractor, Jasmine expectations are patched to resolve WebDriver promises implicitly.
For instance, if you need to assert a count of certain elements, you can use
count()
method (which returns a promise) directly:expect($$(".myclass").count()).toEqual(5);
But, newcomers to Protractor can miss that and would do the same resolving promises explicitly which leads to a more complex code:
$$(".myclass").then(function (elements) { expect(elements.length).toEqual(5); });
I've developed an ESLint
rule to try to catch this kind of explicit "count" value check. The logic behind the rule is:
- It first locates the
.length
"member expression" - Iterates over the ancestors and checks if inside
expect
- Iterates over the ancestors and checks if inside a "then" callback and there is at least one argument passed to the callback function
- Determines if
.then
was called on an "element array finder"
'use strict'
/**
* @fileoverview Recommend using `count()` instead of `then()` and `length`
* @author Alexander Afanasyev
*/
var isElementArrayFinder = require('../is-element-array-finder')
var isThenCallBack = require('../is-then-callback')
var isExpect = require('../is-expect')
module.exports = {
meta: {
schema: []
},
create: function (context) {
return {
MemberExpression: function (node) {
if (node.property && node.object && node.property.name === 'length') {
// remember the variable name the ".length" was used on
var variableName = node.object.name
// find out if we are in an expect()
var expectAncestor
var thenAncestor
var ancestors = context.getAncestors(node)
for (var i = 0; i < ancestors.length; i++) {
expectAncestor = ancestors[i]
if (expectAncestor && expectAncestor.type === 'CallExpression' && isExpect(expectAncestor)) {
// find out if we are inside a then callback
ancestors = context.getAncestors(expectAncestor)
for (var j = 0; j < ancestors.length; j++) {
thenAncestor = ancestors[j]
if (thenAncestor && thenAncestor.type === 'CallExpression') {
var thenCallback = isThenCallBack(thenAncestor)
// it has to have at least one argument
if (thenCallback && thenCallback.params && thenCallback.params.length > 0) {
var thenCallbackArgument = thenCallback.params[0]
// the same variable is a "then" callback function argument
if (thenCallbackArgument && thenCallbackArgument.name === variableName) {
// check that it was an ElementArrayFinder resolution
if (thenAncestor.callee && thenAncestor.callee.object) {
if (isElementArrayFinder(thenAncestor.callee.object)) {
context.report({
node: node,
message: 'Array.length inside promise resolution function detected. Use count() instead.'
})
return
}
}
}
}
}
}
}
}
}
}
}
}
}
Source code is also available on GitHub.
The rule is overly complicated, very deep and nested and not readable at all (cyclomatic code complexity is 10). What refactoring methods would you apply to improve on readability?
FYI, using standard
style.