From efe79c990f4bbd155d5a7f1dd797a448a847f6c9 Mon Sep 17 00:00:00 2001 From: Yukihiro Hasegawa <49516827+y-hsgw@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:32:21 +0900 Subject: [PATCH] fix: expect.hasAssertions() / expect.assertions() - Does not work when using expect() bound to current test (#595) * fix: ensure prefer-expect-assertions rule works correctly with test context * test: add test * fix: require-local-test-context-for-concurrent-snapshots rule --- src/rules/prefer-expect-assertions.ts | 6 -- ...l-test-context-for-concurrent-snapshots.ts | 11 +++- src/utils/parse-vitest-fn-call.ts | 31 +++++---- tests/prefer-expect-assertions.test.ts | 65 +++++++++++++++---- 4 files changed, 81 insertions(+), 32 deletions(-) diff --git a/src/rules/prefer-expect-assertions.ts b/src/rules/prefer-expect-assertions.ts index b4bf312..d6df45d 100644 --- a/src/rules/prefer-expect-assertions.ts +++ b/src/rules/prefer-expect-assertions.ts @@ -202,12 +202,6 @@ export default createEslintRule({ const [, secondArg] = node.arguments - if (secondArg?.type === AST_NODE_TYPES.ArrowFunctionExpression && secondArg.params.length) { - if (secondArg?.params[0].type === AST_NODE_TYPES.ObjectPattern) { - if (secondArg.params[0].properties[0].type === AST_NODE_TYPES.Property && secondArg.params[0].properties[0].key.type === AST_NODE_TYPES.Identifier && secondArg.params[0].properties[0].key.name === "expect") return - } - } - if (!isFunction(secondArg) || !shouldCheckFunction(secondArg)) return diff --git a/src/rules/require-local-test-context-for-concurrent-snapshots.ts b/src/rules/require-local-test-context-for-concurrent-snapshots.ts index 8559b58..e2da574 100644 --- a/src/rules/require-local-test-context-for-concurrent-snapshots.ts +++ b/src/rules/require-local-test-context-for-concurrent-snapshots.ts @@ -1,6 +1,6 @@ import { AST_NODE_TYPES } from '@typescript-eslint/utils' import { createEslintRule, isSupportedAccessor } from '../utils' -import { isTypeOfVitestFnCall } from '../utils/parse-vitest-fn-call' +import { isTypeOfVitestFnCall, parseVitestFnCall } from '../utils/parse-vitest-fn-call' export const RULE_NAME = 'require-local-test-context-for-concurrent-snapshots' @@ -21,8 +21,13 @@ export default createEslintRule({ create(context) { return { CallExpression(node) { - const isNotAnAssertion = !isTypeOfVitestFnCall(node, context, ['expect']) - if (isNotAnAssertion) return + const vitestFnCall = parseVitestFnCall(node, context) + if (vitestFnCall === null) + return + if (vitestFnCall.type !== "expect") + return + if (vitestFnCall.type === "expect" && vitestFnCall.head.type === "testContext") + return const isNotASnapshotAssertion = ![ 'toMatchSnapshot', diff --git a/src/utils/parse-vitest-fn-call.ts b/src/utils/parse-vitest-fn-call.ts index 8717637..abc5e6d 100644 --- a/src/utils/parse-vitest-fn-call.ts +++ b/src/utils/parse-vitest-fn-call.ts @@ -1,7 +1,7 @@ import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/utils' import { DescribeAlias, HookName, ModifierName, TestCaseName } from './types' import { ValidVitestFnCallChains } from './valid-vitest-fn-call-chains' -import { AccessorNode, getAccessorValue, getStringValue, isIdentifier, isStringNode, isSupportedAccessor } from '.' +import { AccessorNode, getAccessorValue, getStringValue, isFunction, isIdentifier, isStringNode, isSupportedAccessor } from '.' export type VitestFnType = | 'test' @@ -17,7 +17,7 @@ export type VitestFnType = interface ResolvedVitestFn { original: string | null local: string - type: 'import' | 'global' + type: 'import' | 'global' | 'testContext' } interface ImportDetails { @@ -309,17 +309,11 @@ export function getNodeChain(node: TSESTree.Node): AccessorNode[] | null { return null } -interface ResolvedVitestFnType { - original: string | null - local: string - type: 'import' | 'global' -} - const resolveVitestFn = ( context: TSESLint.RuleContext, - node: TSESTree.Node, + node: TSESTree.CallExpression, identifier: string -): ResolvedVitestFnType | null => { +): ResolvedVitestFn | null => { const scope = context.sourceCode.getScope ? context.sourceCode.getScope(node) : context.getScope() @@ -328,6 +322,13 @@ const resolveVitestFn = ( if (maybeImport === 'local') return null + if (maybeImport === "testContext") + return { + local: identifier, + original: null, + type: "testContext" + } + if (maybeImport) { if (maybeImport.source === 'vitest') { return { @@ -364,7 +365,7 @@ const resolvePossibleAliasedGlobal = ( export const resolveScope = ( scope: TSESLint.Scope.Scope, identifier: string -): ImportDetails | 'local' | null => { +): ImportDetails | 'local' | 'testContext' | null => { let currentScope: TSESLint.Scope.Scope | null = scope while (currentScope !== null) { @@ -373,6 +374,14 @@ export const resolveScope = ( if (ref && ref.defs.length > 0) { const def = ref.defs[ref.defs.length - 1] + const objectParam = isFunction(def.node) ? def.node.params.find(params => params.type === AST_NODE_TYPES.ObjectPattern ) : undefined + if (objectParam) { + const property = objectParam.properties.find(property => property.type === AST_NODE_TYPES.Property) + const key = property?.key.type === AST_NODE_TYPES.Identifier ? property.key : undefined + if (key?.name === identifier) + return "testContext" + } + const importDetails = describePossibleImportDef(def) if (importDetails?.local === identifier) diff --git a/tests/prefer-expect-assertions.test.ts b/tests/prefer-expect-assertions.test.ts index 9dff27b..b7ca47c 100644 --- a/tests/prefer-expect-assertions.test.ts +++ b/tests/prefer-expect-assertions.test.ts @@ -23,7 +23,7 @@ ruleTester.run(RULE_NAME, rule, { expect(number).toBeGreaterThan(value); } }; - + it('returns numbers that are greater than two', function () { expectNumbersToBeGreaterThan(getNumbers(), 2); }); @@ -63,6 +63,47 @@ ruleTester.run(RULE_NAME, rule, { } ] }, + { + code: ` +it('my test description', ({ expect }) => { + const a = 1; + const b = 2; + + expect(sum(a, b)).toBe(a + b); +}) +`, + errors: [ + { + messageId: 'haveExpectAssertions', + column: 1, + line: 2, + suggestions: [ + { + messageId: 'suggestAddingHasAssertions', + output: ` +it('my test description', ({ expect }) => {expect.hasAssertions(); + const a = 1; + const b = 2; + + expect(sum(a, b)).toBe(a + b); +}) +` + }, + { + messageId: 'suggestAddingAssertions', + output: ` +it('my test description', ({ expect }) => {expect.assertions(); + const a = 1; + const b = 2; + + expect(sum(a, b)).toBe(a + b); +}) +` + } + ] + } + ], + }, { code: 'it(\'resolves\', () => expect(staged()).toBe(true));', errors: [ @@ -251,12 +292,12 @@ ruleTester.run(RULE_NAME, rule, { { code: `it("it1", () => { expect.hasAssertions(); - + for (const number of getNumbers()) { expect(number).toBeGreaterThan(0); } }); - + it("it1", () => { for (const number of getNumbers()) { expect(number).toBeGreaterThan(0); @@ -273,12 +314,12 @@ ruleTester.run(RULE_NAME, rule, { messageId: 'suggestAddingHasAssertions', output: `it("it1", () => { expect.hasAssertions(); - + for (const number of getNumbers()) { expect(number).toBeGreaterThan(0); } }); - + it("it1", () => {expect.hasAssertions(); for (const number of getNumbers()) { expect(number).toBeGreaterThan(0); @@ -289,12 +330,12 @@ ruleTester.run(RULE_NAME, rule, { messageId: 'suggestAddingAssertions', output: `it("it1", () => { expect.hasAssertions(); - + for (const number of getNumbers()) { expect(number).toBeGreaterThan(0); } }); - + it("it1", () => {expect.assertions(); for (const number of getNumbers()) { expect(number).toBeGreaterThan(0); @@ -311,7 +352,7 @@ ruleTester.run(RULE_NAME, rule, { expect(number).toBeGreaterThan(4); } }); - + it("returns numbers that are greater than five", () => { for (const number of getNumbers()) { expect(number).toBeGreaterThan(5); @@ -332,7 +373,7 @@ ruleTester.run(RULE_NAME, rule, { expect(number).toBeGreaterThan(4); } }); - + it("returns numbers that are greater than five", () => { for (const number of getNumbers()) { expect(number).toBeGreaterThan(5); @@ -347,7 +388,7 @@ ruleTester.run(RULE_NAME, rule, { expect(number).toBeGreaterThan(4); } }); - + it("returns numbers that are greater than five", () => { for (const number of getNumbers()) { expect(number).toBeGreaterThan(5); @@ -370,7 +411,7 @@ ruleTester.run(RULE_NAME, rule, { expect(number).toBeGreaterThan(4); } }); - + it("returns numbers that are greater than five", () => {expect.hasAssertions(); for (const number of getNumbers()) { expect(number).toBeGreaterThan(5); @@ -385,7 +426,7 @@ ruleTester.run(RULE_NAME, rule, { expect(number).toBeGreaterThan(4); } }); - + it("returns numbers that are greater than five", () => {expect.assertions(); for (const number of getNumbers()) { expect(number).toBeGreaterThan(5);