Skip to content

Commit 7029cb3

Browse files
committed
feat: Add prefer-equality-matcher rule
1 parent a8a2ea4 commit 7029cb3

File tree

5 files changed

+413
-0
lines changed

5 files changed

+413
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ CLI option\
146146
| [no-wait-for-selector](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-selector.md) | Disallow usage of `page.waitForSelector()` || | 💡 |
147147
| [no-wait-for-timeout](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-wait-for-timeout.md) | Disallow usage of `page.waitForTimeout()` || | 💡 |
148148
| [prefer-comparison-matcher](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-comparison-matcher.md) | Suggest using the built-in comparison matchers | | 🔧 | |
149+
| [prefer-equality-matcher](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-equality-matcher.md) | Suggest using the built-in equality matchers | | | 💡 |
149150
| [prefer-hooks-in-order](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-in-order.md) | Prefer having hooks in a consistent order | | | |
150151
| [prefer-hooks-on-top](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-hooks-on-top.md) | Suggest having hooks before any test cases | | | |
151152
| [prefer-strict-equal](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-strict-equal.md) | Suggest using `toStrictEqual()` | | | 💡 |

docs/rules/prefer-equality-matcher.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Suggest using the built-in equality matchers (`prefer-equality-matcher`)
2+
3+
Playwright has built-in matchers for expecting equality, which allow for more
4+
readable tests and error messages if an expectation fails.
5+
6+
## Rule details
7+
8+
This rule checks for _strict_ equality checks (`===` & `!==`) in tests that
9+
could be replaced with one of the following built-in equality matchers:
10+
11+
- `toBe`
12+
- `toEqual`
13+
- `toStrictEqual`
14+
15+
Examples of **incorrect** code for this rule:
16+
17+
```js
18+
expect(x === 5).toBe(true);
19+
expect(name === 'Carl').not.toEqual(true);
20+
expect(myObj !== thatObj).toStrictEqual(true);
21+
```
22+
23+
Examples of **correct** code for this rule:
24+
25+
```js
26+
expect(x).toBe(5);
27+
expect(name).not.toEqual('Carl');
28+
expect(myObj).toStrictEqual(thatObj);
29+
```

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import noUselessNot from './rules/no-useless-not';
2626
import noWaitForSelector from './rules/no-wait-for-selector';
2727
import noWaitForTimeout from './rules/no-wait-for-timeout';
2828
import preferComparisonMatcher from './rules/prefer-comparison-matcher';
29+
import preferEqualityMatcher from './rules/prefer-equality-matcher';
2930
import preferHooksInOrder from './rules/prefer-hooks-in-order';
3031
import preferHooksOnTop from './rules/prefer-hooks-on-top';
3132
import preferLowercaseTitle from './rules/prefer-lowercase-title';
@@ -70,6 +71,7 @@ const index = {
7071
'no-wait-for-selector': noWaitForSelector,
7172
'no-wait-for-timeout': noWaitForTimeout,
7273
'prefer-comparison-matcher': preferComparisonMatcher,
74+
'prefer-equality-matcher': preferEqualityMatcher,
7375
'prefer-hooks-in-order': preferHooksInOrder,
7476
'prefer-hooks-on-top': preferHooksOnTop,
7577
'prefer-lowercase-title': preferLowercaseTitle,

src/rules/prefer-equality-matcher.ts

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { Rule } from 'eslint';
2+
import {
3+
equalityMatchers,
4+
getParent,
5+
getRawValue,
6+
getStringValue,
7+
isBooleanLiteral,
8+
} from '../utils/ast';
9+
import { parseExpectCall } from '../utils/parseExpectCall';
10+
11+
export default {
12+
create(context) {
13+
return {
14+
CallExpression(node) {
15+
const expectCall = parseExpectCall(context, node);
16+
if (!expectCall || expectCall.args.length === 0) return;
17+
18+
const { args, matcher } = expectCall;
19+
const [comparison] = node.arguments;
20+
const expectCallEnd = node.range![1];
21+
const [matcherArg] = args;
22+
23+
if (
24+
comparison?.type !== 'BinaryExpression' ||
25+
(comparison.operator !== '===' && comparison.operator !== '!==') ||
26+
!equalityMatchers.has(getStringValue(matcher)) ||
27+
!isBooleanLiteral(matcherArg)
28+
) {
29+
return;
30+
}
31+
32+
const matcherValue = getRawValue(matcherArg) === 'true';
33+
const [modifier] = expectCall.modifiers;
34+
const hasNot = expectCall.modifiers.some(
35+
(node) => getStringValue(node) === 'not',
36+
);
37+
38+
// we need to negate the expectation if the current expected
39+
// value is itself negated by the "not" modifier
40+
const addNotModifier =
41+
(comparison.operator === '!==' ? !matcherValue : matcherValue) ===
42+
hasNot;
43+
44+
context.report({
45+
messageId: 'useEqualityMatcher',
46+
node: matcher,
47+
suggest: [...equalityMatchers.keys()].map((equalityMatcher) => ({
48+
data: { matcher: equalityMatcher },
49+
fix(fixer) {
50+
// preserve the existing modifier if it's not a negation
51+
let modifierText =
52+
modifier && getStringValue(modifier) !== 'not'
53+
? `.${getStringValue(modifier)}`
54+
: '';
55+
56+
if (addNotModifier) {
57+
modifierText += `.not`;
58+
}
59+
60+
return [
61+
// replace the comparison argument with the left-hand side of the comparison
62+
fixer.replaceText(
63+
comparison,
64+
context.sourceCode.getText(comparison.left),
65+
),
66+
// replace the current matcher & modifier with the preferred matcher
67+
fixer.replaceTextRange(
68+
[expectCallEnd, getParent(matcher)!.range![1]],
69+
`${modifierText}.${equalityMatcher}`,
70+
),
71+
// replace the matcher argument with the right-hand side of the comparison
72+
fixer.replaceText(
73+
matcherArg,
74+
context.sourceCode.getText(comparison.right),
75+
),
76+
];
77+
},
78+
messageId: 'suggestEqualityMatcher',
79+
})),
80+
});
81+
},
82+
};
83+
},
84+
meta: {
85+
docs: {
86+
category: 'Best Practices',
87+
description: 'Suggest using the built-in equality matchers',
88+
recommended: false,
89+
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-equality-matcher.md',
90+
},
91+
hasSuggestions: true,
92+
messages: {
93+
suggestEqualityMatcher: 'Use `{{ matcher }}`',
94+
useEqualityMatcher: 'Prefer using one of the equality matchers instead',
95+
},
96+
type: 'suggestion',
97+
},
98+
} as Rule.RuleModule;

0 commit comments

Comments
 (0)