From 41c7c39e1c310383336fc418125ca5703a735a1b Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Fri, 31 Dec 2021 08:16:21 +1300 Subject: [PATCH] feat(prefer-snapshot-hint): check nested scope for multiple snapshot matchers --- docs/rules/prefer-snapshot-hint.md | 25 +- .../__tests__/prefer-snapshot-hint.test.ts | 262 ++++++++++++++++++ src/rules/prefer-snapshot-hint.ts | 97 +++---- 3 files changed, 319 insertions(+), 65 deletions(-) diff --git a/docs/rules/prefer-snapshot-hint.md b/docs/rules/prefer-snapshot-hint.md index 33f91e42c..4c4dbc515 100644 --- a/docs/rules/prefer-snapshot-hint.md +++ b/docs/rules/prefer-snapshot-hint.md @@ -108,18 +108,20 @@ describe('cli', () => { ### `'multi'` (default) Require a hint to be provided when there are multiple external snapshot matchers -within the same test body. +within the scope (meaning it includes nested calls). Examples of **incorrect** code for the `'multi'` option: ```js +const snapshotOutput = ({ stdout, stderr }) => { + expect(stdout).toMatchSnapshot(); + expect(stderr).toMatchSnapshot(); +}; + describe('cli', () => { describe('--version flag', () => { it('prints the version', async () => { - const { stdout, stderr } = await runCli(['--version']); - - expect(stdout).toMatchSnapshot(); - expect(stderr).toMatchSnapshot(); + snapshotOutput(await runCli(['--version'])); }); }); @@ -146,13 +148,18 @@ describe('cli', () => { Examples of **correct** code for the `'multi'` option: ```js +const snapshotOutput = ({ stdout, stderr }, hints) => { + expect(stdout).toMatchSnapshot({}, `stdout: ${hints.stdout}`); + expect(stderr).toMatchSnapshot({}, `stderr: ${hints.stderr}`); +}; + describe('cli', () => { describe('--version flag', () => { it('prints the version', async () => { - const { stdout, stderr } = await runCli(['--version']); - - expect(stdout).toMatchSnapshot('stdout: version string'); - expect(stderr).toMatchSnapshot('stderr: empty'); + snapshotOutput(await runCli(['--version']), { + stdout: 'version string', + stderr: 'empty', + }); }); }); diff --git a/src/rules/__tests__/prefer-snapshot-hint.test.ts b/src/rules/__tests__/prefer-snapshot-hint.test.ts index aefb3b49d..241cf4aa1 100644 --- a/src/rules/__tests__/prefer-snapshot-hint.test.ts +++ b/src/rules/__tests__/prefer-snapshot-hint.test.ts @@ -157,6 +157,21 @@ ruleTester.run('prefer-snapshot-hint (always)', rule, { }, ], }, + { + code: dedent` + it('is true', () => { + { expect(1).toMatchSnapshot(); } + }); + `, + options: ['always'], + errors: [ + { + messageId: 'missingHint', + column: 15, + line: 2, + }, + ], + }, ], }); @@ -260,6 +275,57 @@ ruleTester.run('prefer-snapshot-hint (multi)', rule, { `, options: ['multi'], }, + { + code: dedent` + const myReusableTestBody = (value, snapshotHint) => { + const innerFn = anotherValue => { + expect(anotherValue).toMatchSnapshot(); + + expect(value).toBe(1); + }; + + expect(value).toBe(1); + }; + + it('my test', () => { + expect(1).toMatchSnapshot(); + }); + `, + options: ['multi'], + }, + { + code: dedent` + const myReusableTestBody = (value, snapshotHint) => { + const innerFn = anotherValue => { + expect(value).toBe(1); + }; + + expect(value).toBe(1); + expect(anotherValue).toMatchSnapshot(); + }; + + it('my test', () => { + expect(1).toMatchSnapshot(); + }); + `, + options: ['multi'], + }, + { + code: dedent` + const myReusableTestBody = (value, snapshotHint) => { + const innerFn = anotherValue => { + expect(anotherValue).toMatchSnapshot(); + + expect(value).toBe(1); + }; + + expect(value).toBe(1); + }; + + expect(1).toMatchSnapshot(); + `, + options: ['multi'], + }, ], invalid: [ { @@ -346,6 +412,50 @@ ruleTester.run('prefer-snapshot-hint (multi)', rule, { }, ], }, + { + code: dedent` + it('is true', () => { + expect(1).toMatchSnapshot({}); + { + expect(2).toMatchSnapshot({}); + } + }); + `, + options: ['multi'], + errors: [ + { + messageId: 'missingHint', + column: 13, + line: 2, + }, + { + messageId: 'missingHint', + column: 15, + line: 4, + }, + ], + }, + { + code: dedent` + it('is true', () => { + { expect(1).toMatchSnapshot(); } + { expect(2).toMatchSnapshot(); } + }); + `, + options: ['multi'], + errors: [ + { + messageId: 'missingHint', + column: 15, + line: 2, + }, + { + messageId: 'missingHint', + column: 15, + line: 3, + }, + ], + }, { code: dedent` it('is true', () => { @@ -460,5 +570,157 @@ ruleTester.run('prefer-snapshot-hint (multi)', rule, { }, ], }, + { + code: dedent` + const myReusableTestBody = (value, snapshotHint) => { + expect(value).toMatchSnapshot(); + + const innerFn = anotherValue => { + expect(anotherValue).toMatchSnapshot(); + }; + + expect(value).toBe(1); + expect(value + 1).toMatchSnapshot(null); + expect(value + 2).toThrowErrorMatchingSnapshot(snapshotHint); + }; + `, + options: ['multi'], + errors: [ + { + messageId: 'missingHint', + column: 17, + line: 2, + }, + { + messageId: 'missingHint', + column: 26, + line: 5, + }, + { + messageId: 'missingHint', + column: 21, + line: 9, + }, + ], + }, + { + code: dedent` + const myReusableTestBody = (value, snapshotHint) => { + expect(value).toMatchSnapshot(); + + const innerFn = anotherValue => { + expect(anotherValue).toMatchSnapshot(); + + expect(value).toBe(1); + expect(value + 1).toMatchSnapshot(null); + expect(value + 2).toMatchSnapshot(null, snapshotHint); + }; + }; + `, + options: ['multi'], + errors: [ + { + messageId: 'missingHint', + column: 17, + line: 2, + }, + { + messageId: 'missingHint', + column: 26, + line: 5, + }, + { + messageId: 'missingHint', + column: 23, + line: 8, + }, + ], + }, + { + code: dedent` + const myReusableTestBody = (value, snapshotHint) => { + const innerFn = anotherValue => { + expect(anotherValue).toMatchSnapshot(); + + expect(value).toBe(1); + expect(value + 1).toMatchSnapshot(null); + expect(value + 2).toMatchSnapshot(null, snapshotHint); + }; + + expect(value).toThrowErrorMatchingSnapshot(); + }; + `, + options: ['multi'], + errors: [ + { + messageId: 'missingHint', + column: 26, + line: 3, + }, + { + messageId: 'missingHint', + column: 23, + line: 6, + }, + { + messageId: 'missingHint', + column: 17, + line: 10, + }, + ], + }, + { + code: dedent` + const myReusableTestBody = (value, snapshotHint) => { + const innerFn = anotherValue => { + expect(anotherValue).toMatchSnapshot(); + + expect(value).toBe(1); + }; + + expect(value).toMatchSnapshot(); + }; + + it('my test', () => { + expect(1).toMatchSnapshot(); + }); + `, + options: ['multi'], + errors: [ + { + messageId: 'missingHint', + column: 26, + line: 3, + }, + { + messageId: 'missingHint', + column: 17, + line: 8, + }, + ], + }, + { + code: dedent` + const myReusableTestBody = value => { + expect(value).toMatchSnapshot(); + }; + + expect(1).toMatchSnapshot(); + expect(1).toThrowErrorMatchingSnapshot(); + `, + options: ['multi'], + errors: [ + { + messageId: 'missingHint', + column: 11, + line: 5, + }, + { + messageId: 'missingHint', + column: 11, + line: 6, + }, + ], + }, ], }); diff --git a/src/rules/prefer-snapshot-hint.ts b/src/rules/prefer-snapshot-hint.ts index 8c0691076..98f82d3a2 100644 --- a/src/rules/prefer-snapshot-hint.ts +++ b/src/rules/prefer-snapshot-hint.ts @@ -2,7 +2,6 @@ import { ParsedExpectMatcher, createRule, isExpectCall, - isTestCaseCall, parseExpectCall, } from './utils'; @@ -13,10 +12,6 @@ const isSnapshotMatcher = (matcher: ParsedExpectMatcher) => { }; const isSnapshotMatcherWithoutHint = (matcher: ParsedExpectMatcher) => { - if (!isSnapshotMatcher(matcher)) { - return false; - } - const expectedNumberOfArgumentsWithHint = 1 + Number(matcher.name === 'toMatchSnapshot'); @@ -46,72 +41,62 @@ export default createRule<[('always' | 'multi')?], keyof typeof messages>({ }, defaultOptions: ['multi'], create(context, [mode]) { - let withinTestBody = false; - let previousSnapshotMatcher: ParsedExpectMatcher | null = null; - let hasReportedPreviousSnapshotMatcher = false; - - return { - CallExpression(node) { - if (isTestCaseCall(node)) { - withinTestBody = true; - - return; - } + const snapshotMatchers: ParsedExpectMatcher[] = []; + let expressionDepth = 0; - if (!isExpectCall(node)) { - return; + const reportSnapshotMatchersWithoutHints = () => { + for (const snapshotMatcher of snapshotMatchers) { + if (isSnapshotMatcherWithoutHint(snapshotMatcher)) { + context.report({ + messageId: 'missingHint', + node: snapshotMatcher.node.property, + }); } + } + }; - const { matcher } = parseExpectCall(node); + const enterExpression = () => { + expressionDepth++; + }; - if (!matcher) { - return; - } + const exitExpression = () => { + expressionDepth--; - if (mode === 'always' && isSnapshotMatcherWithoutHint(matcher)) { - context.report({ - messageId: 'missingHint', - node: matcher.node.property, - }); + if (mode === 'always') { + reportSnapshotMatchersWithoutHints(); + snapshotMatchers.length = 0; + } - return; + if (mode === 'multi' && expressionDepth === 0) { + if (snapshotMatchers.length > 1) { + reportSnapshotMatchersWithoutHints(); } - if (!withinTestBody || !isSnapshotMatcher(matcher)) { + snapshotMatchers.length = 0; + } + }; + + return { + 'Program:exit'() { + enterExpression(); + exitExpression(); + }, + FunctionExpression: enterExpression, + 'FunctionExpression:exit': exitExpression, + ArrowFunctionExpression: enterExpression, + 'ArrowFunctionExpression:exit': exitExpression, + CallExpression(node) { + if (!isExpectCall(node)) { return; } - if (!previousSnapshotMatcher) { - previousSnapshotMatcher = matcher; + const { matcher } = parseExpectCall(node); + if (!matcher || !isSnapshotMatcher(matcher)) { return; } - if (isSnapshotMatcherWithoutHint(matcher)) { - context.report({ - messageId: 'missingHint', - node: matcher.node.property, - }); - } - - if ( - isSnapshotMatcherWithoutHint(previousSnapshotMatcher) && - !hasReportedPreviousSnapshotMatcher - ) { - context.report({ - messageId: 'missingHint', - node: previousSnapshotMatcher.node.property, - }); - - hasReportedPreviousSnapshotMatcher = true; - } - }, - 'CallExpression:exit'(node) { - if (isTestCaseCall(node)) { - withinTestBody = false; - previousSnapshotMatcher = null; - hasReportedPreviousSnapshotMatcher = false; - } + snapshotMatchers.push(matcher); }, }; },