From e22549d9fa64d9788d35a3af9753cbf0e449ca8d Mon Sep 17 00:00:00 2001 From: Han Feng Date: Fri, 12 May 2023 19:16:21 +0800 Subject: [PATCH 1/5] feat: throw error if inline snapshot inside of `x.each` --- packages/runner/src/suite.ts | 16 +++++++++------- packages/runner/src/types/tasks.ts | 1 + packages/runner/src/utils/chain.ts | 2 +- packages/snapshot/src/client.ts | 5 +++++ .../vitest/src/integrations/snapshot/chai.ts | 4 ++++ .../fixtures/inline-snapshop-inside-each.test.ts | 15 +++++++++++++++ .../fails/test/__snapshots__/runner.test.ts.snap | 6 ++++++ 7 files changed, 41 insertions(+), 8 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 15050880ce50..70e2e934d018 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?: number | TestOptions) { +function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, mode: RunMode, concurrent?: boolean, shuffle?: boolean, each?: boolean, suiteOptions?: number | TestOptions) { const tasks: (Test | TaskCustom | Suite | SuiteCollector)[] = [] const factoryQueue: (Test | Suite | SuiteCollector)[] = [] @@ -79,6 +79,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m id: '', type: 'test', name, + each: this.each, mode, suite: undefined!, fails: this.fails, @@ -143,6 +144,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m type: 'suite', name, mode, + each, shuffle, tasks: [], } @@ -186,11 +188,11 @@ function createSuite() { function suiteFn(this: Record, name: string, factory?: SuiteFactory, options?: number | TestOptions) { checkVersion() const mode: RunMode = this.only ? 'only' : this.skip ? 'skip' : this.todo ? 'todo' : 'run' - 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[]) { - const suite = this.withContext() + suiteFn.each = function(this: { withContext: (entries: Record) => SuiteAPI }, cases: ReadonlyArray, ...args: any[]) { + const suite = this.withContext({ each: true }) if (Array.isArray(cases) && args.length) cases = formatTemplateString(cases, args) @@ -217,7 +219,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 @@ -225,8 +227,8 @@ function createTest(fn: ( )) { const testFn = fn as any - testFn.each = function(this: { withContext: () => TestAPI }, cases: ReadonlyArray, ...args: any[]) { - const test = this.withContext() + testFn.each = function(this: { withContext: (entries: Record) => TestAPI }, cases: ReadonlyArray, ...args: any[]) { + const test = this.withContext({ each: true }) if (Array.isArray(cases) && args.length) cases = formatTemplateString(cases, args) diff --git a/packages/runner/src/types/tasks.ts b/packages/runner/src/types/tasks.ts index 5c548d87240d..b9a40d0c8cbf 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..65580a6c244c 100644 --- a/packages/runner/src/utils/chain.ts +++ b/packages/runner/src/utils/chain.ts @@ -15,7 +15,7 @@ export function createChainable chain.bind(context) + chain.withContext = (entries?: Record) => chain.bind(Object.assign(context, entries)) for (const key of keys) { Object.defineProperty(chain, key, { get() { diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts index 5e7a92ea2dd9..fd392bd80320 100644 --- a/packages/snapshot/src/client.ts +++ b/packages/snapshot/src/client.ts @@ -32,6 +32,7 @@ interface AssertOptions { name?: string message?: string isInline?: boolean + isInsideEach?: boolean properties?: object inlineSnapshot?: string error?: Error @@ -95,6 +96,7 @@ export class SnapshotClient { name = this.name, message, isInline = false, + isInsideEach = false, properties, inlineSnapshot, error, @@ -106,6 +108,9 @@ export class SnapshotClient { if (!filepath) throw new Error('Snapshot cannot be used outside of test') + if (isInline && isInsideEach) + throw new Error('InlineSnapshot cannot be used inside of test.each or describe.each') + if (typeof properties === 'object') { if (typeof received !== 'object' || !received) throw new Error('Received value must be an object when the matcher has properties') diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index 20c82338330f..e11b75109457 100644 --- a/packages/vitest/src/integrations/snapshot/chai.ts +++ b/packages/vitest/src/integrations/snapshot/chai.ts @@ -112,10 +112,12 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { if (inlineSnapshot) inlineSnapshot = stripSnapshotIndentation(inlineSnapshot) const errorMessage = utils.flag(this, 'message') + getSnapshotClient().assert({ received: expected, message, isInline: true, + isInsideEach: test && (test.each || test.suite?.each), properties, inlineSnapshot, error, @@ -149,11 +151,13 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { const test = utils.flag(this, 'vitest-test') const promise = utils.flag(this, 'promise') as string | undefined const errorMessage = utils.flag(this, 'message') + const isInsideEach = test && (test.each || test.suite?.each) getSnapshotClient().assert({ received: getErrorString(expected, promise), message, inlineSnapshot, isInline: true, + isInsideEach, error, errorMessage, ...getTestNames(test), 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..49f7d0b6884f --- /dev/null +++ b/test/fails/fixtures/inline-snapshop-inside-each.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, test } from 'vitest' + +test.each([1])('', () => { + expect('').toMatchInlineSnapshot() +}) + +describe.each([1])('', () => { + test('', () => { + expect('').toMatchInlineSnapshot() + }) + + test.each([1])('', () => { + expect('').toMatchInlineSnapshot() + }) +}) diff --git a/test/fails/test/__snapshots__/runner.test.ts.snap b/test/fails/test/__snapshots__/runner.test.ts.snap index 8a0d962d31ea..7451f7763ad0 100644 --- a/test/fails/test/__snapshots__/runner.test.ts.snap +++ b/test/fails/test/__snapshots__/runner.test.ts.snap @@ -15,6 +15,12 @@ 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" +`; + 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"`; From 788f6a52a3d3a94cfa1aec750e306868045c2f44 Mon Sep 17 00:00:00 2001 From: Han Feng Date: Wed, 17 May 2023 18:42:05 +0800 Subject: [PATCH 2/5] chore: remove `isInsideEach` --- packages/snapshot/src/client.ts | 5 ----- packages/vitest/src/integrations/snapshot/chai.ts | 14 +++++++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/snapshot/src/client.ts b/packages/snapshot/src/client.ts index fd392bd80320..5e7a92ea2dd9 100644 --- a/packages/snapshot/src/client.ts +++ b/packages/snapshot/src/client.ts @@ -32,7 +32,6 @@ interface AssertOptions { name?: string message?: string isInline?: boolean - isInsideEach?: boolean properties?: object inlineSnapshot?: string error?: Error @@ -96,7 +95,6 @@ export class SnapshotClient { name = this.name, message, isInline = false, - isInsideEach = false, properties, inlineSnapshot, error, @@ -108,9 +106,6 @@ export class SnapshotClient { if (!filepath) throw new Error('Snapshot cannot be used outside of test') - if (isInline && isInsideEach) - throw new Error('InlineSnapshot cannot be used inside of test.each or describe.each') - if (typeof properties === 'object') { if (typeof received !== 'object' || !received) throw new Error('Received value must be an object when the matcher has properties') diff --git a/packages/vitest/src/integrations/snapshot/chai.ts b/packages/vitest/src/integrations/snapshot/chai.ts index e11b75109457..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 @@ -117,7 +120,6 @@ export const SnapshotPlugin: ChaiPlugin = (chai, utils) => { received: expected, message, isInline: true, - isInsideEach: test && (test.each || test.suite?.each), properties, inlineSnapshot, error, @@ -146,18 +148,20 @@ 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') - const isInsideEach = test && (test.each || test.suite?.each) + getSnapshotClient().assert({ received: getErrorString(expected, promise), message, inlineSnapshot, isInline: true, - isInsideEach, error, errorMessage, ...getTestNames(test), From c4e2f7496dbc4535e4d4aab62ab60345e637e8b6 Mon Sep 17 00:00:00 2001 From: Han Feng Date: Wed, 17 May 2023 18:43:03 +0800 Subject: [PATCH 3/5] test: `toThrowErrorMatchingInlineSnapshot` in `x.each` --- .../fixtures/inline-snapshop-inside-each.test.ts | 12 ++++++++++++ test/fails/test/__snapshots__/runner.test.ts.snap | 2 ++ 2 files changed, 14 insertions(+) diff --git a/test/fails/fixtures/inline-snapshop-inside-each.test.ts b/test/fails/fixtures/inline-snapshop-inside-each.test.ts index 49f7d0b6884f..8a66298bc215 100644 --- a/test/fails/fixtures/inline-snapshop-inside-each.test.ts +++ b/test/fails/fixtures/inline-snapshop-inside-each.test.ts @@ -12,4 +12,16 @@ describe.each([1])('', () => { 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 7451f7763ad0..66dbe981fe39 100644 --- a/test/fails/test/__snapshots__/runner.test.ts.snap +++ b/test/fails/test/__snapshots__/runner.test.ts.snap @@ -18,6 +18,8 @@ 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" `; From baa47b241026d20628181176e27b29377df13b0b Mon Sep 17 00:00:00 2001 From: Han Feng Date: Fri, 19 May 2023 17:06:08 +0800 Subject: [PATCH 4/5] fix: reset `each` flag after envoking `x.each` --- packages/runner/src/suite.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 70e2e934d018..7d0f7fb16b72 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -205,6 +205,8 @@ function createSuite() { ? suite(formatTitle(name, items, idx), () => fn(...items), options) : suite(formatTitle(name, items, idx), () => fn(i), options) }) + + this.withContext({ each: undefined }) } } @@ -242,6 +244,8 @@ function createTest(fn: ( ? test(formatTitle(name, items, idx), () => fn(...items), options) : test(formatTitle(name, items, idx), () => fn(i), options) }) + + this.withContext({ each: undefined }) } } From 5cf06d824991fb291af9d835e738785df14a601b Mon Sep 17 00:00:00 2001 From: Han Feng Date: Thu, 25 May 2023 15:44:22 +0800 Subject: [PATCH 5/5] chore: use `setContext` method --- packages/runner/src/suite.ts | 14 ++++++++------ packages/runner/src/utils/chain.ts | 5 ++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/runner/src/suite.ts b/packages/runner/src/suite.ts index 665be8e2dd47..231048970e4f 100644 --- a/packages/runner/src/suite.ts +++ b/packages/runner/src/suite.ts @@ -203,8 +203,9 @@ function createSuite() { return createSuiteCollector(name, factory, mode, this.concurrent, this.shuffle, this.each, options) } - suiteFn.each = function(this: { withContext: (entries: Record) => SuiteAPI }, cases: ReadonlyArray, ...args: any[]) { - const suite = this.withContext({ each: true }) + 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) @@ -218,7 +219,7 @@ function createSuite() { : suite(formatTitle(name, items, idx), () => fn(i), options) }) - this.withContext({ each: undefined }) + this.setContext('each', undefined) } } @@ -241,8 +242,9 @@ function createTest(fn: ( )) { const testFn = fn as any - testFn.each = function(this: { withContext: (entries: Record) => TestAPI }, cases: ReadonlyArray, ...args: any[]) { - const test = this.withContext({ each: true }) + 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) @@ -257,7 +259,7 @@ function createTest(fn: ( : test(formatTitle(name, items, idx), () => fn(i), options) }) - this.withContext({ each: undefined }) + this.setContext('each', undefined) } } diff --git a/packages/runner/src/utils/chain.ts b/packages/runner/src/utils/chain.ts index 65580a6c244c..ba467c1b05e3 100644 --- a/packages/runner/src/utils/chain.ts +++ b/packages/runner/src/utils/chain.ts @@ -15,7 +15,10 @@ export function createChainable) => chain.bind(Object.assign(context, entries)) + chain.withContext = () => chain.bind(context) + chain.setContext = (key: T, value: boolean | undefined) => { + context[key] = value + } for (const key of keys) { Object.defineProperty(chain, key, { get() {