Skip to content

Commit 1f8dad6

Browse files
committed
feat(rules): add 'no-bootstrap-classes' rule
1 parent 325301b commit 1f8dad6

File tree

9 files changed

+453
-1
lines changed

9 files changed

+453
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Rule | Default | Options
4343
[no-by-xpath][] | 1 |
4444
[no-describe-selectors][] | 1 |
4545
[no-angular-classes][] | 1 |
46+
[no-bootstrap-classes][] | 1 |
4647
[use-angular-locators][] | 1 |
4748
[use-simple-repeaters][] | 1 |
4849
[no-shadowing][] | 1 |
@@ -74,6 +75,7 @@ See [configuring rules][] for more information.
7475
[no-by-xpath]: docs/rules/no-by-xpath.md
7576
[no-describe-selectors]: docs/rules/no-describe-selectors.md
7677
[no-angular-classes]: docs/rules/no-angular-classes.md
78+
[no-bootstrap-classes]: docs/rules/no-bootstrap-classes.md
7779
[use-angular-locators]: docs/rules/use-angular-locators.md
7880
[use-simple-repeaters]: docs/rules/use-simple-repeaters.md
7981
[no-shadowing]: docs/rules/no-shadowing.md

docs/rules/no-bootstrap-classes.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Discourage using Bootstrap layout-oriented CSS classes inside CSS selectors
2+
3+
Ensure layout-oriented Bootstrap classes are not used inside CSS selectors.
4+
5+
Classes like `col-lg-pull-5` or `col-sm-offset-11` define a container layout on a page and do not bring any valuable information about the element. Neither they uniquely identify elements.
6+
Compare these classes with, for example, `product` or `itemPrice` classes - these classes are, generally, a good choice to base locators on since they have a "data" meaning not related to the way elements are presented on a page.
7+
8+
## Rule details
9+
10+
Current list of Bootstrap classes this rule would complain about can be viewed [here](lib/bootstrap-layout-classes.js) (hand-picked).
11+
12+
Any use of the following patterns are considered warnings:
13+
14+
```js
15+
element(by.css(".col-lg-12"));
16+
element.all(by.css(".col-lg-offset-11"));
17+
$(".col-sm-11");
18+
$$(".col-sm-push-4");
19+
$("[class='col-md-10']");
20+
$$("[class*='col-md-offset-4']");
21+
$(".myclass.col-lg-pull-8");
22+
element(by.id("id")).$$(".col-lg-offset-8.myclass");
23+
element(by.id("id")).$("input.col-lg-pull-8");
24+
```
25+
26+
The following patterns are not warnings:
27+
28+
```js
29+
element(by.css(".myclass"));
30+
element.all(by.css(".myclass"));
31+
$(".myclass");
32+
$$(".myclass");
33+
$("input.myclass");
34+
```

