-
Notifications
You must be signed in to change notification settings - Fork 144
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: new no-render-in-setup rule (#209)
* feat(no-render-in-setup): adds no-render-in-setup rule, closes #207 * refactor: address review comments * Update docs/rules/no-render-in-setup.md Co-authored-by: Tim Deschryver <[email protected]> * fix: updates docs, adds tests * fix: handle require, add tests * fix: code coverage Co-authored-by: Tim Deschryver <[email protected]>
- Loading branch information
1 parent
cbdfd5f
commit 5f35316
Showing
9 changed files
with
464 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Disallow the use of `render` in setup functions (no-render-in-setup) | ||
|
||
## Rule Details | ||
|
||
This rule disallows the usage of `render` (or a custom render function) in setup functions (`beforeEach` and `beforeAll`) in favor of moving `render` closer to test assertions. | ||
|
||
Examples of **incorrect** code for this rule: | ||
|
||
```js | ||
beforeEach(() => { | ||
render(<MyComponent />); | ||
}); | ||
|
||
it('Should have foo', () => { | ||
expect(screen.getByText('foo')).toBeInTheDocument(); | ||
}); | ||
|
||
it('Should have bar', () => { | ||
expect(screen.getByText('bar')).toBeInTheDocument(); | ||
}); | ||
``` | ||
|
||
```js | ||
beforeAll(() => { | ||
render(<MyComponent />); | ||
}); | ||
|
||
it('Should have foo', () => { | ||
expect(screen.getByText('foo')).toBeInTheDocument(); | ||
}); | ||
|
||
it('Should have bar', () => { | ||
expect(screen.getByText('bar')).toBeInTheDocument(); | ||
}); | ||
``` | ||
|
||
Examples of **correct** code for this rule: | ||
|
||
```js | ||
it('Should have foo and bar', () => { | ||
render(<MyComponent />); | ||
expect(screen.getByText('foo')).toBeInTheDocument(); | ||
expect(screen.getByText('bar')).toBeInTheDocument(); | ||
}); | ||
``` | ||
|
||
If you use [custom render functions](https://testing-library.com/docs/example-react-redux) then you can set a config option in your `.eslintrc` to look for these. | ||
|
||
``` | ||
"testing-library/no-render-in-setup": ["error", {"renderFunctions": ["renderWithRedux", "renderWithRouter"]}], | ||
``` | ||
|
||
If you would like to allow the use of `render` (or a custom render function) in _either_ `beforeAll` or `beforeEach`, this can be configured using the option `allowTestingFrameworkSetupHook`. This may be useful if you have configured your tests to [skip auto cleanup](https://testing-library.com/docs/react-testing-library/setup#skipping-auto-cleanup). `allowTestingFrameworkSetupHook` is an enum that accepts either `"beforeAll"` or `"beforeEach"`. | ||
|
||
``` | ||
"testing-library/no-render-in-setup": ["error", {"allowTestingFrameworkSetupHook": "beforeAll"}], | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils'; | ||
import { getDocsUrl, TESTING_FRAMEWORK_SETUP_HOOKS } from '../utils'; | ||
import { | ||
isLiteral, | ||
isProperty, | ||
isIdentifier, | ||
isObjectPattern, | ||
isCallExpression, | ||
isRenderFunction, | ||
isImportSpecifier, | ||
} from '../node-utils'; | ||
|
||
export const RULE_NAME = 'no-render-in-setup'; | ||
export type MessageIds = 'noRenderInSetup'; | ||
|
||
export function findClosestBeforeHook( | ||
node: TSESTree.Node, | ||
testingFrameworkSetupHooksToFilter: string[] | ||
): TSESTree.Identifier | null { | ||
if (node === null) return null; | ||
if ( | ||
isCallExpression(node) && | ||
isIdentifier(node.callee) && | ||
testingFrameworkSetupHooksToFilter.includes(node.callee.name) | ||
) { | ||
return node.callee; | ||
} | ||
|
||
return findClosestBeforeHook(node.parent, testingFrameworkSetupHooksToFilter); | ||
} | ||
|
||
export default ESLintUtils.RuleCreator(getDocsUrl)({ | ||
name: RULE_NAME, | ||
meta: { | ||
type: 'problem', | ||
docs: { | ||
description: 'Disallow the use of `render` in setup functions', | ||
category: 'Best Practices', | ||
recommended: false, | ||
}, | ||
messages: { | ||
noRenderInSetup: | ||
'Move `render` out of `{{name}}` and into individual tests.', | ||
}, | ||
fixable: null, | ||
schema: [ | ||
{ | ||
type: 'object', | ||
properties: { | ||
renderFunctions: { | ||
type: 'array', | ||
}, | ||
allowTestingFrameworkSetupHook: { | ||
enum: TESTING_FRAMEWORK_SETUP_HOOKS, | ||
}, | ||
}, | ||
anyOf: [ | ||
{ | ||
required: ['renderFunctions'], | ||
}, | ||
{ | ||
required: ['allowTestingFrameworkSetupHook'], | ||
}, | ||
], | ||
}, | ||
], | ||
}, | ||
defaultOptions: [ | ||
{ | ||
renderFunctions: [], | ||
allowTestingFrameworkSetupHook: '', | ||
}, | ||
], | ||
|
||
create(context, [{ renderFunctions, allowTestingFrameworkSetupHook }]) { | ||
let renderImportedFromTestingLib = false; | ||
let wildcardImportName: string | null = null; | ||
|
||
return { | ||
// checks if import has shape: | ||
// import * as dtl from '@testing-library/dom'; | ||
'ImportDeclaration[source.value=/testing-library/] ImportNamespaceSpecifier'( | ||
node: TSESTree.ImportNamespaceSpecifier | ||
) { | ||
wildcardImportName = node.local && node.local.name; | ||
}, | ||
// checks if `render` is imported from a '@testing-library/foo' | ||
'ImportDeclaration[source.value=/testing-library/]'( | ||
node: TSESTree.ImportDeclaration | ||
) { | ||
renderImportedFromTestingLib = node.specifiers.some(specifier => { | ||
return ( | ||
isImportSpecifier(specifier) && specifier.local.name === 'render' | ||
); | ||
}); | ||
}, | ||
[`VariableDeclarator > CallExpression > Identifier[name="require"]`]( | ||
node: TSESTree.Identifier | ||
) { | ||
const { | ||
arguments: callExpressionArgs, | ||
} = node.parent as TSESTree.CallExpression; | ||
const testingLibImport = callExpressionArgs.find( | ||
args => | ||
isLiteral(args) && | ||
typeof args.value === 'string' && | ||
RegExp(/testing-library/, 'g').test(args.value) | ||
); | ||
if (!testingLibImport) { | ||
return; | ||
} | ||
const declaratorNode = node.parent | ||
.parent as TSESTree.VariableDeclarator; | ||
|
||
renderImportedFromTestingLib = | ||
isObjectPattern(declaratorNode.id) && | ||
declaratorNode.id.properties.some( | ||
property => | ||
isProperty(property) && | ||
isIdentifier(property.key) && | ||
property.key.name === 'render' | ||
); | ||
}, | ||
CallExpression(node) { | ||
let testingFrameworkSetupHooksToFilter = TESTING_FRAMEWORK_SETUP_HOOKS; | ||
if (allowTestingFrameworkSetupHook.length !== 0) { | ||
testingFrameworkSetupHooksToFilter = TESTING_FRAMEWORK_SETUP_HOOKS.filter( | ||
hook => hook !== allowTestingFrameworkSetupHook | ||
); | ||
} | ||
const beforeHook = findClosestBeforeHook( | ||
node, | ||
testingFrameworkSetupHooksToFilter | ||
); | ||
// if `render` is imported from a @testing-library/foo or | ||
// imported with a wildcard, add `render` to the list of | ||
// disallowed render functions | ||
const disallowedRenderFns = | ||
renderImportedFromTestingLib || wildcardImportName | ||
? ['render', ...renderFunctions] | ||
: renderFunctions; | ||
|
||
if (isRenderFunction(node, disallowedRenderFns) && beforeHook) { | ||
context.report({ | ||
node, | ||
messageId: 'noRenderInSetup', | ||
data: { | ||
name: beforeHook.name, | ||
}, | ||
}); | ||
} | ||
}, | ||
}; | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.