From 730b29ec5b4b83f1108ac6647902486c13be59ce Mon Sep 17 00:00:00 2001 From: Dunqing Date: Wed, 27 Sep 2023 15:39:10 +0800 Subject: [PATCH] fix(runner): the fixture of `test.extend` should be init once time in all test (#4168) --- packages/runner/src/fixture.ts | 72 +++++++++--- packages/runner/src/run.ts | 2 + test/core/test/test-extend.test.ts | 181 ++++++++++++++++++++++++----- 3 files changed, 215 insertions(+), 40 deletions(-) diff --git a/packages/runner/src/fixture.ts b/packages/runner/src/fixture.ts index 0ffdce9b164a..9fc94850d105 100644 --- a/packages/runner/src/fixture.ts +++ b/packages/runner/src/fixture.ts @@ -44,6 +44,20 @@ export function mergeContextFixtures(fixtures: Record, context: { f return context } +const fixtureValueMap = new Map() +const fixtureCleanupFnMap = new Map void | Promise>>() + +export async function callFixtureCleanup(id: string) { + const cleanupFnArray = fixtureCleanupFnMap.get(id) + if (!cleanupFnArray) + return + + for (const cleanup of cleanupFnArray.reverse()) + await cleanup() + + fixtureCleanupFnMap.delete(id) +} + export function withFixtures(fn: Function, testContext?: TestContext) { return (hookContext?: TestContext) => { const context: TestContext & { [key: string]: any } | undefined = hookContext || testContext @@ -51,6 +65,12 @@ export function withFixtures(fn: Function, testContext?: TestContext) { if (!context) return fn({}) + let cleanupFnArray = fixtureCleanupFnMap.get(context.task.suite.id)! + if (!cleanupFnArray) { + cleanupFnArray = [] + fixtureCleanupFnMap.set(context.task.suite.id, cleanupFnArray) + } + const fixtures = getFixture(context) if (!fixtures?.length) return fn(context) @@ -63,21 +83,47 @@ export function withFixtures(fn: Function, testContext?: TestContext) { const pendingFixtures = resolveDeps(usedFixtures) let cursor = 0 - async function use(fixtureValue: any) { - const { prop } = pendingFixtures[cursor++] - context![prop] = fixtureValue - - if (cursor < pendingFixtures.length) - await next() - else await fn(context) - } + return new Promise((resolve, reject) => { + async function use(fixtureValue: any) { + const fixture = pendingFixtures[cursor++] + context![fixture.prop] = fixtureValue + + if (!fixtureValueMap.has(fixture)) { + fixtureValueMap.set(fixture, fixtureValue) + cleanupFnArray.unshift(() => { + fixtureValueMap.delete(fixture) + }) + } + + if (cursor < pendingFixtures.length) { + await next() + } + else { + // When all fixtures setup, call the test function + try { + resolve(await fn(context)) + } + catch (err) { + reject(err) + } + return new Promise((resolve) => { + cleanupFnArray.push(resolve) + }) + } + } - async function next() { - const { value } = pendingFixtures[cursor] - typeof value === 'function' ? await value(context, use) : await use(value) - } + async function next() { + const fixture = pendingFixtures[cursor] + const { isFn, value } = fixture + if (fixtureValueMap.has(fixture)) + return use(fixtureValueMap.get(fixture)) + else + return isFn ? value(context, use) : use(value) + } - return next() + const setupFixturePromise = next() + cleanupFnArray.unshift(() => setupFixturePromise) + }) } } diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index bd26172f11a8..798ac93f9633 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -10,6 +10,7 @@ import { collectTests } from './collect' import { setCurrentTest } from './test-state' import { hasFailed, hasTests } from './utils/tasks' import { PendingError } from './errors' +import { callFixtureCleanup } from './fixture' const now = Date.now @@ -321,6 +322,7 @@ export async function runSuite(suite: Suite, runner: VitestRunner) { } try { + await callFixtureCleanup(suite.id) await callSuiteHook(suite, suite, 'afterAll', runner, [suite]) await callCleanupHooks(beforeAllCleanups) } diff --git a/test/core/test/test-extend.test.ts b/test/core/test/test-extend.test.ts index c03fbe0bee78..6da9ac6b2851 100644 --- a/test/core/test/test-extend.test.ts +++ b/test/core/test/test-extend.test.ts @@ -1,6 +1,6 @@ /* eslint-disable prefer-rest-params */ /* eslint-disable no-empty-pattern */ -import { describe, expect, expectTypeOf, test, vi } from 'vitest' +import { afterAll, afterEach, beforeEach, describe, expect, expectTypeOf, test, vi } from 'vitest' interface Fixtures { todoList: number[] @@ -38,39 +38,34 @@ const myTest = test }) describe('test.extend()', () => { - myTest('todoList and doneList', ({ todoList, doneList, archiveList }) => { - expect(todoFn).toBeCalledTimes(1) - expect(doneFn).toBeCalledTimes(1) - - expectTypeOf(todoList).toEqualTypeOf() - expectTypeOf(doneList).toEqualTypeOf() - expectTypeOf(doneList).toEqualTypeOf() + describe('basic', () => { + myTest('todoList and doneList', ({ todoList, doneList, archiveList }) => { + expect(todoFn).toBeCalledTimes(1) + expect(doneFn).toBeCalledTimes(1) - expect(todoList).toEqual([1, 2, 3]) - expect(doneList).toEqual([]) - expect(archiveList).toEqual([]) + expectTypeOf(todoList).toEqualTypeOf() + expectTypeOf(doneList).toEqualTypeOf() + expectTypeOf(doneList).toEqualTypeOf() - doneList.push(todoList.shift()!) - expect(todoList).toEqual([2, 3]) - expect(doneList).toEqual([1]) + expect(todoList).toEqual([1, 2, 3]) + expect(doneList).toEqual([]) + expect(archiveList).toEqual([]) - doneList.push(todoList.shift()!) - expect(todoList).toEqual([3]) - expect(doneList).toEqual([1, 2]) + doneList.push(todoList.shift()!) + expect(todoList).toEqual([2, 3]) + expect(doneList).toEqual([1]) - archiveList.push(todoList.shift()!) - expect(todoList).toEqual([]) - expect(archiveList).toEqual([3]) + doneList.push(todoList.shift()!) + expect(todoList).toEqual([3]) + expect(doneList).toEqual([1, 2]) - archiveList.pop() - }) + archiveList.push(todoList.shift()!) + expect(todoList).toEqual([]) + expect(archiveList).toEqual([3]) - myTest('should called cleanup functions', ({ todoList, doneList, archiveList }) => { - expect(todoList).toEqual([1, 2, 3]) - expect(doneList).toEqual([]) - expect(archiveList).toEqual([]) + archiveList.pop() + }) }) - describe('smartly init fixtures', () => { myTest('should not init any fixtures', function () { expect(todoFn).not.toBeCalled() @@ -150,4 +145,136 @@ describe('test.extend()', () => { expect(archive).toEqual([]) }) }) + + describe('fixture call times', () => { + const apiFn = vi.fn(() => true) + const serviceFn = vi.fn(() => true) + const teardownFn = vi.fn() + + interface APIFixture { + api: boolean + service: boolean + } + + const testAPI = test.extend({ + api: async ({}, use) => { + await use(apiFn()) + apiFn.mockClear() + teardownFn() + }, + service: async ({}, use) => { + await use(serviceFn()) + serviceFn.mockClear() + teardownFn() + }, + }) + + beforeEach(({ api, service }) => { + expect(api).toBe(true) + expect(service).toBe(true) + }) + + testAPI('Should init1 time', ({ api }) => { + expect(api).toBe(true) + expect(apiFn).toBeCalledTimes(1) + }) + + testAPI('Should init 1 time has multiple fixture', ({ api, service }) => { + expect(api).toBe(true) + expect(service).toBe(true) + expect(serviceFn).toBeCalledTimes(1) + expect(apiFn).toBeCalledTimes(1) + }) + + afterEach(({ api, service }) => { + expect(api).toBe(true) + expect(service).toBe(true) + expect(apiFn).toBeCalledTimes(1) + expect(serviceFn).toBeCalledTimes(1) + }) + + afterAll(() => { + expect(serviceFn).toBeCalledTimes(0) + expect(apiFn).toBeCalledTimes(0) + expect(teardownFn).toBeCalledTimes(2) + }) + }) + + describe('fixture in nested describe', () => { + interface Fixture { + foo: number + bar: number + } + + const fooFn = vi.fn(() => 0) + const fooCleanup = vi.fn() + + const barFn = vi.fn(() => 0) + const barCleanup = vi.fn() + + const nestedTest = test.extend({ + async foo({}, use) { + await use(fooFn()) + fooCleanup() + }, + async bar({}, use) { + await use(barFn()) + barCleanup() + }, + }) + + beforeEach(({ foo }) => { + expect(foo).toBe(0) + }) + + nestedTest('should only initialize foo', ({ foo }) => { + expect(foo).toBe(0) + expect(fooFn).toBeCalledTimes(1) + expect(barFn).toBeCalledTimes(0) + }) + + describe('level 2, using both foo and bar together', () => { + beforeEach(({ foo, bar }) => { + expect(foo).toBe(0) + expect(bar).toBe(0) + }) + + nestedTest('should only initialize bar', ({ foo, bar }) => { + expect(foo).toBe(0) + expect(bar).toBe(0) + expect(fooFn).toBeCalledTimes(1) + expect(barFn).toBeCalledTimes(1) + }) + + afterEach(({ foo, bar }) => { + expect(foo).toBe(0) + expect(bar).toBe(0) + }) + + afterAll(() => { + // foo setup in outside describe + // cleanup also called in outside describe + expect(fooCleanup).toHaveBeenCalledTimes(0) + // bar setup in inside describe + // cleanup also called in inside describe + expect(barCleanup).toHaveBeenCalledTimes(1) + }) + }) + + nestedTest('level 2 will not call foo cleanup', ({ foo }) => { + expect(foo).toBe(0) + expect(fooFn).toBeCalledTimes(1) + }) + + afterEach(({ foo }) => { + expect(foo).toBe(0) + }) + + afterAll(() => { + // foo setup in this describe + // cleanup also called in this describe + expect(fooCleanup).toHaveBeenCalledTimes(1) + expect(barCleanup).toHaveBeenCalledTimes(1) + }) + }) })