Skip to content

Commit f2aa1ec

Browse files
authored
fix: Fix more edge cases with missing-playwright-await (#190)
* Test * Fix tests
1 parent ae52f21 commit f2aa1ec

File tree

2 files changed

+112
-34
lines changed

2 files changed

+112
-34
lines changed

src/rules/missing-playwright-await.ts

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -70,50 +70,35 @@ function getCallType(
7070
isIdentifier(node.callee.object, 'test') &&
7171
isPropertyAccessor(node.callee, 'step')
7272
) {
73-
return { messageId: 'testStep' };
73+
return { messageId: 'testStep', node };
7474
}
7575

7676
const expectType = getExpectType(node);
7777
if (!expectType) return;
7878

79-
// expect.poll
80-
if (expectType === 'poll') {
81-
return { messageId: 'expectPoll' };
82-
}
83-
84-
// expect with awaitable matcher
8579
const [lastMatcher] = getMatchers(node).slice(-1);
86-
const matcherName = getStringValue(lastMatcher);
87-
88-
if (awaitableMatchers.has(matcherName)) {
89-
return { data: { matcherName }, messageId: 'expect' };
90-
}
91-
}
80+
const grandparent = lastMatcher.parent.parent;
9281

93-
function isPromiseAll(node: Rule.Node) {
94-
return node.type === 'ArrayExpression' &&
95-
node.parent.type === 'CallExpression' &&
96-
node.parent.callee.type === 'MemberExpression' &&
97-
isIdentifier(node.parent.callee.object, 'Promise') &&
98-
isIdentifier(node.parent.callee.property, 'all')
99-
? node.parent
100-
: null;
101-
}
82+
// If the grandparent is not a CallExpression, then it's an incomplete
83+
// expect statement, and we don't need to check it.
84+
if (grandparent.type !== 'CallExpression') return;
10285

103-
function checkValidity(node: Rule.Node): ESTree.Node | undefined {
104-
if (validTypes.has(node.parent.type)) return;
86+
const matcherName = getStringValue(lastMatcher);
10587

106-
const promiseAll = isPromiseAll(node.parent);
107-
return promiseAll
108-
? checkValidity(promiseAll)
109-
: node.parent.type === 'MemberExpression' ||
110-
(node.parent.type === 'CallExpression' && node.parent.callee === node)
111-
? checkValidity(node.parent)
112-
: node;
88+
// The node needs to be checked if it's an expect.poll expression or an
89+
// awaitable matcher.
90+
if (expectType === 'poll' || awaitableMatchers.has(matcherName)) {
91+
return {
92+
data: { matcherName },
93+
messageId: expectType === 'poll' ? 'expectPoll' : 'expect',
94+
node: grandparent,
95+
};
96+
}
11397
}
11498

11599
export default {
116100
create(context) {
101+
const sourceCode = context.sourceCode ?? context.getSourceCode();
117102
const options = context.options[0] || {};
118103
const awaitableMatchers = new Set([
119104
...expectPlaywrightMatchers,
@@ -122,12 +107,54 @@ export default {
122107
...(options.customMatchers || []),
123108
]);
124109

110+
function checkValidity(node: Rule.Node) {
111+
// If the parent is a valid type (e.g. return or await), we don't need to
112+
// check any further.
113+
if (validTypes.has(node.parent.type)) return true;
114+
115+
// If the parent is an array, we need to check the grandparent to see if
116+
// it's a Promise.all, or a variable.
117+
if (node.parent.type === 'ArrayExpression') {
118+
return checkValidity(node.parent);
119+
}
120+
121+
// If the parent is a call expression, we need to check the grandparent
122+
// to see if it's a Promise.all.
123+
if (
124+
node.parent.type === 'CallExpression' &&
125+
node.parent.callee.type === 'MemberExpression' &&
126+
isIdentifier(node.parent.callee.object, 'Promise') &&
127+
isIdentifier(node.parent.callee.property, 'all')
128+
) {
129+
return true;
130+
}
131+
132+
// If the parent is a variable declarator, we need to check the scope to
133+
// find where it is referenced. When we find the reference, we can
134+
// re-check validity.
135+
if (node.parent.type === 'VariableDeclarator') {
136+
const scope = sourceCode.getScope(node.parent.parent);
137+
138+
for (const ref of scope.references) {
139+
const refParent = (ref.identifier as Rule.Node).parent;
140+
141+
// If the parent of the reference is valid, we can immediately return
142+
// true. Otherwise, we'll check the validity of the parent to continue
143+
// the loop.
144+
if (validTypes.has(refParent.type)) return true;
145+
if (checkValidity(refParent)) return true;
146+
}
147+
}
148+
149+
return false;
150+
}
151+
125152
return {
126153
CallExpression(node) {
127154
const result = getCallType(node, awaitableMatchers);
128-
const reportNode = result ? checkValidity(node) : undefined;
155+
const isValid = result ? checkValidity(result.node) : false;
129156

130-
if (result && reportNode) {
157+
if (result && !isValid) {
131158
context.report({
132159
data: result.data,
133160
fix: (fixer) => fixer.insertTextBefore(node, 'await '),

test/spec/missing-playwright-await.spec.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,6 @@ runRuleTester('missing-playwright-await', rule, {
156156
messageId: 'expect',
157157
},
158158
],
159-
only: true,
160159
output: dedent`
161160
test('test', async () => {
162161
const softExpect = expect.configure({ soft: true })
@@ -191,6 +190,8 @@ runRuleTester('missing-playwright-await', rule, {
191190
],
192191
output: test("await test['step']('foo', async () => {})"),
193192
},
193+
194+
// Lack of Promise.all
194195
{
195196
code: dedent(
196197
test(`
@@ -253,5 +254,55 @@ runRuleTester('missing-playwright-await', rule, {
253254
])
254255
`),
255256
},
257+
{
258+
code: dedent(
259+
test(`
260+
const promises = [
261+
expect(page.locator("foo")).toHaveText("bar"),
262+
expect(page).toHaveTitle("baz"),
263+
]
264+
265+
await Promise.all(promises)
266+
`),
267+
),
268+
},
269+
{
270+
code: dedent(
271+
test(`
272+
const promises = [
273+
expect(page.locator("foo")).toHaveText("bar"),
274+
expect(page).toHaveTitle("baz"),
275+
]
276+
277+
return promises
278+
`),
279+
),
280+
},
281+
{
282+
code: dedent(
283+
test(`
284+
const foo = [
285+
expect(page.locator("foo")).toHaveText("bar"),
286+
expect(page).toHaveTitle("baz"),
287+
]
288+
289+
const bar = await Promise.all(foo)
290+
return bar
291+
`),
292+
),
293+
},
294+
{
295+
code: dedent(
296+
test(`
297+
const foo = [
298+
expect(page.locator("foo")).toHaveText("bar"),
299+
expect(page).toHaveTitle("baz"),
300+
]
301+
302+
const bar = foo
303+
return bar
304+
`),
305+
),
306+
},
256307
],
257308
});

0 commit comments

Comments
 (0)