Skip to content

Add no-restricted-matchers #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -59,6 +59,7 @@ command line option.\
| ✔ | | 💡 | [no-focused-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-focused-test.md) | Disallow usage of `.only` annotation |
| ✔ | | | [no-force-option](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-force-option.md) | Disallow usage of the `{ force: true }` option |
| ✔ | | | [no-page-pause](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-page-pause.md) | Disallow using `page.pause` |
| | | | [no-restricted-matchers](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md) | Disallow specific matchers & modifiers |
| ✔ | | 💡 | [no-skipped-test](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-skipped-test.md) | Disallow usage of the `.skip` annotation |
| ✔ | 🔧 | | [no-useless-not](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-useless-not.md) | Disallow usage of `not` matchers when a specific matcher exists |
| ✔ | | 💡 | [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` |
Expand Down
45 changes: 45 additions & 0 deletions docs/rules/no-restricted-matchers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Disallow specific matchers & modifiers (`no-restricted-matchers`)

This rule bans specific matchers & modifiers from being used, and can suggest
alternatives.

## Rule Details

Bans are expressed in the form of a map, with the value being either a string
message to be shown, or `null` if the default rule message should be used.

Both matchers, modifiers, and chains of the two are checked, allowing for
specific variations of a matcher to be banned if desired.

By default, this map is empty, meaning no matchers or modifiers are banned.

For example:

```json
{
"playwright/no-restricted-matchers": [
"error",
{
"toBeFalsy": "Use `toBe(false)` instead.",
"not": null,
"not.toHaveText": null
}
]
}
```

Examples of **incorrect** code for this rule with the above configuration

```javascript
test('is false', () => {
expect(a).toBeFalsy();
});

test('not', () => {
expect(a).not.toBe(true);
});

test('chain', async () => {
await expect(foo).not.toHaveText('bar');
});
```
10 changes: 6 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import noWaitForTimeout from './rules/no-wait-for-timeout';
import noForceOption from './rules/no-force-option';
import maxNestedDescribe from './rules/max-nested-describe';
import noConditionalInTest from './rules/no-conditional-in-test';
import noRestrictedMatchers from './rules/no-restricted-matchers';
import noUselessNot from './rules/no-useless-not';
import validExpect from './rules/valid-expect';
import preferLowercaseTitle from './rules/prefer-lowercase-title';
import requireTopLevelDescribe from './rules/require-top-level-describe';
import preferToHaveLength from './rules/prefer-to-have-length';
import requireTopLevelDescribe from './rules/require-top-level-describe';
import validExpect from './rules/valid-expect';

export = {
configs: {
Expand Down Expand Up @@ -82,9 +83,10 @@ export = {
'max-nested-describe': maxNestedDescribe,
'no-conditional-in-test': noConditionalInTest,
'no-useless-not': noUselessNot,
'valid-expect': validExpect,
'no-restricted-matchers': noRestrictedMatchers,
'prefer-lowercase-title': preferLowercaseTitle,
'require-top-level-describe': requireTopLevelDescribe,
'prefer-to-have-length': preferToHaveLength,
'require-top-level-describe': requireTopLevelDescribe,
'valid-expect': validExpect,
},
};
61 changes: 61 additions & 0 deletions src/rules/no-restricted-matchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Rule } from 'eslint';
import { getMatchers, getNodeName, isExpectCall } from '../utils/ast';

export default {
create(context) {
const restrictedChains = (context.options?.[0] ?? {}) as {
[key: string]: string | null;
};

return {
CallExpression(node) {
if (!isExpectCall(node)) {
return;
}

const matchers = getMatchers(node);
const permutations = matchers.map((_, i) => matchers.slice(0, i + 1));

for (const permutation of permutations) {
const chain = permutation.map(getNodeName).join('.');

if (chain in restrictedChains) {
const message = restrictedChains[chain];

context.report({
messageId: message ? 'restrictedWithMessage' : 'restricted',
data: { message: message ?? '', chain },
loc: {
start: permutation[0].loc!.start,
end: permutation[permutation.length - 1].loc!.end,
},
});

break;
}
}
},
};
},
meta: {
docs: {
category: 'Best Practices',
description: 'Disallow specific matchers & modifiers',
recommended: false,
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/no-restricted-matchers.md',
},
messages: {
restricted: 'Use of `{{chain}}` is disallowed',
restrictedWithMessage: '{{message}}',
},
type: 'suggestion',
schema: [
{
type: 'object',
additionalProperties: {
type: ['string', 'null'],
},
},
],
},
} as Rule.RuleModule;
149 changes: 149 additions & 0 deletions test/spec/no-restricted-matchers.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import rule from '../../src/rules/no-restricted-matchers';
import { runRuleTester } from '../utils/rule-tester';

runRuleTester('no-restricted-matchers', rule, {
valid: [
'expect(a)',
'expect(a).toBe()',
'expect(a).not.toContain()',
'expect(a).toHaveText()',
'expect(a).toThrow()',
'expect.soft(a)',
'expect.soft(a).toHaveText()',
'expect.poll(() => true).toThrow()',
{
code: 'expect(a).toBe(b)',
options: [{ 'not.toBe': null }],
},
{
code: 'expect(a).toBe(b)',
options: [{ 'not.toBe': null }],
},
{
code: 'expect.soft(a).toBe(b)',
options: [{ 'not.toBe': null }],
},
{
code: 'expect.poll(() => true).toBe(b)',
options: [{ 'not.toBe': null }],
},
],
invalid: [
{
code: 'expect(a).toBe(b)',
options: [{ toBe: null }],
errors: [
{
messageId: 'restricted',
data: { message: '', chain: 'toBe' },
column: 11,
line: 1,
},
],
},
{
code: 'expect.soft(a).toBe(b)',
options: [{ toBe: null }],
errors: [
{
messageId: 'restricted',
data: { message: '', chain: 'toBe' },
column: 16,
line: 1,
},
],
},
{
code: 'expect.poll(() => a).toBe(b)',
options: [{ toBe: null }],
errors: [
{
messageId: 'restricted',
data: { message: '', chain: 'toBe' },
column: 22,
line: 1,
},
],
},
{
code: 'expect(a).not.toBe()',
options: [{ not: null }],
errors: [
{
messageId: 'restricted',
data: { message: '', chain: 'not' },
column: 11,
line: 1,
},
],
},
{
code: 'expect(a).not.toBeTruthy()',
options: [{ 'not.toBeTruthy': null }],
errors: [
{
messageId: 'restricted',
data: { message: '', chain: 'not.toBeTruthy' },
endColumn: 25,
column: 11,
line: 1,
},
],
},
{
code: 'expect.soft(a).not.toBe()',
options: [{ not: null }],
errors: [
{
messageId: 'restricted',
data: { message: '', chain: 'not' },
column: 16,
line: 1,
},
],
},
{
code: 'expect.poll(() => true).not.toBeTruthy()',
options: [{ 'not.toBeTruthy': null }],
errors: [
{
messageId: 'restricted',
data: { message: '', chain: 'not.toBeTruthy' },
endColumn: 39,
column: 25,
line: 1,
},
],
},
{
code: 'expect(a).toBe(b)',
options: [{ toBe: 'Prefer `toStrictEqual` instead' }],
errors: [
{
messageId: 'restrictedWithMessage',
data: {
message: 'Prefer `toStrictEqual` instead',
chain: 'toBe',
},
column: 11,
line: 1,
},
],
},
{
code: "expect(foo).not.toHaveText('bar')",
options: [{ 'not.toHaveText': 'Use not.toContainText instead' }],
errors: [
{
messageId: 'restrictedWithMessage',
data: {
message: 'Use not.toContainText instead',
chain: 'not.toHaveText',
},
endColumn: 27,
column: 13,
},
],
},
],
});