Skip to content

Commit 8550344

Browse files
committed
feat(rules): add 'no-angular-attributes' rule
1 parent 8f0f78c commit 8550344

File tree

8 files changed

+266
-12
lines changed

8 files changed

+266
-12
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,9 @@ Rule | Default Error Level | Auto-fixable | Options
4444
[no-by-xpath][] | 1 | |
4545
[no-describe-selectors][] | 1 | |
4646
[no-angular-classes][] | 1 | |
47-
[no-bootstrap-classes][] | 1 | |
4847
[use-angular-locators][] | 1 | |
48+
[no-angular-attributes][] | 1 | |
49+
[no-bootstrap-classes][] | 1 | |
4950
[use-simple-repeaters][] | 1 | |
5051
[no-shadowing][] | 1 | |
5152
[use-first-last][] | 1 | Yes |
@@ -101,6 +102,7 @@ See [configuring rules][] for more information.
101102
[no-repetitive-locators]: docs/rules/no-repetitive-locators.md
102103
[no-get-inner-outer-html]: docs/rules/no-get-inner-outer-html.md
103104
[no-repetitive-selectors]: docs/rules/no-repetitive-selectors.md
105+
[no-angular-attributes]: docs/rules/no-angular-attributes.md
104106
[configuring rules]: http://eslint.org/docs/user-guide/configuring#configuring-rules
105107

106108
## Recommended configuration
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Discourage using Angular attributes inside CSS selectors
2+
3+
Ensure attributes used by Angular internally are not used to locate elements via Protractor.
4+
5+
## Rule details
6+
7+
The rule would scan attributes used inside CSS selectors and would complain about any attribute that starts with `ng-` or `data-ng-` or `x-ng-`.
8+
9+
Any use of the following patterns are considered warnings:
10+
11+
```js
12+
element(by.css("[ng-show=test]"));
13+
element.all(by.css("[ng-hide]"));
14+
$("[ng-src*=test]");
15+
$$("[x-ng-href$=com]");
16+
$("[data-ng-cloak]");
17+
$$("a[href^=/], .container:has(nav) > [ng-focus]");
18+
$("a[href^=/], [ng-init*='test'] > .container");
19+
element(by.id("id")).$$("[ng-blur^=expression], a[href^=/]");
20+
element(by.id("id")).$("[data-ng-pattern*='test']");
21+
```
22+
23+
The following patterns are not warnings:
24+
25+
```js
26+
element(by.css("input"));
27+
element.all(by.css(".container"));
28+
$(".show");
29+
$$(".hide");
30+
$("[cloak]");
31+
$$("a[href^=/], .container:has(nav)");
32+
$("a[href^=/], .container");
33+
element(by.id("id")).$$("a[href^=/]");
34+
element(by.id("id")).$("input");
35+
var s = "ng-cloak";
36+
element(by.id("data-ng-pattern"));
37+
```

index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var correctChaining = require('./lib/rules/correct-chaining')
2323
var noRepetitiveLocators = require('./lib/rules/no-repetitive-locators')
2424
var noRepetitiveSelectors = require('./lib/rules/no-repetitive-selectors')
2525
var noGetInnerOuterHtml = require('./lib/rules/no-get-inner-outer-html')
26+
var noAngularAttributes = require('./lib/rules/no-angular-attributes')
2627

