diff --git a/README.md b/README.md index e4ac4b164..2bffe40c9 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,7 @@ installations requiring long-term consistency. | [no-commented-out-tests][] | Disallow commented out tests | | | | [no-duplicate-hooks][] | Disallow duplicate hooks within a `describe` block | | | | [no-empty-title][] | Disallow empty titles | | | +| [no-export][] | Disallow export from test files | | | | [no-focused-tests][] | Disallow focused tests | ![recommended][] | | | [no-hooks][] | Disallow setup and teardown hooks | | | | [no-identical-title][] | Disallow identical titles | ![recommended][] | | @@ -163,6 +164,7 @@ https://github.com/dangreenisrael/eslint-plugin-jest-formatting [no-duplicate-hooks]: docs/rules/no-duplicate-hooks.md [no-commented-out-tests]: docs/rules/no-commented-out-tests.md [no-empty-title]: docs/rules/no-empty-title.md +[no-export]: docs/rules/no-export.md [no-focused-tests]: docs/rules/no-focused-tests.md [no-hooks]: docs/rules/no-hooks.md [no-identical-title]: docs/rules/no-identical-title.md diff --git a/docs/rules/no-export.md b/docs/rules/no-export.md new file mode 100644 index 000000000..b31745a79 --- /dev/null +++ b/docs/rules/no-export.md @@ -0,0 +1,46 @@ +# no export from test file (no-export) + +Prevents exports from test files. If a file has at least 1 test in it, then this +rule will prevent exports. + +## Rule Details + +This rule aims to eliminate duplicate runs of tests by exporting things from +test files. If you import from a test file, then all the tests in that file will +be run in each imported instance, so bottom line, don't export from a test, but +instead move helper functions into a seperate file when they need to be shared +across tests. + +Examples of **incorrect** code for this rule: + +```js +export function myHelper() {} + +module.exports = function() {}; + +module.exports = { + something: 'that should be moved to a non-test file', +}; + +describe('a test', () => { + expect(1).toBe(1); +}); +``` + +Examples of **correct** code for this rule: + +```js +function myHelper() {} + +const myThing = { + something: 'that can live here', +}; + +describe('a test', () => { + expect(1).toBe(1); +}); +``` + +## When Not To Use It + +Don't use this rule on non-jest test files. diff --git a/src/__tests__/rules.test.js b/src/__tests__/rules.test.js index ef6a0fffd..b259c2c38 100644 --- a/src/__tests__/rules.test.js +++ b/src/__tests__/rules.test.js @@ -3,7 +3,7 @@ import { resolve } from 'path'; import { rules } from '../'; const ruleNames = Object.keys(rules); -const numberOfRules = 34; +const numberOfRules = 35; describe('rules', () => { it('should have a corresponding doc for each rule', () => { diff --git a/src/rules/__tests__/no-export.test.js b/src/rules/__tests__/no-export.test.js new file mode 100644 index 000000000..cc42dc71d --- /dev/null +++ b/src/rules/__tests__/no-export.test.js @@ -0,0 +1,50 @@ +import { RuleTester } from 'eslint'; +import rule from '../no-export'; + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2015, + sourceType: 'module', + }, +}); + +ruleTester.run('no-export', rule, { + valid: [ + 'describe("a test", () => { expect(1).toBe(1); })', + 'window.location = "valid"', + 'module.somethingElse = "foo";', + 'export const myThing = "valid"', + 'export default function () {}', + 'module.exports = function(){}', + 'module.exports.myThing = "valid";', + ], + invalid: [ + { + code: + 'export const myThing = "invalid"; test("a test", () => { expect(1).toBe(1);});', + parserOptions: { sourceType: 'module' }, + errors: [{ endColumn: 34, column: 1, messageId: 'unexpectedExport' }], + }, + { + code: + 'export default function() {}; test("a test", () => { expect(1).toBe(1);});', + parserOptions: { sourceType: 'module' }, + errors: [{ endColumn: 29, column: 1, messageId: 'unexpectedExport' }], + }, + { + code: + 'module.exports["invalid"] = function() {}; test("a test", () => { expect(1).toBe(1);});', + errors: [{ endColumn: 26, column: 1, messageId: 'unexpectedExport' }], + }, + { + code: + 'module.exports = function() {}; ; test("a test", () => { expect(1).toBe(1);});', + errors: [{ endColumn: 15, column: 1, messageId: 'unexpectedExport' }], + }, + { + code: + 'module.export.invalid = function() {}; ; test("a test", () => { expect(1).toBe(1);});', + errors: [{ endColumn: 22, column: 1, messageId: 'unexpectedExport' }], + }, + ], +}); diff --git a/src/rules/no-export.js b/src/rules/no-export.js new file mode 100644 index 000000000..763e8ed76 --- /dev/null +++ b/src/rules/no-export.js @@ -0,0 +1,47 @@ +import { getDocsUrl, isTestCase } from './util'; + +export default { + meta: { + docs: { + url: getDocsUrl(__filename), + }, + messages: { + unexpectedExport: `Do not export from a test file.`, + }, + schema: [], + }, + create(context) { + const exportNodes = []; + let hasTestCase = false; + + return { + 'Program:exit'() { + if (hasTestCase && exportNodes.length > 0) { + for (let node of exportNodes) { + context.report({ node, messageId: 'unexpectedExport' }); + } + } + }, + + CallExpression(node) { + if (isTestCase(node)) { + hasTestCase = true; + } + }, + 'ExportNamedDeclaration, ExportDefaultDeclaration'(node) { + exportNodes.push(node); + }, + 'AssignmentExpression > MemberExpression'(node) { + let { object, property } = node; + + if (object.type === 'MemberExpression') { + ({ object, property } = object); + } + + if (object.name === 'module' && /^exports?$/.test(property.name)) { + exportNodes.push(node); + } + }, + }; + }, +};