Skip to content

Commit e9b5069

Browse files
authored
Merge pull request #767 from fierysunset/prefer-ember-test-helpers
Add new rule `prefer-ember-test-helpers`
2 parents 0052492 + 1948e4b commit e9b5069

File tree

5 files changed

+438
-0
lines changed

5 files changed

+438
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
187187
| :white_check_mark: | [no-test-import-export](./docs/rules/no-test-import-export.md) | disallow importing of "-test.js" in a test file and exporting from a test file |
188188
| :white_check_mark: | [no-test-module-for](./docs/rules/no-test-module-for.md) | disallow usage of `moduleFor`, `moduleForComponent`, etc |
189189
| | [no-test-this-render](./docs/rules/no-test-this-render.md) | disallow usage of the `this.render` in tests, recommending to use @ember/test-helpers' `render` instead. |
190+
| | [prefer-ember-test-helpers](./docs/rules/prefer-ember-test-helpers.md) | enforce usage of `@ember/test-helpers` methods over native window methods |
190191

191192
<!--RULES_TABLE_END-->
192193

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# prefer-ember-test-helpers
2+
3+
This rule ensures the correct Ember test helper is imported when using methods that have a native window counterpart.
4+
5+
There are currently 3 Ember test helper methods that have a native window counterpart:
6+
7+
* blur
8+
* find
9+
* focus
10+
11+
If these methods are not properly imported from Ember's test-helpers suite, and the native window method version is used instead, any intended asynchronous functions won't work as intended, which can cause tests to fail silently.
12+
13+
## Examples
14+
15+
Examples of **incorrect** code for this rule:
16+
17+
```js
18+
test('foo', async (assert) => {
19+
await blur('.some-element');
20+
});
21+
```
22+
23+
```js
24+
test('foo', async (assert) => {
25+
await find('.some-element');
26+
});
27+
```
28+
29+
```js
30+
test('foo', async (assert) => {
31+
await focus('.some-element');
32+
});
33+
```
34+
35+
Examples of **correct** code for this rule:
36+
37+
```js
38+
import { blur } from '@ember/test-helpers';
39+
40+
test('foo', async (assert) => {
41+
await blur('.some-element');
42+
});
43+
```
44+
45+
```js
46+
import { find } from '@ember/test-helpers';
47+
48+
test('foo', async (assert) => {
49+
await find('.some-element');
50+
});
51+
```
52+
53+
```js
54+
import { focus } from '@ember/test-helpers';
55+
56+
test('foo', async (assert) => {
57+
await focus('.some-element');
58+
});
59+
```
60+
61+
## References
62+
63+
* [Web API Window Methods](https://developer.mozilla.org/en-US/docs/Web/API/Window#Methods)
64+
* [Ember Test Helpers API Methods](https://github.com/emberjs/ember-test-helpers/blob/master/API.md)

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ module.exports = {
6161
'order-in-controllers': require('./rules/order-in-controllers'),
6262
'order-in-models': require('./rules/order-in-models'),
6363
'order-in-routes': require('./rules/order-in-routes'),
64+
'prefer-ember-test-helpers': require('./rules/prefer-ember-test-helpers'),
6465
'require-computed-macros': require('./rules/require-computed-macros'),
6566
'require-computed-property-dependencies': require('./rules/require-computed-property-dependencies'),
6667
'require-return-from-computed': require('./rules/require-return-from-computed'),
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
'use strict';
2+
3+
const emberUtils = require('../utils/ember');
4+
5+
const getImportName = (node, namedImportIdentifier) => {
6+
return node.specifiers
7+
.filter((specifier) => {
8+
return (
9+
(specifier.type === 'ImportSpecifier' &&
10+
specifier.imported.name === namedImportIdentifier) ||
11+
(!namedImportIdentifier && specifier.type === 'ImportDefaultSpecifier')
12+
);
13+
})
14+
.map((specifier) => specifier.local.name)
15+
.pop();
16+
};
17+
18+
//-------------------------------------------------------------------------------------
19+
// Rule Definition
20+
//-------------------------------------------------------------------------------------
21+
22+
module.exports = {
23+
meta: {
24+
type: 'suggestion',
25+
docs: {
26+
description: 'enforce usage of `@ember/test-helpers` methods over native window methods',
27+
category: 'Testing',
28+
recommended: false,
29+
url:
30+
'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/prefer-ember-test-helpers.md',
31+
},
32+
schema: [],
33+
},
34+
35+
create: (context) => {
36+
if (!emberUtils.isTestFile(context.getFilename())) {
37+
return {};
38+
}
39+
40+
let hasBlurFunction = undefined;
41+
let hasFindFunction = undefined;
42+
let hasFocusFunction = undefined;
43+
44+
const markMethodsAsPresent = (fnName) => {
45+
if (fnName === 'blur') {
46+
hasBlurFunction = true;
47+
} else if (fnName === 'find') {
48+
hasFindFunction = true;
49+
} else if (fnName === 'focus') {
50+
hasFocusFunction = true;
51+
}
52+
};
53+
54+
const showErrorMessage = (node, methodName) => {
55+
context.report({
56+
data: { methodName },
57+
message: 'Import the `{{methodName}}()` method from @ember/test-helpers',
58+
node,
59+
});
60+
};
61+
62+
return {
63+
ImportDeclaration(node) {
64+
hasBlurFunction = getImportName(node, 'blur');
65+
hasFindFunction = getImportName(node, 'find');
66+
hasFocusFunction = getImportName(node, 'focus');
67+
},
68+
FunctionDeclaration(node) {
69+
const fnName = node.id.name;
70+
71+
markMethodsAsPresent(fnName);
72+
},
73+
FunctionExpression(node) {
74+
const nodeParent = node.parent;
75+
76+
if (nodeParent && nodeParent.type === 'VariableDeclarator') {
77+
const fnName = nodeParent.id.name;
78+
79+
markMethodsAsPresent(fnName);
80+
}
81+
},
82+
ArrowFunctionExpression(node) {
83+
const nodeParent = node.parent;
84+
85+
if (nodeParent && nodeParent.type === 'VariableDeclarator') {
86+
const fnName = nodeParent.id.name;
87+
88+
markMethodsAsPresent(fnName);
89+
}
90+
},
91+
CallExpression(node) {
92+
if (!hasBlurFunction) {
93+
if (node.callee.name === 'blur') {
94+
showErrorMessage(node, 'blur');
95+
}
96+
}
97+
98+
if (!hasFindFunction) {
99+
if (node.callee.name === 'find') {
100+
showErrorMessage(node, 'find');
101+
}
102+
}
103+
104+
if (!hasFocusFunction) {
105+
if (node.callee.name === 'focus') {
106+
showErrorMessage(node, 'focus');
107+
}
108+
}
109+
},
110+
};
111+
},
112+
};

0 commit comments

Comments
 (0)