Skip to content

Commit 059f41a

Browse files
authored
Support non-awaited expressions in prefer-web-first-assertions (#378)
* Support non-awaited expressions in `prefer-web-first-assertions` * Sync
1 parent 41297dc commit 059f41a

File tree

2 files changed

+181
-26
lines changed

2 files changed

+181
-26
lines changed

src/rules/prefer-web-first-assertions.test.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@ runRuleTester('prefer-web-first-assertions', rule, {
1717
],
1818
output: test('await expect(page.locator(".tweet")).toBeVisible()'),
1919
},
20+
{
21+
code: test('expect(page.locator(".tweet").isVisible()).toBe(true)'),
22+
errors: [
23+
{
24+
column: 28,
25+
data: { matcher: 'toBeVisible', method: 'isVisible' },
26+
endColumn: 70,
27+
line: 1,
28+
messageId: 'useWebFirstAssertion',
29+
},
30+
],
31+
output: test('await expect(page.locator(".tweet")).toBeVisible()'),
32+
},
2033
{
2134
code: test(
2235
'expect(await page.locator(".tweet").isVisible()).toBe(false)',
@@ -176,6 +189,19 @@ runRuleTester('prefer-web-first-assertions', rule, {
176189
],
177190
output: test('await expect(page.locator(".tweet")).toBeVisible()'),
178191
},
192+
{
193+
code: test('expect(page.locator(".button").isVisible()).toBe(false)'),
194+
errors: [
195+
{
196+
column: 28,
197+
data: { matcher: 'toBeHidden', method: 'isVisible' },
198+
endColumn: 72,
199+
line: 1,
200+
messageId: 'useWebFirstAssertion',
201+
},
202+
],
203+
output: test('await expect(page.locator(".button")).toBeHidden()'),
204+
},
179205

180206
// isHidden
181207
{
@@ -230,6 +256,19 @@ runRuleTester('prefer-web-first-assertions', rule, {
230256
],
231257
output: test('await expect(foo).toBeHidden()'),
232258
},
259+
{
260+
code: test('expect(page.locator(".link").isHidden()).toBe(true)'),
261+
errors: [
262+
{
263+
column: 28,
264+
data: { matcher: 'toBeHidden', method: 'isHidden' },
265+
endColumn: 69,
266+
line: 1,
267+
messageId: 'useWebFirstAssertion',
268+
},
269+
],
270+
output: test('await expect(page.locator(".link")).toBeHidden()'),
271+
},
233272

234273
// getAttribute
235274
{
@@ -300,6 +339,23 @@ runRuleTester('prefer-web-first-assertions', rule, {
300339
'await expect.soft(page.locator("foo")).not.toHaveAttribute("aria-label", "bar")',
301340
),
302341
},
342+
{
343+
code: test(
344+
'expect(page.locator(".element").getAttribute("data-testid")).toBe("submit")',
345+
),
346+
errors: [
347+
{
348+
column: 28,
349+
data: { matcher: 'toHaveAttribute', method: 'getAttribute' },
350+
endColumn: 85,
351+
line: 1,
352+
messageId: 'useWebFirstAssertion',
353+
},
354+
],
355+
output: test(
356+
'await expect(page.locator(".element")).toHaveAttribute("data-testid", "submit")',
357+
),
358+
},
303359

304360
// innerText
305361
{
@@ -328,6 +384,23 @@ runRuleTester('prefer-web-first-assertions', rule, {
328384
],
329385
output: test('await expect.soft(foo).not.toHaveText("bar")'),
330386
},
387+
{
388+
code: test(
389+
'expect(page.locator(".text").innerText()).toBe("Hello World")',
390+
),
391+
errors: [
392+
{
393+
column: 28,
394+
data: { matcher: 'toHaveText', method: 'innerText' },
395+
endColumn: 71,
396+
line: 1,
397+
messageId: 'useWebFirstAssertion',
398+
},
399+
],
400+
output: test(
401+
'await expect(page.locator(".text")).toHaveText("Hello World")',
402+
),
403+
},
331404

332405
// inputValue
333406
{
@@ -356,6 +429,23 @@ runRuleTester('prefer-web-first-assertions', rule, {
356429
],
357430
output: test('await expect[`soft`](foo).not.toHaveValue("bar")'),
358431
},
432+
{
433+
code: test(
434+
'expect(page.locator(".input").inputValue()).toBe("user input")',
435+
),
436+
errors: [
437+
{
438+
column: 28,
439+
data: { matcher: 'toHaveValue', method: 'inputValue' },
440+
endColumn: 71,
441+
line: 1,
442+
messageId: 'useWebFirstAssertion',
443+
},
444+
],
445+
output: test(
446+
'await expect(page.locator(".input")).toHaveValue("user input")',
447+
),
448+
},
359449

360450
// textContent
361451
{
@@ -528,6 +618,23 @@ runRuleTester('prefer-web-first-assertions', rule, {
528618
await expect(fooLocatorText).toHaveText('foo');
529619
`),
530620
},
621+
{
622+
code: test(
623+
'expect(page.locator(".content").textContent()).toBe("Some content")',
624+
),
625+
errors: [
626+
{
627+
column: 28,
628+
data: { matcher: 'toHaveText', method: 'textContent' },
629+
endColumn: 75,
630+
line: 1,
631+
messageId: 'useWebFirstAssertion',
632+
},
633+
],
634+
output: test(
635+
'await expect(page.locator(".content")).toHaveText("Some content")',
636+
),
637+
},
531638

532639
// isChecked
533640
{
@@ -646,6 +753,19 @@ runRuleTester('prefer-web-first-assertions', rule, {
646753
],
647754
output: test('await expect(page.locator("howdy")).toBeChecked()'),
648755
},
756+
{
757+
code: test('expect(page.locator(".checkbox").isChecked()).toBe(true)'),
758+
errors: [
759+
{
760+
column: 28,
761+
data: { matcher: 'toBeChecked', method: 'isChecked' },
762+
endColumn: 72,
763+
line: 1,
764+
messageId: 'useWebFirstAssertion',
765+
},
766+
],
767+
output: test('await expect(page.locator(".checkbox")).toBeChecked()'),
768+
},
649769

650770
// isDisabled
651771
{
@@ -700,6 +820,19 @@ runRuleTester('prefer-web-first-assertions', rule, {
700820
],
701821
output: test('await expect(foo).toBeDisabled()'),
702822
},
823+
{
824+
code: test('expect(page.locator(".input").isDisabled()).toBe(true)'),
825+
errors: [
826+
{
827+
column: 28,
828+
data: { matcher: 'toBeDisabled', method: 'isDisabled' },
829+
endColumn: 70,
830+
line: 1,
831+
messageId: 'useWebFirstAssertion',
832+
},
833+
],
834+
output: test('await expect(page.locator(".input")).toBeDisabled()'),
835+
},
703836

704837
// isEnabled
705838
{
@@ -754,6 +887,19 @@ runRuleTester('prefer-web-first-assertions', rule, {
754887
],
755888
output: test('await expect(foo).toBeEnabled()'),
756889
},
890+
{
891+
code: test('expect(page.locator(".field").isEnabled()).toBe(true)'),
892+
errors: [
893+
{
894+
column: 28,
895+
data: { matcher: 'toBeEnabled', method: 'isEnabled' },
896+
endColumn: 69,
897+
line: 1,
898+
messageId: 'useWebFirstAssertion',
899+
},
900+
],
901+
output: test('await expect(page.locator(".field")).toBeEnabled()'),
902+
},
757903

758904
// isEditable
759905
{
@@ -868,6 +1014,19 @@ runRuleTester('prefer-web-first-assertions', rule, {
8681014
],
8691015
output: test('await expect(page.locator("howdy")).toBeEditable()'),
8701016
},
1017+
{
1018+
code: test('expect(page.locator(".textarea").isEditable()).toBe(true)'),
1019+
errors: [
1020+
{
1021+
column: 28,
1022+
data: { matcher: 'toBeEditable', method: 'isEditable' },
1023+
endColumn: 73,
1024+
line: 1,
1025+
messageId: 'useWebFirstAssertion',
1026+
},
1027+
],
1028+
output: test('await expect(page.locator(".textarea")).toBeEditable()'),
1029+
},
8711030
// Global aliases
8721031
{
8731032
code: test('assert(await page.locator(".tweet").isVisible()).toBe(true)'),

src/rules/prefer-web-first-assertions.ts

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -62,40 +62,41 @@ export default createRule({
6262
create(context) {
6363
return {
6464
CallExpression(node) {
65-
const call = parseFnCall(context, node)
66-
if (call?.type !== 'expect') return
65+
const fnCall = parseFnCall(context, node)
66+
if (fnCall?.type !== 'expect') return
6767

68-
const expect = findParent(call.head.node, 'CallExpression')
68+
const expect = findParent(fnCall.head.node, 'CallExpression')
6969
if (!expect) return
7070

71-
const arg = dereference(context, call.args[0])
71+
const arg = dereference(context, fnCall.args[0])
72+
if (!arg) return
73+
74+
const call = arg.type === 'AwaitExpression' ? arg.argument : arg
7275
if (
73-
!arg ||
74-
arg.type !== 'AwaitExpression' ||
75-
arg.argument.type !== 'CallExpression' ||
76-
arg.argument.callee.type !== 'MemberExpression'
76+
call.type !== 'CallExpression' ||
77+
call.callee.type !== 'MemberExpression'
7778
) {
7879
return
7980
}
8081

8182
// Matcher must be supported
82-
if (!supportedMatchers.has(call.matcherName)) return
83+
if (!supportedMatchers.has(fnCall.matcherName)) return
8384

8485
// Playwright method must be supported
85-
const method = getStringValue(arg.argument.callee.property)
86+
const method = getStringValue(call.callee.property)
8687
const methodConfig = methods[method]
8788
if (!methodConfig) return
8889

8990
// Change the matcher
90-
const notModifier = call.modifiers.find(
91+
const notModifier = fnCall.modifiers.find(
9192
(mod) => getStringValue(mod) === 'not',
9293
)
9394

9495
const isFalsy =
9596
methodConfig.type === 'boolean' &&
96-
((!!call.matcherArgs.length &&
97-
isBooleanLiteral(call.matcherArgs[0], false)) ||
98-
call.matcherName === 'toBeFalsy')
97+
((!!fnCall.matcherArgs.length &&
98+
isBooleanLiteral(fnCall.matcherArgs[0], false)) ||
99+
fnCall.matcherName === 'toBeFalsy')
99100

100101
const isInverse = methodConfig.inverse
101102
? notModifier || isFalsy
@@ -108,17 +109,15 @@ export default createRule({
108109
(+!!notModifier ^ +isFalsy && methodConfig.inverse) ||
109110
methodConfig.matcher
110111

111-
const { callee } = arg.argument
112+
const { callee } = call
112113
context.report({
113114
data: {
114115
matcher: newMatcher,
115116
method,
116117
},
117118
fix: (fixer) => {
118119
const methodArgs =
119-
arg.argument.type === 'CallExpression'
120-
? arg.argument.arguments
121-
: []
120+
call.type === 'CallExpression' ? call.arguments : []
122121

123122
const methodEnd = methodArgs.length
124123
? methodArgs.at(-1)!.range![1] + 1
@@ -128,10 +127,7 @@ export default createRule({
128127
// Add await to the expect call
129128
fixer.insertTextBefore(expect, 'await '),
130129
// Remove the await keyword
131-
fixer.replaceTextRange(
132-
[arg.range![0], arg.argument.range![0]],
133-
'',
134-
),
130+
fixer.replaceTextRange([arg.range![0], call.range![0]], ''),
135131
// Remove the old Playwright method and any arguments
136132
fixer.replaceTextRange(
137133
[callee.property.range![0] - 1, methodEnd],
@@ -147,13 +143,13 @@ export default createRule({
147143

148144
// Add not to the matcher chain if no inverse matcher exists
149145
if (!methodConfig.inverse && !notModifier && isFalsy) {
150-
fixes.push(fixer.insertTextBefore(call.matcher, 'not.'))
146+
fixes.push(fixer.insertTextBefore(fnCall.matcher, 'not.'))
151147
}
152148

153-
fixes.push(fixer.replaceText(call.matcher, newMatcher))
149+
fixes.push(fixer.replaceText(fnCall.matcher, newMatcher))
154150

155151
// Remove boolean argument if it exists
156-
const [matcherArg] = call.matcherArgs ?? []
152+
const [matcherArg] = fnCall.matcherArgs ?? []
157153
if (matcherArg && isBooleanLiteral(matcherArg)) {
158154
fixes.push(fixer.remove(matcherArg))
159155
}
@@ -173,7 +169,7 @@ export default createRule({
173169
).length
174170

175171
if (methodArgs) {
176-
const range = call.matcher.range!
172+
const range = fnCall.matcher.range!
177173
const stringArgs = methodArgs
178174
.map((arg) => getRawValue(arg))
179175
.concat(hasOtherArgs ? '' : [])

0 commit comments

Comments
 (0)