Repository: ember-cli/eslint-plugin-ember
Pull request #966 (2020-09-29 00:33:13)

Add new rule `no-test-support-import`
# Problem Statement

We recently had code that made it pass review that didn't see the test-support import in production code. Tests passed and on production this caused a runtime error. 

TODO
- [ ] Determine if this rule should be added to recommended
- [ ] If yes to the above, open issue to set this rule to recommended


Patch:
diff --git a/README.md b/README.md
index 0f952f82..55436bb2 100644
--- a/README.md
+++ b/README.md
@@ -190,6 +190,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
 | :white_check_mark: | [no-test-and-then](./docs/rules/no-test-and-then.md) | disallow usage of the `andThen` test wait helper |
 | :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 |
 | :white_check_mark: | [no-test-module-for](./docs/rules/no-test-module-for.md) | disallow usage of `moduleFor`, `moduleForComponent`, etc |
+|  | [no-test-support-import](./docs/rules/no-test-support-import.md) | disallow importing of "test-support" files in production code. |
 | :white_check_mark: | [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. |
 | :white_check_mark: | [prefer-ember-test-helpers](./docs/rules/prefer-ember-test-helpers.md) | enforce usage of `@ember/test-helpers` methods over native window methods |
 
diff --git a/lib/index.js b/lib/index.js
index 7e5014b1..eb76f9b5 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -56,6 +56,7 @@ module.exports = {
     'no-test-and-then': require('./rules/no-test-and-then'),
     'no-test-import-export': require('./rules/no-test-import-export'),
     'no-test-module-for': require('./rules/no-test-module-for'),
+    'no-test-support-import': require('./rules/no-test-support-import'),
     'no-test-this-render': require('./rules/no-test-this-render'),
     'no-unnecessary-index-route': require('./rules/no-unnecessary-index-route'),
     'no-unnecessary-route-path-option': require('./rules/no-unnecessary-route-path-option'),


Test patch:
diff --git a/docs/rules/no-test-support-import.md b/docs/rules/no-test-support-import.md
new file mode 100644
index 00000000..687aea98
--- /dev/null
+++ b/docs/rules/no-test-support-import.md
@@ -0,0 +1,53 @@
+# no-test-support-import
+
+No importing of test support files into non-test code..
+
+**TL;DR** Do not import from a file located in addon-test-support into non-test code. Doing so will result in production errors that are not capable of being caught in tests as require statements are available in tests but not on production builds.
+
+## Examples
+
+Examples of **incorrect** code for this rule:
+
+> app/routes/index.js
+
+```javascript
+import doSomething from '../test-support/some-other-test';
+
+import Route from '@ember/routing/route';
+import { action } from '@ember/object';
+
+export default class SomeRouteRoute extends Route {
+  // …
+  model() {
+    return doSomething();
+  }
+}
+```
+
+Examples of **correct** code for this rule:
+
+> tests/unit/foo-test.js
+
+```javascript
+import setupModule from '../test-support/setup-module';
+import { module, test } from 'qunit';
+
+module('Acceptance | module', setupModule());
+```
+
+> addon-test-support/setupApplication.js
+
+```javascript
+import setupModule from '../test-support/setup-module';
+
+export default function setupApplicationTest(hooks) {
+  setupModule(hooks);
+  // ...
+}
+```
+
+This is meant as an addition to the [no-test-import-export](no-test-import-export.md) rule as these files do represent test files but are located in addon-test-support rather than in `/tests/`.
+
+## Related Rules
+
+* [no-test-import-export](no-test-import-export.md)
diff --git a/lib/rules/no-test-support-import.js b/lib/rules/no-test-support-import.js
new file mode 100644
index 00000000..88bc36f3
--- /dev/null
+++ b/lib/rules/no-test-support-import.js
@@ -0,0 +1,56 @@
+/**
+ * @fileOverview no importing of test files and no exporting in test files
+ */
+
+'use strict';
+
+const ERROR_MESSAGE_NO_IMPORT =
+  'Do not import a file from test-support into production code, only into test files.';
+
+function hasTestSupportDirectory(importSource) {
+  return (
+    importSource.startsWith('test-support/') ||
+    importSource.endsWith('/test-support') ||
+    importSource.includes('/test-support/')
+  );
+}
+
+module.exports = {
+  ERROR_MESSAGE_NO_IMPORT,
+
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'disallow importing of "test-support" files in production code.',
+      category: 'Testing',
+      recommended: false,
+      url:
+        'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-test-support-import.md',
+    },
+    schema: [],
+  },
+
+  create: function create(context) {
+    const fileName = context.getFilename();
+
+    return {
+      ImportDeclaration(node) {
+        const importSource = node.source.value;
+
+        if (
+          hasTestSupportDirectory(importSource) &&
+          !(
+            fileName.includes('/tests/') ||
+            fileName.includes('/addon-test-support/') ||
+            fileName.includes('/test-support/')
+          )
+        ) {
+          context.report({
+            message: ERROR_MESSAGE_NO_IMPORT,
+            node,
+          });
+        }
+      },
+    };
+  },
+};
diff --git a/tests/lib/rules/no-test-support-import.js b/tests/lib/rules/no-test-support-import.js
new file mode 100644
index 00000000..3463a5dc
--- /dev/null
+++ b/tests/lib/rules/no-test-support-import.js
@@ -0,0 +1,97 @@
+const rule = require('../../../lib/rules/no-test-support-import');
+const RuleTester = require('eslint').RuleTester;
+
+const ruleTester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 6,
+    sourceType: 'module',
+  },
+});
+
+const { ERROR_MESSAGE_NO_IMPORT } = rule;
+
+ruleTester.run('no-test-support-import', rule, {
+  valid: [
+    {
+      filename: 'foo/tests/some-test-helper.js',
+      code: `
+      import setupModule from './test-support/some-test-helper';
+      import { module, test } from 'qunit';
+
+      module('Acceptance | module', setupModule());
+    `,
+    },
+    {
+      filename: 'foo/test-support/some-test-helper.js',
+      code: `
+      import setupModule from './test-support/some-test-helper';
+      import { module, test } from 'qunit';
+
+      module('Acceptance | module', setupModule());
+    `,
+    },
+    {
+      filename: 'foo/addon-test-support/some-test-helper.js',
+      code: `
+      import setupModule from './test-support/some-test-helper';
+      import { module, test } from 'qunit';
+
+      module('Acceptance | module', setupModule());
+    `,
+    },
+  ],
+  invalid: [
+    {
+      code: `
+        import setupModule from './test-support/some-test-helper';
+      `,
+      filename: 'app/routes/index.js',
+      output: null,
+      errors: [
+        {
+          message: ERROR_MESSAGE_NO_IMPORT,
+          type: 'ImportDeclaration',
+        },
+      ],
+    },
+    {
+      code: `
+        import setupModule from 'foo/test-support/some-test-helper';
+      `,
+      filename: '@ember/foo/addon/components/index.js',
+      output: null,
+      errors: [
+        {
+          message: ERROR_MESSAGE_NO_IMPORT,
+          type: 'ImportDeclaration',
+        },
+      ],
+    },
+    {
+      code: `
+        import setupModule from 'foo/test-support/some-test-helper';
+      `,
+      filename: 'foo/test-support-foo/index.js',
+      output: null,
+      errors: [
+        {
+          message: ERROR_MESSAGE_NO_IMPORT,
+          type: 'ImportDeclaration',
+        },
+      ],
+    },
+    {
+      code: `
+        import setupModule from './test-support/some-test-helper';
+      `,
+      filename: 'app/components/index.js',
+      output: null,
+      errors: [
+        {
+          message: ERROR_MESSAGE_NO_IMPORT,
+          type: 'ImportDeclaration',
+        },
+      ],
+    },
+  ],
+});