Skip to content

Commit 3927313

Browse files
committed
feat(rules): add 'user-count-method' rule
1 parent 640eeef commit 3927313

File tree

7 files changed

+347
-1
lines changed

7 files changed

+347
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Rule | Default Error Level | Auto-fixable | Options
6868
[no-repetitive-locators][] | 1 | |
6969
[no-repetitive-selectors][] | 1 | |
7070
[no-get-inner-outer-html][] | 1 | |
71+
[use-count-method][] | 1 | |
7172
[use-promise-all][] | 0 (Turned off) | |
7273
[by-css-shortcut][] | 0 | |
7374

@@ -117,6 +118,7 @@ See [configuring rules][] for more information.
117118
[no-array-finder-methods]: docs/rules/no-array-finder-methods.md
118119
[valid-locator-type]: docs/rules/valid-locator-type.md
119120
[no-compound-classes]: docs/rules/no-compound-classes.md
121+
[use-count-method]: docs/rules/use-count-method.md
120122
[configuring rules]: http://eslint.org/docs/user-guide/configuring#configuring-rules
121123

122124
## Recommended configuration

docs/rules/use-count-method.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# Recommend using `count()` instead of `then()` and `length`
2+
3+
When you need to assert how many elements were found, it is more readable and easier when you do it using [`count()`](http://www.protractortest.org/#/api?view=ElementArrayFinder.prototype.count):
4+
5+
```js
6+
expect(element.all(by.repeater('product in products')).count()).toBeGreaterThan(1);
7+
```
8+
9+
instead of resolving the `ElementArrayFinder` value and getting the `.length` property:
10+
11+
```js
12+
element.all(by.repeater('product in products')).then(function (products) {
13+
expect(products.length >= 1).toBeTruthy();
14+
});
15+
```
16+
17+
## Rule details
18+
19+
Any use of the following patterns are considered warnings:
20+
21+
```js
22+
element.all(by.repeater('product in products')).then(function (products) {
23+
expect(products.length >= 1).toBeTruthy();
24+
});
25+
26+
element.all(by.repeater('product in products')).then(function (products) {
27+
expect(10).toEqual(products.length);
28+
});
29+
```
30+
31+
The following patterns are not warnings:
32+
33+
```js
34+
expect(element.all(by.repeater('product in products')).count()).toBeGreaterThan(1);
35+
36+
var products = [];
37+
console.log(products.length);
38+
39+
element.all(by.repeater('product in products')).then(function (products) {
40+
console.log('test');
41+
});
42+
```

index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ var usePromiseAll = require('./lib/rules/use-promise-all')
2929
var noArrayFinderMethods = require('./lib/rules/no-array-finder-methods')
3030
var validLocatorType = require('./lib/rules/valid-locator-type')
3131
var noCompoundClasses = require('./lib/rules/no-compound-classes')
32+
var useCountMethod = require('./lib/rules/use-count-method')
3233

3334
module.exports = {
3435
rules: {
@@ -60,7 +61,8 @@ module.exports = {
6061
'use-promise-all': usePromiseAll,
6162
'no-array-finder-methods': noArrayFinderMethods,
6263
'valid-locator-type': validLocatorType,
63-
'no-compound-classes': noCompoundClasses
64+
'no-compound-classes': noCompoundClasses,
65+
'use-count-method': useCountMethod
6466
},
6567
configs: {
6668
recommended: {
@@ -92,6 +94,7 @@ module.exports = {
9294
'protractor/no-repetitive-selectors': 1,
9395
'protractor/no-get-inner-outer-html': 1,
9496
'protractor/no-angular-attributes': 1,
97+
'protractor/use-count-method': 1,
9598
'protractor/use-promise-all': 0,
9699
'protractor/by-css-shortcut': 0
97100
},

lib/is-expect.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict'
2+
3+
/**
4+
* Checks if a given node is an "expect" statement - works for both left and right parts
5+
*
6+
* @param {ASTNode} node - A node to check.
7+
* @returns {boolean}
8+
*/
9+
module.exports = function (node) {
10+
var callee = node.callee
11+
12+
// left part of "expect()"
13+
if (callee && callee.name === 'expect') {
14+
return true
15+
}
16+
17+
// right part of "expect()"
18+
if (callee.object && callee.object.type === 'CallExpression' && callee.object.callee) {
19+
callee = callee.object.callee
20+
if (callee.name && callee.name === 'expect') {
21+
return true
22+
}
23+
}
24+
}

lib/is-then-callback.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict'
2+
3+
/**
4+
* Checks if a given node is a "then" callback function
5+
*
6+
* @param {ASTNode} node - A node to check.
7+
* @returns {ASTNode} node - The first function argument node.
8+
*/
9+
module.exports = function (node) {
10+
var property = node.callee.property
11+
var isThen = property && property.name === 'then' && node.arguments
12+
if (isThen) {
13+
var argument = node.arguments[0]
14+
// only function type allowed
15+
var isFunction = argument && (argument.type === 'FunctionExpression' || argument.type === 'ArrowFunctionExpression')
16+
if (isFunction) {
17+
// it has to have at least one argument
18+
if (argument.params && argument.params.length > 0) {
19+
return argument.params[0]
20+
}
21+
}
22+
}
23+
return false
24+
}

lib/rules/use-count-method.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
'use strict'
2+
3+
/**
4+
* @fileoverview Recommend using `count()` instead of `then()` and `length`
5+
* @author Alexander Afanasyev
6+
*/
7+
8+
var isElementArrayFinder = require('../is-element-array-finder')
9+
var isThenCallBack = require('../is-then-callback')
10+
var isExpect = require('../is-expect')
11+
12+
module.exports = {
13+
meta: {
14+
schema: []
15+
},
16+
17+
create: function (context) {
18+
return {
19+
MemberExpression: function (node) {
20+
if (node.property && node.object && node.property.name === 'length') {
21+
// remember the variable name the ".length" was used on
22+
var variableName = node.object.name
23+
24+
// find out if we are in an expect()
25+
var expectAncestor
26+
var thenAncestor
27+
var ancestors = context.getAncestors(node)
28+
29+
for (var i = 0; i < ancestors.length; i++) {
30+
expectAncestor = ancestors[i]
31+
if (expectAncestor && expectAncestor.type === 'CallExpression' && isExpect(expectAncestor)) {
32+
// find out if we are inside a then callback
33+
ancestors = context.getAncestors(expectAncestor)
34+
for (var j = 0; j < ancestors.length; j++) {
35+
thenAncestor = ancestors[j]
36+
if (thenAncestor && thenAncestor.type === 'CallExpression') {
37+
var thenCallbackArgument = isThenCallBack(thenAncestor)
38+
39+
// the same variable is a "then" callback function argument
40+
if (thenCallbackArgument && thenCallbackArgument.name === variableName) {
41+
// check that it was an ElementArrayFinder resolution
42+
if (thenAncestor.callee && thenAncestor.callee.object) {
43+
if (isElementArrayFinder(thenAncestor.callee.object)) {
44+
context.report({
45+
node: node,
46+
message: 'Array.length inside promise resolution function detected. Use count() instead.'
47+
})
48+
return
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
}
59+
}
60+
}

test/rules/use-count-method.js

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
'use strict'
2+
3+
var rule = require('../../lib/rules/use-count-method')
4+
var RuleTester = require('eslint').RuleTester
5+
var toCode = require('../lib/join-multiline-code')
6+
7+
var eslintTester = new RuleTester()
8+
9+
eslintTester.run('use-count-method', rule, {
10+
valid: [
11+
toCode([
12+
'expect(element.all(by.repeater("product in products")).count()).toBeGreaterThan(1);'
13+
]),
14+
toCode([
15+
'var products = [];',
16+
'console.log(products.length);'
17+
]),
18+
toCode([
19+
'element.all(by.repeater("product in products")).then(function (products) {',
20+
' console.log("test");',
21+
'});'
22+
]),
23+
toCode([
24+
'element.all(by.repeater("product in products")).then(function (products) {',
25+
' var testArray = [1, 2, 3];',
26+
' expect(testArray.length).toEqual(3)',
27+
'});'
28+
]),
29+
toCode([
30+
'element.all(by.repeater("product in products")).then(function (products) {',
31+
' var testArray = [1, 2, 3];',
32+
' expect(3).toEqual(testArray.length)',
33+
'});'
34+
]),
35+
toCode([
36+
'element.all(by.repeater("product in products")).then(function () {',
37+
'});'
38+
]),
39+
toCode([
40+
'element.all(by.repeater("product in products")).then(function () {',
41+
' var testArray = [1, 2, 3];',
42+
' expect(3).toEqual(testArray.length)',
43+
'});'
44+
]),
45+
toCode([
46+
'element.all(by.repeater("product in products")).filter(function (elm) {',
47+
' return true;',
48+
'});'
49+
]),
50+
toCode([
51+
'element.all(by.repeater("product in products")).then(console.log);'
52+
]),
53+
toCode([
54+
'somePromise.then(function (products) {',
55+
' expect(products.length).toEqual(1);',
56+
'});'
57+
]),
58+
toCode([
59+
'then(function (products) {',
60+
' expect(products.length).toEqual(1);',
61+
'});'
62+
]),
63+
toCode([
64+
'var products = [1, 2, 3];',
65+
'element.all(by.repeater("product in products")).then(function () {',
66+
' expect(products.length).toEqual(1);',
67+
'});'
68+
]),
69+
toCode([
70+
'promise.method().then(function (products) {',
71+
' expect(products.length).toEqual(1);',
72+
'});'
73+
])
74+
// TODO: promise.all().then() - recognized as an ElementArrayFinder!
75+
// TODO: run against ap-ui-html
76+
],
77+
78+
invalid: [
79+
{
80+
code: toCode([
81+
'element.all(by.repeater("product in products")).then(function (products) {',
82+
' expect(products.length >= 1).toBeTruthy();',
83+
'});'
84+
]),
85+
errors: [
86+
{
87+
message: 'Array.length inside promise resolution function detected. Use count() instead.'
88+
}
89+
]
90+
},
91+
{
92+
code: toCode([
93+
'element.all(by.repeater("product in products")).then((products) => {',
94+
' expect(products.length >= 1).toBeTruthy();',
95+
'});'
96+
]),
97+
parserOptions: {
98+
ecmaVersion: 6
99+
},
100+
errors: [
101+
{
102+
message: 'Array.length inside promise resolution function detected. Use count() instead.'
103+
}
104+
]
105+
},
106+
{
107+
code: toCode([
108+
'element.all(by.repeater("product in products")).then(function (products) {',
109+
' expect(1).toEqual(products.length);',
110+
'});'
111+
]),
112+
errors: [
113+
{
114+
message: 'Array.length inside promise resolution function detected. Use count() instead.'
115+
}
116+
]
117+
},
118+
{
119+
code: toCode([
120+
'element.all(by.repeater("product in products")).then((products) => {',
121+
' expect(1).toEqual(products.length);',
122+
'});'
123+
]),
124+
parserOptions: {
125+
ecmaVersion: 6
126+
},
127+
errors: [
128+
{
129+
message: 'Array.length inside promise resolution function detected. Use count() instead.'
130+
}
131+
]
132+
},
133+
{
134+
code: toCode([
135+
'element.all(by.repeater("product in products")).then(function (products) {',
136+
' expect(products.length).toEqual(products.length);',
137+
'});'
138+
]),
139+
errors: [
140+
{
141+
message: 'Array.length inside promise resolution function detected. Use count() instead.'
142+
},
143+
{
144+
message: 'Array.length inside promise resolution function detected. Use count() instead.'
145+
}
146+
]
147+
},
148+
{
149+
code: toCode([
150+
'element.all(by.repeater("product in products")).then((products) => {',
151+
' expect(products.length).toEqual(products.length);',
152+
'});'
153+
]),
154+
parserOptions: {
155+
ecmaVersion: 6
156+
},
157+
errors: [
158+
{
159+
message: 'Array.length inside promise resolution function detected. Use count() instead.'
160+
},
161+
{
162+
message: 'Array.length inside promise resolution function detected. Use count() instead.'
163+
}
164+
]
165+
},
166+
{
167+
code: toCode([
168+
'$$(".product").then(function (products) {',
169+
' expect(products.length >= 1).toBeTruthy();',
170+
'});'
171+
]),
172+
errors: [
173+
{
174+
message: 'Array.length inside promise resolution function detected. Use count() instead.'
175+
}
176+
]
177+
},
178+
{
179+
code: toCode([
180+
'$$(".product").then(function (products) {',
181+
' expect(1).toEqual(products.length);',
182+
'});'
183+
]),
184+
errors: [
185+
{
186+
message: 'Array.length inside promise resolution function detected. Use count() instead.'
187+
}
188+
]
189+
}
190+
]
191+
})

0 commit comments

Comments
 (0)