Skip to content

Commit 89caf8c

Browse files
committed
feat: add no-global-regex-flag-in-query
1 parent 7bc2b9c commit 89caf8c

11 files changed

+221
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ To enable this configuration use the `extends` property in your
198198
| [`testing-library/no-container`](./docs/rules/no-container.md) | Disallow the use of `container` methods | | ![angular-badge][] ![react-badge][] ![vue-badge][] |
199199
| [`testing-library/no-debugging-utils`](./docs/rules/no-debugging-utils.md) | Disallow the use of debugging utilities like `debug` | | ![angular-badge][] ![react-badge][] ![vue-badge][] |
200200
| [`testing-library/no-dom-import`](./docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | 🔧 | ![angular-badge][] ![react-badge][] ![vue-badge][] |
201+
| [`testing-library/no-global-regexp-flag-in-query`](./docs/rules/no-global-regexp-flag-in-query.md) | Disallow the use of the global RegExp flag (/g) in queries | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] |
201202
| [`testing-library/no-manual-cleanup`](./docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
202203
| [`testing-library/no-node-access`](./docs/rules/no-node-access.md) | Disallow direct Node access | | ![angular-badge][] ![react-badge][] ![vue-badge][] |
203204
| [`testing-library/no-promise-in-fire-event`](./docs/rules/no-promise-in-fire-event.md) | Disallow the use of promises passed to a `fireEvent` method | | ![dom-badge][] ![angular-badge][] ![react-badge][] ![vue-badge][] |
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Disallow the use of the global RegExp flag (/g) in queries (`testing-library/no-global-regexp-flag-in-query`)
2+
3+
Ensure that there are no global RegExp flags used when using queries.
4+
5+
## Rule Details
6+
7+
A RegExp instance that's using the global flag `/g` holds state and this might cause false-positives while querying for elements.
8+
9+
Examples of **incorrect** code for this rule:
10+
11+
```js
12+
screen.getByText(/hello/gi);
13+
```
14+
15+
```js
16+
await screen.findByRole('button', { otherProp: true, name: /hello/g });
17+
```
18+
19+
Examples of **correct** code for this rule:
20+
21+
```js
22+
screen.getByText(/hello/i);
23+
```
24+
25+
```js
26+
await screen.findByRole('button', { otherProp: true, name: /hello/ });
27+
```
28+
29+
## Further Reading
30+
31+
- [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex)

lib/configs/angular.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export = {
1111
'testing-library/no-container': 'error',
1212
'testing-library/no-debugging-utils': 'error',
1313
'testing-library/no-dom-import': ['error', 'angular'],
14+
'testing-library/no-global-regexp-flag-in-query': 'error',
1415
'testing-library/no-node-access': 'error',
1516
'testing-library/no-promise-in-fire-event': 'error',
1617
'testing-library/no-render-in-setup': 'error',

lib/configs/dom.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export = {
88
'testing-library/await-async-query': 'error',
99
'testing-library/await-async-utils': 'error',
1010
'testing-library/no-await-sync-query': 'error',
11+
'testing-library/no-global-regexp-flag-in-query': 'error',
1112
'testing-library/no-promise-in-fire-event': 'error',
1213
'testing-library/no-wait-for-empty-callback': 'error',
1314
'testing-library/no-wait-for-multiple-assertions': 'error',

lib/configs/react.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export = {
1111
'testing-library/no-container': 'error',
1212
'testing-library/no-debugging-utils': 'error',
1313
'testing-library/no-dom-import': ['error', 'react'],
14+
'testing-library/no-global-regexp-flag-in-query': 'error',
1415
'testing-library/no-node-access': 'error',
1516
'testing-library/no-promise-in-fire-event': 'error',
1617
'testing-library/no-render-in-setup': 'error',

lib/configs/vue.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export = {
1212
'testing-library/no-container': 'error',
1313
'testing-library/no-debugging-utils': 'error',
1414
'testing-library/no-dom-import': ['error', 'vue'],
15+
'testing-library/no-global-regexp-flag-in-query': 'error',
1516
'testing-library/no-node-access': 'error',
1617
'testing-library/no-promise-in-fire-event': 'error',
1718
'testing-library/no-render-in-setup': 'error',

lib/node-utils/is-node-of-type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,4 @@ export const isReturnStatement = ASTUtils.isNodeOfType(
5959
export const isFunctionExpression = ASTUtils.isNodeOfType(
6060
AST_NODE_TYPES.FunctionExpression
6161
);
62+
export const isIdentifier = ASTUtils.isNodeOfType(AST_NODE_TYPES.Identifier);
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { TSESTree } from '@typescript-eslint/utils';
2+
3+
import { createTestingLibraryRule } from '../create-testing-library-rule';
4+
import {
5+
isMemberExpression,
6+
isIdentifier,
7+
isCallExpression,
8+
isProperty,
9+
isObjectExpression,
10+
} from '../node-utils';
11+
12+
export const RULE_NAME = 'no-global-regexp-flag-in-query';
13+
export type MessageIds = 'noGlobalRegExpFlagInQuery';
14+
type Options = [];
15+
16+
export default createTestingLibraryRule<Options, MessageIds>({
17+
name: RULE_NAME,
18+
meta: {
19+
type: 'suggestion',
20+
docs: {
21+
description: 'Disallow the use of the global RegExp flag (/g) in queries',
22+
recommendedConfig: {
23+
dom: 'error',
24+
angular: 'error',
25+
react: 'error',
26+
vue: 'error',
27+
},
28+
},
29+
messages: {
30+
noGlobalRegExpFlagInQuery:
31+
'Avoid using the global RegExp flag (/g) in queries',
32+
},
33+
schema: [],
34+
},
35+
defaultOptions: [],
36+
create(context, _, helpers) {
37+
function lint(
38+
regexpNode: TSESTree.Literal,
39+
identifier: TSESTree.Identifier
40+
) {
41+
if (helpers.isQuery(identifier)) {
42+
context.report({
43+
node: regexpNode,
44+
messageId: 'noGlobalRegExpFlagInQuery',
45+
});
46+
}
47+
}
48+
49+
return {
50+
[`CallExpression[callee.type=MemberExpression] > Literal[regex.flags=/g/].arguments`](
51+
node: TSESTree.Literal
52+
) {
53+
if (
54+
isCallExpression(node.parent) &&
55+
isMemberExpression(node.parent.callee) &&
56+
isIdentifier(node.parent.callee.property)
57+
) {
58+
lint(node, node.parent.callee.property);
59+
}
60+
},
61+
[`CallExpression[callee.type=Identifier] > Literal[regex.flags=/g/].arguments`](
62+
node: TSESTree.Literal
63+
) {
64+
if (isCallExpression(node.parent) && isIdentifier(node.parent.callee)) {
65+
lint(node, node.parent.callee);
66+
}
67+
},
68+
[`ObjectExpression:has(Property>[name="name"]) Literal[regex.flags=/g/]`](
69+
node: TSESTree.Literal
70+
) {
71+
if (
72+
isProperty(node.parent) &&
73+
isObjectExpression(node.parent.parent) &&
74+
isCallExpression(node.parent.parent.parent) &&
75+
isMemberExpression(node.parent.parent.parent.callee) &&
76+
isIdentifier(node.parent.parent.parent.callee.property)
77+
) {
78+
lint(node, node.parent.parent.parent.callee.property);
79+
}
80+
},
81+
};
82+
},
83+
});

tests/__snapshots__/index.test.ts.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Object {
1616
"error",
1717
"angular",
1818
],
19+
"testing-library/no-global-regexp-flag-in-query": "error",
1920
"testing-library/no-node-access": "error",
2021
"testing-library/no-promise-in-fire-event": "error",
2122
"testing-library/no-render-in-setup": "error",
@@ -38,6 +39,7 @@ Object {
3839
"testing-library/await-async-query": "error",
3940
"testing-library/await-async-utils": "error",
4041
"testing-library/no-await-sync-query": "error",
42+
"testing-library/no-global-regexp-flag-in-query": "error",
4143
"testing-library/no-promise-in-fire-event": "error",
4244
"testing-library/no-wait-for-empty-callback": "error",
4345
"testing-library/no-wait-for-multiple-assertions": "error",
@@ -63,6 +65,7 @@ Object {
6365
"error",
6466
"react",
6567
],
68+
"testing-library/no-global-regexp-flag-in-query": "error",
6669
"testing-library/no-node-access": "error",
6770
"testing-library/no-promise-in-fire-event": "error",
6871
"testing-library/no-render-in-setup": "error",
@@ -93,6 +96,7 @@ Object {
9396
"error",
9497
"vue",
9598
],
99+
"testing-library/no-global-regexp-flag-in-query": "error",
96100
"testing-library/no-node-access": "error",
97101
"testing-library/no-promise-in-fire-event": "error",
98102
"testing-library/no-render-in-setup": "error",

tests/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import plugin from '../lib';
88
const execAsync = util.promisify(exec);
99
const generateConfigs = () => execAsync(`npm run generate:configs`);
1010

11-
const numberOfRules = 26;
11+
const numberOfRules = 27;
1212
const ruleNames = Object.keys(plugin.rules);
1313

1414
// eslint-disable-next-line jest/expect-expect
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import rule, {
2+
RULE_NAME,
3+
} from '../../../lib/rules/no-global-regexp-flag-in-query';
4+
import { createRuleTester } from '../test-utils';
5+
6+
const ruleTester = createRuleTester();
7+
8+
ruleTester.run(RULE_NAME, rule, {
9+
valid: [
10+
`
11+
import { screen } from '@testing-library/dom'
12+
screen.getByText(/hello/)
13+
`,
14+
`
15+
import { screen } from '@testing-library/dom'
16+
screen.getByText(/hello/i)
17+
`,
18+
`
19+
import { screen } from '@testing-library/dom'
20+
screen.getByText('hello')
21+
`,
22+
23+
`
24+
import { screen } from '@testing-library/dom'
25+
screen.findByRole('button', {name: /hello/})
26+
`,
27+
`
28+
import { screen } from '@testing-library/dom'
29+
screen.findByRole('button', {name: /hello/i})
30+
`,
31+
`
32+
import { screen } from '@testing-library/dom'
33+
screen.findByRole('button', {name: 'hello'})
34+
`,
35+
`
36+
const utils = render(<Component/>)
37+
utils.findByRole('button', {name: /hello/i})
38+
`,
39+
`
40+
const {queryAllByPlaceholderText} = render(<Component/>)
41+
queryAllByPlaceholderText(/hello/i)
42+
`,
43+
],
44+
invalid: [
45+
{
46+
code: `
47+
import { screen } from '@testing-library/dom'
48+
screen.getByText(/hello/g)`,
49+
errors: [
50+
{
51+
messageId: 'noGlobalRegExpFlagInQuery',
52+
},
53+
],
54+
},
55+
{
56+
code: `
57+
import { screen } from '@testing-library/dom'
58+
screen.findByRole('button', {name: /hello/g})`,
59+
errors: [
60+
{
61+
messageId: 'noGlobalRegExpFlagInQuery',
62+
},
63+
],
64+
},
65+
{
66+
code: `
67+
import { screen } from '@testing-library/dom'
68+
screen.findByRole('button', {otherProp: true, name: /hello/g})`,
69+
errors: [
70+
{
71+
messageId: 'noGlobalRegExpFlagInQuery',
72+
},
73+
],
74+
},
75+
{
76+
code: `
77+
const utils = render(<Component/>)
78+
utils.findByRole('button', {name: /hello/ig})`,
79+
errors: [
80+
{
81+
messageId: 'noGlobalRegExpFlagInQuery',
82+
},
83+
],
84+
},
85+
{
86+
code: `
87+
const {queryAllByLabelText} = render(<Component/>)
88+
queryAllByLabelText(/hello/ig)`,
89+
errors: [
90+
{
91+
messageId: 'noGlobalRegExpFlagInQuery',
92+
},
93+
],
94+
},
95+
],
96+
});

0 commit comments

Comments
 (0)