2728
module.exports = {
2829
rules: {
@@ -48,7 +49,8 @@ module.exports = {
4849
'no-repetitive-locators': noRepetitiveLocators,
4950
'no-repetitive-selectors': noRepetitiveSelectors,
5051
'correct-chaining': correctChaining,
51-
'no-get-inner-outer-html': noGetInnerOuterHtml
52+
'no-get-inner-outer-html': noGetInnerOuterHtml,
53+
'no-angular-attributes': noAngularAttributes
5254
},
5355
configs: {
5456
recommended: {
@@ -75,6 +77,7 @@ module.exports = {
7577
'protractor/no-repetitive-locators': 1,
7678
'protractor/no-repetitive-selectors': 1,
7779
'protractor/no-get-inner-outer-html': 1,
80+
'protractor/no-angular-attributes': 1,
7881
'protractor/by-css-shortcut': 0
7982
},
8083
globals: {

lib/extract-attributes.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
'use strict'
2+
3+
/**
4+
* @fileoverview Utility function to extract attribute names from CSS selectors
5+
* @author Alexander Afanasyev
6+
*/
7+
8+
var parser = require('./get-css-parser')
9+
10+
function extractAttributes (rule) {
11+
var attributes = []
12+
if (rule.attrs) {
13+
rule.attrs.forEach(function (attr) {
14+
attributes.push(attr.name)
15+
})
16+
}
17+
return attributes
18+
}
19+
20+
module.exports = function (cssSelector) {
21+
try {
22+
var result = parser.parse(cssSelector)
23+
} catch (err) {
24+
// ignore parsing errors - we don't want it to fail miserably on a target machine during a ESLint run
25+
console.log('Parsing CSS selector: "' + cssSelector + '". ' + err)
26+
return []
27+
}
28+
29+
// handling empty inputs
30+
if (!result) {
31+
return []
32+
}
33+
34+
var attributes = []
35+
36+
if (result.type === 'ruleSet') {
37+
var rule = result.rule
38+
while (rule) {
39+
attributes.push.apply(attributes, extractAttributes(rule))
40+
rule = rule.rule
41+
}
42+
} else if (result.type === 'selectors' && result.selectors) {
43+
result.selectors.forEach(function (selector) {
44+
var rule = selector.rule
45+
while (rule) {
46+
attributes.push.apply(attributes, extractAttributes(rule))
47+
rule = rule.rule
48+
}
49+
})
50+
}
51+
return attributes
52+
}

lib/extract-class-names.js

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,7 @@
55
* @author Alexander Afanasyev
66
*/
77

8-
// setup up CSS selector parser
9-
var CssSelectorParser = require('css-selector-parser').CssSelectorParser
10-
var parser = new CssSelectorParser()
11-
12-
parser.registerSelectorPseudos('has', 'contains')
13-
parser.registerNestingOperators('>', '+', '~')
14-
parser.registerAttrEqualityMods('^', '$', '*', '~', '|')
15-
parser.enableSubstitutes()
8+
var parser = require('./get-css-parser')
169

1710
function extractClassNames (rule) {
1811
var classNames = []
@@ -29,7 +22,6 @@ function extractClassNames (rule) {
2922
}
3023
})
3124
}
32-
3325
return classNames
3426
}
3527

@@ -48,7 +40,6 @@ module.exports = function (cssSelector) {
4840
}
4941

5042
var classNames = []
51-
5243
if (result.type === 'ruleSet') {
5344
var rule = result.rule
5445
while (rule) {

lib/get-css-parser.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
'use strict'
2+
3+
/**
4+
* @fileoverview Utility function to set up and configure CSS selector parser
5+
* @author Alexander Afanasyev
6+
*/
7+
8+
// setup up CSS selector parser
9+
var CssSelectorParser = require('css-selector-parser').CssSelectorParser
10+
var parser = new CssSelectorParser()
11+
12+
parser.registerSelectorPseudos('has', 'contains')
13+
parser.registerNestingOperators('>', '+', '~')
14+
parser.registerAttrEqualityMods('^', '$', '*', '~', '|')
15+
parser.enableSubstitutes()
16+
17+
module.exports = parser

lib/rules/no-angular-attributes.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict'
2+
3+
/**
4+
* @fileoverview Discourage using Angular attributes inside CSS selectors
5+
* @author Alexander Afanasyev
6+
*/
7+
var isCSSLocator = require('../find-css-locator')
8+
var extractAttributes = require('../extract-attributes')
9+
10+
var ANGULAR_ATTR_RE = /^(ng-|data-ng-|x-ng-)/
11+
12+
module.exports = {
13+
meta: {
14+
schema: []
15+
},
16+
17+
create: function (context) {
18+
return {
19+
'CallExpression': function (node) {
20+
if (node.arguments && node.arguments.length && node.arguments[0].hasOwnProperty('value')) {
21+
if (isCSSLocator(node)) {
22+
var extractedAttributes = extractAttributes(node.arguments[0].value)
23+
24+
extractedAttributes.forEach(function (extractedAttribute) {
25+
if (ANGULAR_ATTR_RE.test(extractedAttribute)) {
26+
context.report({
27+
node: node,
28+
message: 'Unexpected Angular attribute "' + extractedAttribute + '" inside a CSS selector'
29+
})
30+
}
31+
})
32+
}
33+
}
34+
}
35+
}
36+
}
37+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
'use strict'
2+
3+
var rule = require('../../lib/rules/no-angular-attributes')
4+
var RuleTester = require('eslint').RuleTester
5+
6+
var eslintTester = new RuleTester()
7+
8+
eslintTester.run('no-angular-attributes', rule, {
9+
valid: [
10+
'element(by.css("input"));',
11+
'element(by.css("[ng-show"));',
12+
'element.all(by.css(".container"));',
13+
'$(".show");',
14+
'$$(".hide");',
15+
'$("[cloak]");',
16+
'$("[test-ng-cloak]");',
17+
'$$("a[href^=/], .container:has(nav)");',
18+
'$("a[href^=/], .container");',
19+
'element(by.id("id")).$$("a[href^=/]");',
20+
'element(by.id("id")).$("input");',
21+
'var s = "ng-cloak";',
22+
'element(by.id("data-ng-pattern"));',
23+
'$("");',
24+
'$();',
25+
'$$();',
26+
'element(by.css());',
27+
'element.all(by.css());'
28+
],
29+
30+
invalid: [
31+
{
32+
code: 'element(by.css("[ng-show=test]"));',
33+
errors: [
34+
{
35+
message: 'Unexpected Angular attribute "ng-show" inside a CSS selector'
36+
}
37+
]
38+
},
39+
{
40+
code: 'element.all(by.css("[ng-hide]"));',
41+
errors: [
42+
{
43+
message: 'Unexpected Angular attribute "ng-hide" inside a CSS selector'
44+
}
45+
]
46+
},
47+
{
48+
code: '$("[ng-src*=test]");',
49+
errors: [
50+
{
51+
message: 'Unexpected Angular attribute "ng-src" inside a CSS selector'
52+
}
53+
]
54+
},
55+
{
56+
code: '$$("[x-ng-href$=com]");',
57+
errors: [
58+
{
59+
message: 'Unexpected Angular attribute "x-ng-href" inside a CSS selector'
60+
}
61+
]
62+
},
63+
{
64+
code: '$("[data-ng-cloak]");',
65+
errors: [
66+
{
67+
message: 'Unexpected Angular attribute "data-ng-cloak" inside a CSS selector'
68+
}
69+
]
70+
},
71+
{
72+
code: '$$("a[href^=/], .container:has(nav) > [ng-focus]");',
73+
errors: [
74+
{
75+
message: 'Unexpected Angular attribute "ng-focus" inside a CSS selector'
76+
}
77+
]
78+
},
79+
{
80+
code: '$("a[href^=/], [ng-init*=\'test\'] > .container");',
81+
errors: [
82+
{
83+
message: 'Unexpected Angular attribute "ng-init" inside a CSS selector'
84+
}
85+
]
86+
},
87+
{
88+
code: 'element(by.id("id")).$$("[ng-blur^=expression], a[href^=/]");',
89+
errors: [
90+
{
91+
message: 'Unexpected Angular attribute "ng-blur" inside a CSS selector'
92+
}
93+
]
94+
},
95+
{
96+
code: 'element(by.id("id")).$("[data-ng-pattern*=\'test\']");',
97+
errors: [
98+
{
99+
message: 'Unexpected Angular attribute "data-ng-pattern" inside a CSS selector'
100+
}
101+
]
102+
},
103+
{
104+
code: 'element(by.id("id")).$("[ng-show][data-ng-src]");',
105+
errors: [
106+
{
107+
message: 'Unexpected Angular attribute "ng-show" inside a CSS selector'
108+
},
109+
{
110+
message: 'Unexpected Angular attribute "data-ng-src" inside a CSS selector'
111+
}
112+
]
113+
}
114+
]
115+
})

0 commit comments

Comments
 (0)