Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
[![Tweet][tweet-badge]][tweet-url]

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->

[![All Contributors](https://img.shields.io/badge/all_contributors-29-orange.svg?style=flat-square)](#contributors-)

<!-- ALL-CONTRIBUTORS-BADGE:END -->

## Installation
Expand Down Expand Up @@ -141,6 +143,7 @@ To enable this configuration use the `extends` property in your
| [no-debug](docs/rules/no-debug.md) | Disallow the use of `debug` | ![angular-badge][] ![react-badge][] ![vue-badge][] | |
| [no-dom-import](docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
| [no-manual-cleanup](docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
| [no-render-in-setup](docs/rules/no-render-in-setup.md) | Disallow the use of `render` in setup functions | | |
| [no-wait-for-empty-callback](docs/rules/no-wait-for-empty-callback.md) | Disallow empty callbacks for `waitFor` and `waitForElementToBeRemoved` | | |
| [prefer-explicit-assert](docs/rules/prefer-explicit-assert.md) | Suggest using explicit assertions rather than just `getBy*` queries | | |
| [prefer-find-by](docs/rules/prefer-find-by.md) | Suggest using `findBy*` methods instead of the `waitFor` + `getBy` queries | ![recommended-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] | ![fixable-badge][] |
Expand Down Expand Up @@ -219,6 +222,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d

<!-- markdownlint-enable -->
<!-- prettier-ignore-end -->

<!-- ALL-CONTRIBUTORS-LIST:END -->

This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
42 changes: 42 additions & 0 deletions docs/rules/no-render-in-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Disallow the use of `render` in setup functions (no-render-in-setup)

## Rule Details

This rule disallows the usage of `render` in setup functions (`beforeEach` or `beforeAll`) in favor of a single test with multiple 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();
});

it('Should have baz', () => {
expect(screen.getByText('baz')).toBeInTheDocument();
});
```

Examples of **correct** code for this rule:

```js
it('Should have foo, bar and baz', () => {
render(<MyComponent />);
expect(screen.getByText('foo')).toBeInTheDocument();
expect(screen.getByText('bar')).toBeInTheDocument();
expect(screen.getByText('baz')).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"]}],
```
2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import noAwaitSyncQuery from './rules/no-await-sync-query';
import noDebug from './rules/no-debug';
import noDomImport from './rules/no-dom-import';
import noManualCleanup from './rules/no-manual-cleanup';
import noRenderInSetup from './rules/no-render-in-setup';
import noWaitForEmptyCallback from './rules/no-wait-for-empty-callback';
import preferExplicitAssert from './rules/prefer-explicit-assert';
import preferPresenceQueries from './rules/prefer-presence-queries';
Expand All @@ -22,6 +23,7 @@ const rules = {
'no-debug': noDebug,
'no-dom-import': noDomImport,
'no-manual-cleanup': noManualCleanup,
'no-render-in-setup': noRenderInSetup,
'no-wait-for-empty-callback': noWaitForEmptyCallback,
'prefer-explicit-assert': preferExplicitAssert,
'prefer-find-by': preferFindBy,
Expand Down
9 changes: 9 additions & 0 deletions lib/node-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ export function isVariableDeclarator(
return node && node.type === AST_NODE_TYPES.VariableDeclarator;
}

export function isRenderFunction(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this one was already moved to node-utils.ts but that was done in v4 of course 😓

callNode: TSESTree.CallExpression,
renderFunctions: string[]
) {
return ['render', ...renderFunctions].some(
name => isIdentifier(callNode.callee) && name === callNode.callee.name
);
}

export function isObjectPattern(
node: TSESTree.Node
): node is TSESTree.ObjectPattern {
Expand Down
10 changes: 1 addition & 9 deletions lib/rules/no-debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,11 @@ import {
isAwaitExpression,
isMemberExpression,
isImportSpecifier,
isRenderFunction,
} from '../node-utils';

export const RULE_NAME = 'no-debug';

function isRenderFunction(
callNode: TSESTree.CallExpression,
renderFunctions: string[]
) {
return ['render', ...renderFunctions].some(
name => isIdentifier(callNode.callee) && name === callNode.callee.name
);
}

function isRenderVariableDeclarator(
node: TSESTree.VariableDeclarator,
renderFunctions: string[]
Expand Down
3 changes: 1 addition & 2 deletions lib/rules/no-manual-cleanup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
meta: {
type: 'problem',
docs: {
description: ' Disallow the use of `cleanup`',
description: 'Disallow the use of `cleanup`',
category: 'Best Practices',
recommended: false,
},
Expand Down Expand Up @@ -121,7 +121,6 @@ export default ESLintUtils.RuleCreator(getDocsUrl)<Options, MessageIds>({
messageId: 'noManualCleanup',
});
}

} else {
defaultRequireFromTestingLibrary = declaratorNode.id;
}
Expand Down
74 changes: 74 additions & 0 deletions lib/rules/no-render-in-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ESLintUtils, TSESTree } from '@typescript-eslint/experimental-utils';
import { getDocsUrl, BEFORE_HOOKS } from '../utils';
import {
isIdentifier,
isCallExpression,
isRenderFunction,
} from '../node-utils';

export const RULE_NAME = 'no-render-in-setup';
export type MessageIds = 'noRenderInSetup';

export function findClosestBeforeHook(
node: TSESTree.Node
): TSESTree.Identifier | null {
if (node === null) return null;
if (
isCallExpression(node) &&
isIdentifier(node.callee) &&
BEFORE_HOOKS.includes(node.callee.name)
) {
return node.callee;
}

return findClosestBeforeHook(node.parent);
}

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:
'Combine assertions into a single test instead of re-rendering the component.',
},
fixable: null,
schema: [
{
type: 'object',
properties: {
renderFunctions: {
type: 'array',
},
},
},
],
},
defaultOptions: [
{
renderFunctions: [],
},
],

create(context, [{ renderFunctions }]) {
return {
CallExpression(node) {
const beforeHook = findClosestBeforeHook(node);
if (isRenderFunction(node, renderFunctions) && beforeHook) {
context.report({
node,
messageId: 'noRenderInSetup',
data: {
name: beforeHook.name,
},
});
}
},
};
},
});
3 changes: 3 additions & 0 deletions lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ const ASYNC_UTILS = [
'waitForDomChange',
];

const BEFORE_HOOKS = ['beforeEach', 'beforeAll'];

export {
getDocsUrl,
SYNC_QUERIES_VARIANTS,
Expand All @@ -73,5 +75,6 @@ export {
ASYNC_QUERIES_COMBINATIONS,
ALL_QUERIES_COMBINATIONS,
ASYNC_UTILS,
BEFORE_HOOKS,
LIBRARY_MODULES,
};
82 changes: 82 additions & 0 deletions tests/lib/rules/no-render-in-setup.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { createRuleTester } from '../test-utils';
import { BEFORE_HOOKS } from '../../../lib/utils';
import rule, { RULE_NAME } from '../../../lib/rules/no-render-in-setup';

const ruleTester = createRuleTester({
ecmaFeatures: {
jsx: true,
},
});

ruleTester.run(RULE_NAME, rule, {
valid: [
{
code: `
it('Test', () => {
render(<Component/>)
})
`,
},
],

invalid: [
...BEFORE_HOOKS.map(beforeHook => ({
code: `
${beforeHook}(() => {
render(<Component/>)
})
`,
errors: [
{
messageId: 'noRenderInSetup',
},
],
})),
...BEFORE_HOOKS.map(beforeHook => ({
code: `
${beforeHook}(function() {
render(<Component/>)
})
`,
errors: [
{
messageId: 'noRenderInSetup',
},
],
})),
// custom render function
...BEFORE_HOOKS.map(beforeHook => ({
code: `
${beforeHook}(() => {
renderWithRedux(<Component/>)
})
`,
options: [
{
renderFunctions: ['renderWithRedux'],
},
],
errors: [
{
messageId: 'noRenderInSetup',
},
],
})),
// call render within a wrapper function
...BEFORE_HOOKS.map(beforeHook => ({
code: `
${beforeHook}(() => {
const wrapper = () => {
render(<Component/>)
}
wrapper();
})
`,
errors: [
{
messageId: 'noRenderInSetup',
},
],
})),
],
});