Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ To enable this configuration use the `extends` property in your
| [`testing-library/no-container`](./docs/rules/no-container.md) | Disallow the use of `container` methods | | ![angular-badge][] ![react-badge][] ![vue-badge][] |
| [`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][] |
| [`testing-library/no-dom-import`](./docs/rules/no-dom-import.md) | Disallow importing from DOM Testing Library | 🔧 | ![angular-badge][] ![react-badge][] ![vue-badge][] |
| [`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][] |
| [`testing-library/no-manual-cleanup`](./docs/rules/no-manual-cleanup.md) | Disallow the use of `cleanup` | | |
| [`testing-library/no-node-access`](./docs/rules/no-node-access.md) | Disallow direct Node access | | ![angular-badge][] ![react-badge][] ![vue-badge][] |
| [`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][] |
Expand Down
31 changes: 31 additions & 0 deletions docs/rules/no-global-regexp-flag-in-query.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Disallow the use of the global RegExp flag (/g) in queries (`testing-library/no-global-regexp-flag-in-query`)

Ensure that there are no global RegExp flags used when using queries.

## Rule Details

A RegExp instance that's using the global flag `/g` holds state and this might cause false-positives while querying for elements.

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

```js
screen.getByText(/hello/gi);
```

```js
await screen.findByRole('button', { otherProp: true, name: /hello/g });
```

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

```js
screen.getByText(/hello/i);
```

```js
await screen.findByRole('button', { otherProp: true, name: /hello/ });
```

## Further Reading

- [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/lastIndex)
85 changes: 85 additions & 0 deletions lib/rules/no-global-regexp-flag-in-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { ASTUtils, TSESTree } from '@typescript-eslint/utils';

import { createTestingLibraryRule } from '../create-testing-library-rule';
import {
isMemberExpression,
isCallExpression,
isProperty,
isObjectExpression,
} from '../node-utils';

export const RULE_NAME = 'no-global-regexp-flag-in-query';
export type MessageIds = 'noGlobalRegExpFlagInQuery';
type Options = [];

export default createTestingLibraryRule<Options, MessageIds>({
name: RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: 'Disallow the use of the global RegExp flag (/g) in queries',
recommendedConfig: {
dom: false,
angular: false,
react: false,
vue: false,
},
},
messages: {
noGlobalRegExpFlagInQuery:
'Avoid using the global RegExp flag (/g) in queries',
},
schema: [],
},
defaultOptions: [],
create(context, _, helpers) {
function lint(
regexpNode: TSESTree.Literal,
identifier: TSESTree.Identifier
) {
if (helpers.isQuery(identifier)) {
context.report({
node: regexpNode,
messageId: 'noGlobalRegExpFlagInQuery',
});
}
}

return {
[`CallExpression[callee.type=MemberExpression] > Literal[regex.flags=/g/].arguments`](
node: TSESTree.Literal
) {
if (
isCallExpression(node.parent) &&
isMemberExpression(node.parent.callee) &&
ASTUtils.isIdentifier(node.parent.callee.property)
) {
lint(node, node.parent.callee.property);
}
},
[`CallExpression[callee.type=Identifier] > Literal[regex.flags=/g/].arguments`](
node: TSESTree.Literal
) {
if (
isCallExpression(node.parent) &&
ASTUtils.isIdentifier(node.parent.callee)
) {
lint(node, node.parent.callee);
}
},
[`ObjectExpression:has(Property>[name="name"]) Literal[regex.flags=/g/]`](
node: TSESTree.Literal
) {
if (
isProperty(node.parent) &&
isObjectExpression(node.parent.parent) &&
isCallExpression(node.parent.parent.parent) &&
isMemberExpression(node.parent.parent.parent.callee) &&
ASTUtils.isIdentifier(node.parent.parent.parent.callee.property)
) {
lint(node, node.parent.parent.parent.callee.property);
}
},
};
},
});
2 changes: 1 addition & 1 deletion tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import plugin from '../lib';
const execAsync = util.promisify(exec);
const generateConfigs = () => execAsync(`npm run generate:configs`);

const numberOfRules = 26;
const numberOfRules = 27;
const ruleNames = Object.keys(plugin.rules);

// eslint-disable-next-line jest/expect-expect
Expand Down
134 changes: 134 additions & 0 deletions tests/lib/rules/no-global-regexp-flag-in-query.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import rule, {
RULE_NAME,
} from '../../../lib/rules/no-global-regexp-flag-in-query';
import { createRuleTester } from '../test-utils';

const ruleTester = createRuleTester();

ruleTester.run(RULE_NAME, rule, {
valid: [
`
import { screen } from '@testing-library/dom'
screen.getByText(/hello/)
`,
`
import { screen } from '@testing-library/dom'
screen.getByText(/hello/i)
`,
`
import { screen } from '@testing-library/dom'
screen.getByText('hello')
`,

`
import { screen } from '@testing-library/dom'
screen.findByRole('button', {name: /hello/})
`,
`
import { screen } from '@testing-library/dom'
screen.findByRole('button', {name: /hello/i})
`,
`
import { screen } from '@testing-library/dom'
screen.findByRole('button', {name: 'hello'})
`,
`
const utils = render(<Component/>)
utils.findByRole('button', {name: /hello/i})
`,
`
const {queryAllByPlaceholderText} = render(<Component/>)
queryAllByPlaceholderText(/hello/i)
`,
`
const text = 'hello';
/hello/g.test(text)
text.match(/hello/g)
`,
`
const text = somethingElse()
/hello/g.test(text)
text.match(/hello/g)
`,
`
import somethingElse from 'somethingElse'
somethingElse.lookup(/hello/g)
`,
`
import { screen } from '@testing-library/dom'
screen.notAQuery(/hello/g)
`,
`
import { screen } from '@testing-library/dom'
screen.notAQuery('button', {name: /hello/g})
`,
`
const utils = render(<Component/>)
utils.notAQuery('button', {name: /hello/i})
`,
`
const utils = render(<Component/>)
utils.notAQuery(/hello/i)
`,
],
invalid: [
{
code: `
import { screen } from '@testing-library/dom'
screen.getByText(/hello/g)`,
errors: [
{
messageId: 'noGlobalRegExpFlagInQuery',
},
],
},
{
code: `
import { screen } from '@testing-library/dom'
screen.findByRole('button', {name: /hello/g})`,
errors: [
{
messageId: 'noGlobalRegExpFlagInQuery',
line: 3,
column: 46,
},
],
},
{
code: `
import { screen } from '@testing-library/dom'
screen.findByRole('button', {otherProp: true, name: /hello/g})`,
errors: [
{
messageId: 'noGlobalRegExpFlagInQuery',
line: 3,
column: 65,
},
],
},
{
code: `
const utils = render(<Component/>)
utils.findByRole('button', {name: /hello/ig})`,
errors: [
{
messageId: 'noGlobalRegExpFlagInQuery',
line: 3,
column: 47,
},
],
},
{
code: `
const {queryAllByLabelText} = render(<Component/>)
queryAllByLabelText(/hello/ig)`,
errors: [
{
messageId: 'noGlobalRegExpFlagInQuery',
line: 3,
column: 33,
},
],
},
],
});