diff --git a/docs/config/index.md b/docs/config/index.md index 9ff794c8c767..797d1d09c8d5 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -924,7 +924,7 @@ globalThis.resetBeforeEachTest = true - **Type:** `string | string[]` -Path to global setup files, relative to project root +Path to global setup files, relative to project root. A global setup file can either export named functions `setup` and `teardown` or a `default` function that returns a teardown function ([example](https://github.com/vitest-dev/vitest/blob/main/test/global-setup/vitest.config.ts)). @@ -933,7 +933,7 @@ Multiple globalSetup files are possible. setup and teardown are executed sequent ::: ::: warning -Beware that the global setup is run in a different global scope, so your tests don't have access to variables defined here. +Beware that the global setup is running in a different global scope, so your tests don't have access to variables defined here. Also, since Vitest 1.0.0-beta, global setup runs only if there is at least one running test. This means that global setup might start running during watch mode after test file is changed, for example (the test file will wait for global setup to finish before running). ::: diff --git a/packages/vitest/src/node/core.ts b/packages/vitest/src/node/core.ts index 05c654c8e9c3..a8a14bb10914 100644 --- a/packages/vitest/src/node/core.ts +++ b/packages/vitest/src/node/core.ts @@ -420,6 +420,10 @@ export class Vitest { return Array.from(projects).map(project => [project, file] as WorkspaceSpec) } + async initializeGlobalSetup(paths: WorkspaceSpec[]) { + await Promise.all(paths.map(async ([project]) => project.initializeGlobalSetup())) + } + async runFiles(paths: WorkspaceSpec[]) { const filepaths = paths.map(([, file]) => file) this.state.collectPaths(filepaths) @@ -440,6 +444,8 @@ export class Vitest { this.invalidates.clear() this.snapshot.clear() this.state.clearErrors() + await this.initializeGlobalSetup(paths) + try { await this.pool.runTests(paths, invalidates) } diff --git a/packages/vitest/src/node/globalSetup.ts b/packages/vitest/src/node/globalSetup.ts new file mode 100644 index 000000000000..165a89634fbf --- /dev/null +++ b/packages/vitest/src/node/globalSetup.ts @@ -0,0 +1,38 @@ +import { toArray } from '@vitest/utils' +import type { ViteNodeRunner } from 'vite-node/client' +import type { WorkspaceProject } from './workspace' + +export interface GlobalSetupFile { + file: string + setup?: () => Promise | void + teardown?: Function +} + +export async function loadGlobalSetupFiles(project: WorkspaceProject): Promise { + const globalSetupFiles = toArray(project.server.config.test?.globalSetup) + return Promise.all(globalSetupFiles.map(file => loadGlobalSetupFile(file, project.runner))) +} + +async function loadGlobalSetupFile(file: string, runner: ViteNodeRunner): Promise { + const m = await runner.executeFile(file) + for (const exp of ['default', 'setup', 'teardown']) { + if (m[exp] != null && typeof m[exp] !== 'function') + throw new Error(`invalid export in globalSetup file ${file}: ${exp} must be a function`) + } + if (m.default) { + return { + file, + setup: m.default, + } + } + else if (m.setup || m.teardown) { + return { + file, + setup: m.setup, + teardown: m.teardown, + } + } + else { + throw new Error(`invalid globalSetup file ${file}. Must export setup, teardown or have a default export`) + } +} diff --git a/packages/vitest/src/node/plugins/globalSetup.ts b/packages/vitest/src/node/plugins/globalSetup.ts deleted file mode 100644 index 7910590aff8c..000000000000 --- a/packages/vitest/src/node/plugins/globalSetup.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Plugin } from 'vite' -import type { ViteNodeRunner } from 'vite-node/client' -import c from 'picocolors' -import { toArray } from '../../utils' -import { divider } from '../reporters/renderers/utils' -import type { Vitest } from '../core' -import type { Logger } from '../logger' - -interface GlobalSetupFile { - file: string - setup?: () => Promise | void - teardown?: Function -} - -type SetupInstance = Pick - -async function loadGlobalSetupFiles(project: SetupInstance): Promise { - const server = project.server - const runner = project.runner - const globalSetupFiles = toArray(server.config.test?.globalSetup) - return Promise.all(globalSetupFiles.map(file => loadGlobalSetupFile(file, runner))) -} - -async function loadGlobalSetupFile(file: string, runner: ViteNodeRunner): Promise { - const m = await runner.executeFile(file) - for (const exp of ['default', 'setup', 'teardown']) { - if (m[exp] != null && typeof m[exp] !== 'function') - throw new Error(`invalid export in globalSetup file ${file}: ${exp} must be a function`) - } - if (m.default) { - return { - file, - setup: m.default, - } - } - else if (m.setup || m.teardown) { - return { - file, - setup: m.setup, - teardown: m.teardown, - } - } - else { - throw new Error(`invalid globalSetup file ${file}. Must export setup, teardown or have a default export`) - } -} - -export function GlobalSetupPlugin(project: SetupInstance, logger: Logger): Plugin { - let globalSetupFiles: GlobalSetupFile[] - return { - name: 'vitest:global-setup-plugin', - enforce: 'pre', - - async buildStart() { - if (!project.server.config.test?.globalSetup) - return - - globalSetupFiles = await loadGlobalSetupFiles(project) - - try { - for (const globalSetupFile of globalSetupFiles) { - const teardown = await globalSetupFile.setup?.() - if (teardown == null || !!globalSetupFile.teardown) - continue - if (typeof teardown !== 'function') - throw new Error(`invalid return value in globalSetup file ${globalSetupFile.file}. Must return a function`) - globalSetupFile.teardown = teardown - } - } - catch (e) { - logger.error(`\n${c.red(divider(c.bold(c.inverse(' Error during global setup '))))}`) - await logger.printError(e) - process.exit(1) - } - }, - - async buildEnd() { - if (globalSetupFiles?.length) { - for (const globalSetupFile of globalSetupFiles.reverse()) { - try { - await globalSetupFile.teardown?.() - } - catch (error) { - logger.error(`error during global teardown of ${globalSetupFile.file}`, error) - } - } - } - }, - } -} diff --git a/packages/vitest/src/node/plugins/index.ts b/packages/vitest/src/node/plugins/index.ts index 2f30b17dc958..d7c8fc7f7047 100644 --- a/packages/vitest/src/node/plugins/index.ts +++ b/packages/vitest/src/node/plugins/index.ts @@ -8,7 +8,6 @@ import { resolveApiServerConfig } from '../config' import { Vitest } from '../core' import { generateScopedClassName } from '../../integrations/css/css-modules' import { SsrReplacerPlugin } from './ssrReplacer' -import { GlobalSetupPlugin } from './globalSetup' import { CSSEnablerPlugin } from './cssEnabler' import { CoverageTransform } from './coverageTransform' import { MocksPlugin } from './mocks' @@ -178,7 +177,6 @@ export async function VitestPlugin(options: UserConfig = {}, ctx = new Vitest('t }, }, SsrReplacerPlugin(), - GlobalSetupPlugin(ctx, ctx.logger), ...CSSEnablerPlugin(ctx), CoverageTransform(ctx), options.ui diff --git a/packages/vitest/src/node/plugins/workspace.ts b/packages/vitest/src/node/plugins/workspace.ts index cb4b016b85f6..119b7c453941 100644 --- a/packages/vitest/src/node/plugins/workspace.ts +++ b/packages/vitest/src/node/plugins/workspace.ts @@ -8,7 +8,6 @@ import type { ResolvedConfig, UserWorkspaceConfig } from '../../types' import { CoverageTransform } from './coverageTransform' import { CSSEnablerPlugin } from './cssEnabler' import { SsrReplacerPlugin } from './ssrReplacer' -import { GlobalSetupPlugin } from './globalSetup' import { MocksPlugin } from './mocks' import { deleteDefineConfig, hijackVitePluginInject, resolveFsAllow } from './utils' import { VitestResolver } from './vitestResolver' @@ -123,7 +122,6 @@ export function WorkspaceVitestPlugin(project: WorkspaceProject, options: Worksp SsrReplacerPlugin(), ...CSSEnablerPlugin(project), CoverageTransform(project.ctx), - GlobalSetupPlugin(project, project.ctx.logger), MocksPlugin(), VitestResolver(project.ctx), VitestOptimizer(), diff --git a/packages/vitest/src/node/workspace.ts b/packages/vitest/src/node/workspace.ts index e0e445ee2e18..947c4018776c 100644 --- a/packages/vitest/src/node/workspace.ts +++ b/packages/vitest/src/node/workspace.ts @@ -5,6 +5,7 @@ import { dirname, relative, resolve, toNamespacedPath } from 'pathe' import type { ViteDevServer, InlineConfig as ViteInlineConfig } from 'vite' import { ViteNodeRunner } from 'vite-node/client' import { ViteNodeServer } from 'vite-node/server' +import c from 'picocolors' import { createBrowserServer } from '../integrations/browser/server' import type { ArgumentsType, Reporter, ResolvedConfig, UserConfig, UserWorkspaceConfig, Vitest } from '../types' import { deepMerge, hasFailed } from '../utils' @@ -14,6 +15,9 @@ import { getBrowserProvider } from '../integrations/browser' import { isBrowserEnabled, resolveConfig } from './config' import { WorkspaceVitestPlugin } from './plugins/workspace' import { createViteServer } from './vite' +import type { GlobalSetupFile } from './globalSetup' +import { loadGlobalSetupFiles } from './globalSetup' +import { divider } from './reporters/renderers/utils' interface InitializeProjectOptions extends UserWorkspaceConfig { workspaceConfigPath: string @@ -64,6 +68,9 @@ export class WorkspaceProject { testFilesList: string[] = [] + private _globalSetupInit = false + private _globalSetups: GlobalSetupFile[] = [] + constructor( public path: string | number, public ctx: Vitest, @@ -77,6 +84,50 @@ export class WorkspaceProject { return this.ctx.getCoreWorkspaceProject() === this } + async initializeGlobalSetup() { + if (this._globalSetupInit) + return + + this._globalSetupInit = true + + this._globalSetups = await loadGlobalSetupFiles(this) + + try { + for (const globalSetupFile of this._globalSetups) { + const teardown = await globalSetupFile.setup?.() + if (teardown == null || !!globalSetupFile.teardown) + continue + if (typeof teardown !== 'function') + throw new Error(`invalid return value in globalSetup file ${globalSetupFile.file}. Must return a function`) + globalSetupFile.teardown = teardown + } + } + catch (e) { + this.logger.error(`\n${c.red(divider(c.bold(c.inverse(' Error during global setup '))))}`) + await this.logger.printError(e) + process.exit(1) + } + } + + async teardownGlobalSetup() { + if (!this._globalSetupInit || !this._globalSetups.length) + return + for (const globalSetupFile of this._globalSetups.reverse()) { + try { + await globalSetupFile.teardown?.() + } + catch (error) { + this.logger.error(`error during global teardown of ${globalSetupFile.file}`, error) + await this.logger.printError(error) + process.exitCode = 1 + } + } + } + + get logger() { + return this.ctx.logger + } + // it's possible that file path was imported with different queries (?raw, ?url, etc) getModulesByFilepath(file: string) { const set = this.server.moduleGraph.getModulesByFile(file) @@ -337,6 +388,7 @@ export class WorkspaceProject { this.server.close(), this.typechecker?.stop(), this.browser?.close(), + this.teardownGlobalSetup(), ].filter(Boolean)) } return this.closingPromise diff --git a/test/global-setup-fail/fixtures/example.test.ts b/test/global-setup-fail/fixtures/example.test.ts new file mode 100644 index 000000000000..f71db97810eb --- /dev/null +++ b/test/global-setup-fail/fixtures/example.test.ts @@ -0,0 +1,5 @@ +import { expect, test } from 'vitest' + +test('example test', () => { + expect(1 + 1).toBe(2) +})