Skip to content

Commit f69316a

Browse files
feat: Add require-to-pass-timeout rule (playwright-community#345)
1 parent c269371 commit f69316a

File tree

5 files changed

+121
-0
lines changed

5 files changed

+121
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ CLI option\
161161
| [prefer-web-first-assertions](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/prefer-web-first-assertions.md) | Suggest using web first assertions || 🔧 | |
162162
| [require-hook](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-hook.md) | Require setup and teardown code to be within a hook | | | |
163163
| [require-soft-assertions](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-soft-assertions.md) | Require assertions to use `expect.soft()` | | 🔧 | |
164+
| [require-to-pass-timeout](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-to-pass-timeout.md) | Require a timeout for `toPass()` | | | |
164165
| [require-to-throw-message](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-to-throw-message.md) | Require a message for `toThrow()` | | | |
165166
| [require-top-level-describe](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-top-level-describe.md) | Require test cases and hooks to be inside a `test.describe` block | | | |
166167
| [valid-describe-callback](https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/valid-describe-callback.md) | Enforce valid `describe()` callback || | |

docs/rules/require-to-pass-timeout.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Require a timeout for `toPass()` (`require-to-pass-timeout`)
2+
3+
`toPass()` is used to retry blocks of code until they pass successfully, such as
4+
in `await expect(async () => { ... }).toPass()`. However, if no timeout is
5+
defined, the test may run indefinitely until the test timeout. Requiring a
6+
timeout ensures that the test will fail early if it does not pass within the
7+
specified time.
8+
9+
## Rule details
10+
11+
This rule triggers a warning if `toPass()` is used without a timeout.
12+
13+
The following patterns are considered warnings:
14+
15+
```js
16+
await expect(async () => {
17+
const response = await page.request.get('https://api.example.com')
18+
expect(response.status()).toBe(200)
19+
}).toPass()
20+
```
21+
22+
The following patterns are not considered warnings:
23+
24+
```js
25+
await expect(async () => {
26+
const response = await page.request.get('https://api.example.com')
27+
expect(response.status()).toBe(200)
28+
}).toPass({
29+
timeout: 60000,
30+
})
31+
```

src/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import preferToHaveLength from './rules/prefer-to-have-length.js'
4242
import preferWebFirstAssertions from './rules/prefer-web-first-assertions.js'
4343
import requireHook from './rules/require-hook.js'
4444
import requireSoftAssertions from './rules/require-soft-assertions.js'
45+
import requireToPassTimeout from './rules/require-to-pass-timeout.js'
4546
import requireToThrowMessage from './rules/require-to-throw-message.js'
4647
import requireTopLevelDescribe from './rules/require-top-level-describe.js'
4748
import validDescribeCallback from './rules/valid-describe-callback.js'
@@ -95,6 +96,7 @@ const index = {
9596
'prefer-web-first-assertions': preferWebFirstAssertions,
9697
'require-hook': requireHook,
9798
'require-soft-assertions': requireSoftAssertions,
99+
'require-to-pass-timeout': requireToPassTimeout,
98100
'require-to-throw-message': requireToThrowMessage,
99101
'require-top-level-describe': requireTopLevelDescribe,
100102
'valid-describe-callback': validDescribeCallback,
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { runRuleTester } from '../utils/rule-tester.js'
2+
import rule from './require-to-pass-timeout.js'
3+
4+
runRuleTester('require-to-pass-timeout', rule, {
5+
invalid: [
6+
// toPass without timeout
7+
{
8+
code: "await expect(async () => { const response = await page.request.get('https://api.example.com'); expect(response.status()).toBe(200); }).toPass();",
9+
errors: [
10+
{
11+
column: 136,
12+
line: 1,
13+
messageId: 'addTimeoutOption',
14+
},
15+
],
16+
},
17+
// toPass with empty object
18+
{
19+
code: "await expect(async () => { const response = await page.request.get('https://api.example.com'); expect(response.status()).toBe(200); }).toPass({});",
20+
errors: [
21+
{
22+
column: 136,
23+
line: 1,
24+
messageId: 'addTimeoutOption',
25+
},
26+
],
27+
},
28+
],
29+
valid: [
30+
// toPass with timeout
31+
{
32+
code: "await expect(async () => { const response = await page.request.get('https://api.example.com'); expect(response.status()).toBe(200); }).toPass({ timeout: 60000 });",
33+
},
34+
// toPass with other options including timeout
35+
{
36+
code: "await expect(async () => { const response = await page.request.get('https://api.example.com'); expect(response.status()).toBe(200); }).toPass({ intervals: [1000, 2000], timeout: 60000 });",
37+
},
38+
],
39+
})

src/rules/require-to-pass-timeout.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { createRule } from '../utils/createRule.js'
2+
import { parseFnCall } from '../utils/parseFnCall.js'
3+
4+
export default createRule({
5+
create(context) {
6+
return {
7+
CallExpression(node) {
8+
const call = parseFnCall(context, node)
9+
if (call?.type !== 'expect') return
10+
11+
if (call.matcherName !== 'toPass') return
12+
13+
const objectArg = call.matcherArgs.find(
14+
(arg) => arg.type === 'ObjectExpression',
15+
)
16+
17+
if (
18+
!objectArg?.properties.find(
19+
(prop) =>
20+
prop.type === 'Property' &&
21+
prop.key.type === 'Identifier' &&
22+
prop.key.name === 'timeout',
23+
)
24+
) {
25+
// Look for `toPass` calls without `timeout` in the arguments.
26+
context.report({
27+
data: { matcherName: call.matcherName },
28+
messageId: 'addTimeoutOption',
29+
node: call.matcher,
30+
})
31+
}
32+
},
33+
}
34+
},
35+
meta: {
36+
docs: {
37+
category: 'Best Practices',
38+
description: 'Require a timeout option for `toPass()`',
39+
recommended: false,
40+
url: 'https://github.com/playwright-community/eslint-plugin-playwright/tree/main/docs/rules/require-to-pass-timeout.md',
41+
},
42+
messages: {
43+
addTimeoutOption: 'Add a timeout option to {{ matcherName }}()',
44+
},
45+
schema: [],
46+
type: 'suggestion',
47+
},
48+
})

0 commit comments

Comments
 (0)