diff --git a/src/rules/missing-playwright-await.test.ts b/src/rules/missing-playwright-await.test.ts index 8c53a44e..098d1c68 100644 --- a/src/rules/missing-playwright-await.test.ts +++ b/src/rules/missing-playwright-await.test.ts @@ -368,5 +368,17 @@ runRuleTester('missing-playwright-await', rule, { }, }, }, + // Regression: variable passed to getByText (should not crash or false positive) + { + code: dedent( + test(` + const thisIsCausingTheBug = 'some text'; + const pageCover = page.getByText(thisIsCausingTheBug); + const expectation = expect(pageCover, "message").toBeVisible(); + await page.clock.runFor(60_000); + await expectation; + `) + ), + }, ], }) diff --git a/src/rules/missing-playwright-await.ts b/src/rules/missing-playwright-await.ts index eb2f1241..7d193c21 100644 --- a/src/rules/missing-playwright-await.ts +++ b/src/rules/missing-playwright-await.ts @@ -95,48 +95,54 @@ export default createRule({ ]) function checkValidity(node: ESTree.Node) { - const parent = getParent(node) - if (!parent) return false - - // If the parent is a valid type (e.g. return or await), we don't need to - // check any further. - if (validTypes.has(parent.type)) return true - - // If the parent is an array, we need to check the grandparent to see if - // it's a Promise.all, or a variable. - if (parent.type === 'ArrayExpression') { - return checkValidity(parent) - } - - // If the parent is a call expression, we need to check the grandparent - // to see if it's a Promise.all. - if ( - parent.type === 'CallExpression' && - parent.callee.type === 'MemberExpression' && - isIdentifier(parent.callee.object, 'Promise') && - isIdentifier(parent.callee.property, 'all') - ) { - return true - } - - // If the parent is a variable declarator, we need to check the scope to - // find where it is referenced. When we find the reference, we can - // re-check validity. - if (parent.type === 'VariableDeclarator') { - const scope = context.sourceCode.getScope(parent.parent) + // Add visited set to prevent infinite recursion + function _checkValidity(node: ESTree.Node, visited: Set): boolean { + const parent = getParent(node) + if (!parent) return false + if (visited.has(parent)) return false + visited.add(parent) + + // If the parent is a valid type (e.g. return or await), we don't need to + // check any further. + if (validTypes.has(parent.type)) return true + + // If the parent is an array, we need to check the grandparent to see if + // it's a Promise.all, or a variable. + if (parent.type === 'ArrayExpression') { + return _checkValidity(parent, visited) + } - for (const ref of scope.references) { - const refParent = (ref.identifier as Rule.Node).parent + // If the parent is a call expression, we need to check the grandparent + // to see if it's a Promise.all. + if ( + parent.type === 'CallExpression' && + parent.callee.type === 'MemberExpression' && + isIdentifier(parent.callee.object, 'Promise') && + isIdentifier(parent.callee.property, 'all') + ) { + return true + } - // If the parent of the reference is valid, we can immediately return - // true. Otherwise, we'll check the validity of the parent to continue - // the loop. - if (validTypes.has(refParent.type)) return true - if (checkValidity(refParent)) return true + // If the parent is a variable declarator, we need to check the scope to + // find where it is referenced. When we find the reference, we can + // re-check validity. + if (parent.type === 'VariableDeclarator') { + const scope = context.sourceCode.getScope(parent.parent) + + for (const ref of scope.references) { + const refParent = (ref.identifier as Rule.Node).parent + if (visited.has(refParent)) continue + // If the parent of the reference is valid, we can immediately return + // true. Otherwise, we'll check the validity of the parent to continue + // the loop. + if (validTypes.has(refParent.type)) return true + if (_checkValidity(refParent, visited)) return true + } } - } - return false + return false + } + return _checkValidity(node, new Set()) } return {