diff --git a/src/rules/expect-expect.test.ts b/src/rules/expect-expect.test.ts index 725288d4..a5891d3a 100644 --- a/src/rules/expect-expect.test.ts +++ b/src/rules/expect-expect.test.ts @@ -29,6 +29,16 @@ runRuleTester('expect-expect', rule, { name: 'Custom assert function', options: [{ assertFunctionNames: ['wayComplexCustomCondition'] }], }, + { + code: javascript` + test('should fail', async ({ page }) => { + await assertCustomCondition(page) + }) + `, + errors: [{ messageId: 'noAssertions', type: 'Identifier' }], + name: 'Custom assert function pattern mismatch', + options: [{ assertFunctionPatterns: ['^verify.*', '^check.*'] }], + }, { code: 'it("should pass", () => hi(true).toBeDefined())', errors: [{ messageId: 'noAssertions', type: 'Identifier' }], @@ -110,6 +120,38 @@ runRuleTester('expect-expect', rule, { name: 'Custom assert class method', options: [{ assertFunctionNames: ['assertCustomCondition'] }], }, + { + code: javascript` + test('should pass', async ({ page }) => { + await verifyElementVisible(page.locator('button')) + }) + `, + name: 'Custom assert function matching regex pattern', + options: [{ assertFunctionPatterns: ['^verify.*'] }], + }, + { + code: javascript` + test('should pass', async ({ page }) => { + await page.checkTextContent('Hello') + await validateUserLoggedIn(page) + }) + `, + name: 'Multiple custom assert functions matching different regex patterns', + options: [{ assertFunctionPatterns: ['^check.*', '^validate.*'] }], + }, + { + code: javascript` + test('should pass', async ({ page }) => { + await myCustomAssert(page) + await anotherAssertion(true) + }) + `, + name: 'Mixed string and regex pattern matching', + options: [{ + assertFunctionNames: ['myCustomAssert'], + assertFunctionPatterns: ['.*Assertion$'] + }], + }, { code: 'it("should pass", () => expect(true).toBeDefined())', name: 'Global alias - test', diff --git a/src/rules/expect-expect.ts b/src/rules/expect-expect.ts index 481cae4f..a8803c80 100644 --- a/src/rules/expect-expect.ts +++ b/src/rules/expect-expect.ts @@ -3,20 +3,25 @@ import { dig } from '../utils/ast.js' import { createRule } from '../utils/createRule.js' import { parseFnCall } from '../utils/parseFnCall.js' + export default createRule({ create(context) { const options = { assertFunctionNames: [] as string[], + assertFunctionPatterns: [] as string[], ...((context.options?.[0] as Record) ?? {}), } + const unchecked: ESTree.CallExpression[] = [] + function checkExpressions(nodes: ESTree.Node[]) { for (const node of nodes) { const index = node.type === 'CallExpression' ? unchecked.indexOf(node) : -1 + if (index !== -1) { unchecked.splice(index, 1) break @@ -24,15 +29,42 @@ export default createRule({ } } + + function matchesAssertFunction(node: ESTree.CallExpression): boolean { + // Check exact string matches + if (options.assertFunctionNames.some((name) => dig(node.callee, name))) { + return true + } + + + // Check regex patterns + if (options.assertFunctionPatterns.some((pattern) => { + try { + const regex = new RegExp(pattern) + return dig(node.callee, regex) + } catch { + // Invalid regex pattern, skip it + return false + } + })) { + return true + } + + + return false + } + + return { CallExpression(node) { const call = parseFnCall(context, node) + if (call?.type === 'test') { unchecked.push(node) } else if ( call?.type === 'expect' || - options.assertFunctionNames.find((name) => dig(node.callee, name)) + matchesAssertFunction(node) ) { const ancestors = context.sourceCode.getAncestors(node) checkExpressions(ancestors) @@ -63,6 +95,10 @@ export default createRule({ items: [{ type: 'string' }], type: 'array', }, + assertFunctionPatterns: { + items: [{ type: 'string' }], + type: 'array', + }, }, type: 'object', },