index.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ var noByXpath = require('./lib/rules/no-by-xpath')
88
var noDescribeSelectors = require('./lib/rules/no-describe-selectors')
99
var byCssShortcut = require('./lib/rules/by-css-shortcut')
1010
var noAngularClasses = require('./lib/rules/no-angular-classes')
11+
var noBootstrapClasses = require('./lib/rules/no-bootstrap-classes')
1112
var useAngularLocators = require('./lib/rules/use-angular-locators')
1213
var useSimpleRepeaters = require('./lib/rules/use-simple-repeaters')
1314
var noShadowing = require('./lib/rules/no-shadowing')
@@ -26,6 +27,7 @@ module.exports = {
2627
'no-describe-selectors': noDescribeSelectors,
2728
'by-css-shortcut': byCssShortcut,
2829
'no-angular-classes': noAngularClasses,
30+
'no-bootstrap-classes': noBootstrapClasses,
2931
'use-angular-locators': useAngularLocators,
3032
'use-simple-repeaters': useSimpleRepeaters,
3133
'no-shadowing': noShadowing,
@@ -44,6 +46,7 @@ module.exports = {
4446
'protractor/no-by-xpath': 1,
4547
'protractor/no-describe-selectors': 1,
4648
'protractor/no-angular-classes': 1,
49+
'protractor/no-bootstrap-classes': 1,
4750
'protractor/use-angular-locators': 1,
4851
'protractor/use-simple-repeaters': 1,
4952
'protractor/no-shadowing': 1,

lib/bootstrap-layout-classes.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
'use strict'
2+
3+
/**
4+
* @fileoverview Provides an array of Bootstrap layout-oriented classes
5+
* @author Alexander Afanasyev
6+
*/
7+
module.exports = [
8+
'col-lg-12',
9+
'col-xs-11',
10+
'col-xs-1',
11+
'col-xs-2',
12+
'col-xs-3',
13+
'col-xs-4',
14+
'col-xs-5',
15+
'col-xs-6',
16+
'col-xs-7',
17+
'col-xs-8',
18+
'col-xs-9',
19+
'col-xs-10',
20+
'col-xs-12',
21+
'col-sm-11',
22+
'col-sm-1',
23+
'col-sm-2',
24+
'col-sm-3',
25+
'col-sm-4',
26+
'col-sm-5',
27+
'col-sm-6',
28+
'col-sm-7',
29+
'col-sm-8',
30+
'col-sm-9',
31+
'col-sm-10',
32+
'col-sm-12',
33+
'col-sm-push-1',
34+
'col-sm-push-2',
35+
'col-sm-push-3',
36+
'col-sm-push-4',
37+
'col-sm-push-5',
38+
'col-sm-push-6',
39+
'col-sm-push-7',
40+
'col-sm-push-8',
41+
'col-sm-push-9',
42+
'col-sm-push-10',
43+
'col-sm-push-11',
44+
'col-sm-pull-1',
45+
'col-sm-pull-2',
46+
'col-sm-pull-3',
47+
'col-sm-pull-4',
48+
'col-sm-pull-5',
49+
'col-sm-pull-6',
50+
'col-sm-pull-7',
51+
'col-sm-pull-8',
52+
'col-sm-pull-9',
53+
'col-sm-pull-10',
54+
'col-sm-pull-11',
55+
'col-sm-offset-1',
56+
'col-sm-offset-2',
57+
'col-sm-offset-3',
58+
'col-sm-offset-4',
59+
'col-sm-offset-5',
60+
'col-sm-offset-6',
61+
'col-sm-offset-7',
62+
'col-sm-offset-8',
63+
'col-sm-offset-9',
64+
'col-sm-offset-10',
65+
'col-sm-offset-11',
66+
'col-md-11',
67+
'col-md-1',
68+
'col-md-2',
69+
'col-md-3',
70+
'col-md-4',
71+
'col-md-5',
72+
'col-md-6',
73+
'col-md-7',
74+
'col-md-8',
75+
'col-md-9',
76+
'col-md-10',
77+
'col-md-12',
78+
'col-md-push-0',
79+
'col-md-push-1',
80+
'col-md-push-2',
81+
'col-md-push-3',
82+
'col-md-push-4',
83+
'col-md-push-5',
84+
'col-md-push-6',
85+
'col-md-push-7',
86+
'col-md-push-8',
87+
'col-md-push-9',
88+
'col-md-push-10',
89+
'col-md-push-11',
90+
'col-md-pull-0',
91+
'col-md-pull-1',
92+
'col-md-pull-2',
93+
'col-md-pull-3',
94+
'col-md-pull-4',
95+
'col-md-pull-5',
96+
'col-md-pull-6',
97+
'col-md-pull-7',
98+
'col-md-pull-8',
99+
'col-md-pull-9',
100+
'col-md-pull-10',
101+
'col-md-pull-11',
102+
'col-md-offset-0',
103+
'col-md-offset-1',
104+
'col-md-offset-2',
105+
'col-md-offset-3',
106+
'col-md-offset-4',
107+
'col-md-offset-5',
108+
'col-md-offset-6',
109+
'col-md-offset-7',
110+
'col-md-offset-8',
111+
'col-md-offset-9',
112+
'col-md-offset-10',
113+
'col-md-offset-11',
114+
'col-lg-11',
115+
'col-lg-1',
116+
'col-lg-2',
117+
'col-lg-3',
118+
'col-lg-4',
119+
'col-lg-5',
120+
'col-lg-6',
121+
'col-lg-7',
122+
'col-lg-8',
123+
'col-lg-9',
124+
'col-lg-10',
125+
'col-lg-push-0',
126+
'col-lg-push-1',
127+
'col-lg-push-2',
128+
'col-lg-push-3',
129+
'col-lg-push-4',
130+
'col-lg-push-5',
131+
'col-lg-push-6',
132+
'col-lg-push-7',
133+
'col-lg-push-8',
134+
'col-lg-push-9',
135+
'col-lg-push-10',
136+
'col-lg-push-11',
137+
'col-lg-pull-0',
138+
'col-lg-pull-1',
139+
'col-lg-pull-2',
140+
'col-lg-pull-3',
141+
'col-lg-pull-4',
142+
'col-lg-pull-5',
143+
'col-lg-pull-6',
144+
'col-lg-pull-7',
145+
'col-lg-pull-8',
146+
'col-lg-pull-9',
147+
'col-lg-pull-10',
148+
'col-lg-pull-11',
149+
'col-lg-offset-0',
150+
'col-lg-offset-1',
151+
'col-lg-offset-2',
152+
'col-lg-offset-3',
153+
'col-lg-offset-4',
154+
'col-lg-offset-5',
155+
'col-lg-offset-6',
156+
'col-lg-offset-7',
157+
'col-lg-offset-8',
158+
'col-lg-offset-9',
159+
'col-lg-offset-10',
160+
'col-lg-offset-11'
161+
]

lib/extract-class-names.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use strict'
2+
3+
/**
4+
* @fileoverview Utility function to extract class names from a CSS selector
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+
function extractClassNames (rule) {
18+
var classNames = []
19+
// extract class names defined with ".", e.g. .myclass
20+
if (rule.classNames) {
21+
classNames.push.apply(classNames, rule.classNames)
22+
}
23+
24+
// extract class names defined in attributes, e.g. [class*=myclass]
25+
if (rule.attrs) {
26+
rule.attrs.forEach(function (attr) {
27+
if (attr.name === 'class') {
28+
classNames.push(attr.value)
29+
}
30+
})
31+
}
32+
33+
return classNames
34+
}
35+
36+
module.exports = function (cssSelector) {
37+
try {
38+
var result = parser.parse(cssSelector)
39+
} catch (err) {
40+
// ignore parsing errors - we don't want it to fail miserably on a target machine during a ESLint run
41+
console.log('Parsing CSS selector: "' + cssSelector + '". ' + err)
42+
return []
43+
}
44+
45+
var classNames = []
46+
47+
if (result.type === 'rule') {
48+
classNames = extractClassNames(result.rule)
49+
} else if (result.type === 'ruleSet') {
50+
var rule = result.rule
51+
while (rule) {
52+
classNames.push.apply(classNames, extractClassNames(rule))
53+
rule = rule.rule
54+
}
55+
} else if (result.type === 'selectors' && result.selectors) {
56+
result.selectors.forEach(function (selector) {
57+
classNames.push.apply(classNames, extractClassNames(selector.rule))
58+
})
59+
}
60+
return classNames
61+
}

lib/rules/no-angular-classes.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* @author Alexander Afanasyev
66
*/
77
var isCSSLocator = require('../find-css-locator')
8+
var extractClassNames = require('../extract-class-names')
89

910
module.exports = {
1011
meta: {
@@ -28,8 +29,10 @@ module.exports = {
2829
'CallExpression': function (node) {
2930
if (node.arguments && node.arguments.length && node.arguments[0].hasOwnProperty('value')) {
3031
if (isCSSLocator(node)) {
32+
var extractedClassNames = extractClassNames(node.arguments[0].value)
33+
3134
for (var i = 0; i < prohibitedClasses.length; i++) {
32-
if (node.arguments[0].value.indexOf(prohibitedClasses[i]) >= 0) {
35+
if (extractedClassNames.indexOf(prohibitedClasses[i]) >= 0) {
3336
context.report({
3437
node: node,
3538
message: 'Unexpected Angular class "' + prohibitedClasses[i] + '" inside a CSS selector'

lib/rules/no-bootstrap-classes.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use strict'
2+
3+
/**
4+
* @fileoverview Discourage using Bootstrap layout-oriented CSS classes inside CSS selectors
5+
* @author Alexander Afanasyev
6+
*/
7+
var isCSSLocator = require('../find-css-locator')
8+
var bootstrapClasses = require('../bootstrap-layout-classes')
9+
var extractClassNames = require('../extract-class-names')
10+
11+
module.exports = {
12+
meta: {
13+
schema: []
14+
},
15+
16+
create: function (context) {
17+
return {
18+
'CallExpression': function (node) {
19+
if (node.arguments && node.arguments.length && node.arguments[0].hasOwnProperty('value')) {
20+
if (isCSSLocator(node)) {
21+
var extractedClassNames = extractClassNames(node.arguments[0].value)
22+
23+
for (var i = 0; i < bootstrapClasses.length; i++) {
24+
if (extractedClassNames.indexOf(bootstrapClasses[i]) >= 0) {
25+
context.report({
26+
node: node,
27+
message: 'Unexpected Bootstrap class "' + bootstrapClasses[i] + '" inside a CSS selector'
28+
})
29+
}
30+
}
31+
}
32+
}
33+
}
34+
}
35+
}
36+
}

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,13 @@
4848
"chai": "^3.5.0",
4949
"commitizen": "^2.8.1",
5050
"coveralls": "^2.11.9",
51+
"css-selector-parser": "^1.1.0",
5152
"cz-conventional-changelog": "^1.1.6",
5253
"eslint": ">=2.0.0",
54+
"install": "^0.8.1",
5355
"istanbul": "^0.4.3",
5456
"mocha": "^2.5.1",
57+
"npm": "^3.10.5",
5558
"semantic-release": "^4.3.5",
5659
"standard": "^7.1.2"
5760
},

0 commit comments

Comments
 (0)