From 7c2f7088e62d22605026a37942c1c1c9a4bd162c Mon Sep 17 00:00:00 2001 From: Han Feng Date: Thu, 25 May 2023 17:27:43 +0800 Subject: [PATCH] feat: throw error if using inline snapshot inside of `test.each` or `describe.each` (#3360) Co-authored-by: Vladimir --- packages/runner/src/suite.ts | 18 +++++++++---- packages/runner/src/types/tasks.ts | 1 + packages/runner/src/utils/chain.ts | 3 +++ .../vitest/src/integrations/snapshot/chai.ts | 12 +++++++-- .../inline-snapshop-inside-each.test.ts | 27 +++++++++++++++++++ .../test/__snapshots__/runner.test.ts.snap | 8 ++++++ 6 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 test/fails/fixtures/inline-snapshop-inside-each.test.ts diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 2c8fa8ecfef4..231048970e4f 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -53,7 +53,7 @@ export function createSuiteHooks() { } // implementations -function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, mode: RunMode, concurrent?: boolean, shuffle?: boolean, suiteOptions?: TestOptions) { +function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, mode: RunMode, concurrent?: boolean, shuffle?: boolean, each?: boolean, suiteOptions?: TestOptions) { const tasks: (Test | TaskCustom | Suite | SuiteCollector)[] = [] const factoryQueue: (Test | Suite | SuiteCollector)[] = [] @@ -80,6 +80,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m id: '', type: 'test', name, + each: this.each, mode, suite: undefined!, fails: this.fails, @@ -145,6 +146,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m type: 'suite', name, mode, + each, shuffle, tasks: [], } @@ -198,11 +200,12 @@ function createSuite() { options = { repeats: currentSuite.options.repeats, ...options } } - return createSuiteCollector(name, factory, mode, this.concurrent, this.shuffle, options) + return createSuiteCollector(name, factory, mode, this.concurrent, this.shuffle, this.each, options) } - suiteFn.each = function(this: { withContext: () => SuiteAPI }, cases: ReadonlyArray, ...args: any[]) { + suiteFn.each = function(this: { withContext: () => SuiteAPI; setContext: (key: string, value: boolean | undefined) => SuiteAPI }, cases: ReadonlyArray, ...args: any[]) { const suite = this.withContext() + this.setContext('each', true) if (Array.isArray(cases) && args.length) cases = formatTemplateString(cases, args) @@ -215,6 +218,8 @@ function createSuite() { ? suite(formatTitle(name, items, idx), () => fn(...items), options) : suite(formatTitle(name, items, idx), () => fn(i), options) }) + + this.setContext('each', undefined) } } @@ -229,7 +234,7 @@ function createSuite() { function createTest(fn: ( ( - this: Record<'concurrent' | 'skip' | 'only' | 'todo' | 'fails', boolean | undefined>, + this: Record<'concurrent' | 'skip' | 'only' | 'todo' | 'fails' | 'each', boolean | undefined>, title: string, fn?: TestFunction, options?: number | TestOptions @@ -237,8 +242,9 @@ function createTest(fn: ( )) { const testFn = fn as any - testFn.each = function(this: { withContext: () => TestAPI }, cases: ReadonlyArray, ...args: any[]) { + testFn.each = function(this: { withContext: () => SuiteAPI; setContext: (key: string, value: boolean | undefined) => SuiteAPI }, cases: ReadonlyArray, ...args: any[]) { const test = this.withContext() + this.setContext('each', true) if (Array.isArray(cases) && args.length) cases = formatTemplateString(cases, args) @@ -252,6 +258,8 @@ function createTest(fn: ( ? test(formatTitle(name, items, idx), () => fn(...items), options) : test(formatTitle(name, items, idx), () => fn(i), options) }) + + this.setContext('each', undefined) } } diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 194189ba60e2..0c8bab18b301 100644 --- a/packages/runner/src/types/tasks.ts +++ b/packages/runner/src/types/tasks.ts @@ -9,6 +9,7 @@ export interface TaskBase { id: string name: string mode: RunMode + each?: boolean concurrent?: boolean shuffle?: boolean suite?: Suite diff --git a/packages/runner/src/utils/chain.ts b/packages/runner/src/utils/chain.ts index 12db6fbb125b..ba467c1b05e3 100644 --- a/packages/runner/src/utils/chain.ts +++ b/packages/runner/src/utils/chain.ts @@ -16,6 +16,9 @@ export function createChainable chain.bind(context) + chain.setContext = (key: T, value: boolean | undefined) => { + context[key] = value + } for (const key of keys) { Object.defineProperty(chain, key, { get() { diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 20c82338330f..2b2e1dac2a0e 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -101,9 +101,12 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { chai.Assertion.prototype, 'toMatchInlineSnapshot', function __INLINE_SNAPSHOT__(this: Record, properties?: object, inlineSnapshot?: string, message?: string) { + const test = utils.flag(this, 'vitest-test') + const isInsideEach = test && (test.each || test.suite?.each) + if (isInsideEach) + throw new Error('InlineSnapshot cannot be used inside of test.each or describe.each') const expected = utils.flag(this, 'object') const error = utils.flag(this, 'error') - const test = utils.flag(this, 'vitest-test') if (typeof properties === 'string') { message = inlineSnapshot inlineSnapshot = properties @@ -112,6 +115,7 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { if (inlineSnapshot) inlineSnapshot = stripSnapshotIndentation(inlineSnapshot) const errorMessage = utils.flag(this, 'message') + getSnapshotClient().assert({ received: expected, message, @@ -144,11 +148,15 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { chai.Assertion.prototype, 'toThrowErrorMatchingInlineSnapshot', function __INLINE_SNAPSHOT__(this: Record, inlineSnapshot: string, message: string) { + const test = utils.flag(this, 'vitest-test') + const isInsideEach = test && (test.each || test.suite?.each) + if (isInsideEach) + throw new Error('InlineSnapshot cannot be used inside of test.each or describe.each') const expected = utils.flag(this, 'object') const error = utils.flag(this, 'error') - const test = utils.flag(this, 'vitest-test') const promise = utils.flag(this, 'promise') as string | undefined const errorMessage = utils.flag(this, 'message') + getSnapshotClient().assert({ received: getErrorString(expected, promise), message, diff --git a/test/fails/fixtures/inline-snapshop-inside-each.test.ts b/test/fails/fixtures/inline-snapshop-inside-each.test.ts new file mode 100644 index 000000000000..8a66298bc215 --- /dev/null +++ b/test/fails/fixtures/inline-snapshop-inside-each.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from 'vitest' + +test.each([1])('', () => { + expect('').toMatchInlineSnapshot() +}) + +describe.each([1])('', () => { + test('', () => { + expect('').toMatchInlineSnapshot() + }) + + test.each([1])('', () => { + expect('').toMatchInlineSnapshot() + }) + + test('', () => { + expect(() => { + throw new Error('1') + }).toThrowErrorMatchingInlineSnapshot() + }) + + test.each([1])('', () => { + expect(() => { + throw new Error('1') + }).toThrowErrorMatchingInlineSnapshot() + }) +}) diff --git a/test/fails/test/__snapshots__/runner.test.ts.snap b/test/fails/test/__snapshots__/runner.test.ts.snap index 8a0d962d31ea..66dbe981fe39 100644 --- a/test/fails/test/__snapshots__/runner.test.ts.snap +++ b/test/fails/test/__snapshots__/runner.test.ts.snap @@ -15,6 +15,14 @@ exports[`should fails > hooks-called.test.ts > hooks-called.test.ts 1`] = ` Error: before all" `; +exports[`should fails > inline-snapshop-inside-each.test.ts > inline-snapshop-inside-each.test.ts 1`] = ` +"Error: InlineSnapshot cannot be used inside of test.each or describe.each +Error: InlineSnapshot cannot be used inside of test.each or describe.each +Error: InlineSnapshot cannot be used inside of test.each or describe.each +Error: InlineSnapshot cannot be used inside of test.each or describe.each +Error: InlineSnapshot cannot be used inside of test.each or describe.each" +`; + exports[`should fails > mock-import-proxy-module.test.ts > mock-import-proxy-module.test.ts 1`] = `"Error: There are some problems in resolving the mocks API."`; exports[`should fails > nested-suite.test.ts > nested-suite.test.ts 1`] = `"AssertionError: expected true to be false // Object.is equality"`;