Skip to content

Commit

Permalink
feat: global setup (#372)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
dominikg and antfu committed Jan 10, 2022
1 parent 480514d commit eaa119f
Show file tree
Hide file tree
Showing 12 changed files with 212 additions and 0 deletions.
13 changes: 13 additions & 0 deletions docs/config/index.md
Expand Up @@ -235,6 +235,19 @@ Silent mode

Path to setup files

### globalSetup

- **Type:** `string | string[]`

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)).

::: info
Multiple globalSetup files are possible. setup and teardown are executed sequentially with teardown in reverse order.
:::


### watchIgnore

- **Type:** `(string | RegExp)[]`
Expand Down
2 changes: 2 additions & 0 deletions packages/vitest/src/node/index.ts
Expand Up @@ -11,6 +11,7 @@ import type { ArgumentsType, Reporter, ResolvedConfig, UserConfig } from '../typ
import { SnapshotManager } from '../integrations/snapshot/manager'
import { configFiles } from '../constants'
import { deepMerge, ensurePackageInstalled, hasFailed, noop, notNullish, slash, toArray } from '../utils'
import { GlobalSetupPlugin } from '../plugins/globalSetup'
import { MocksPlugin } from '../plugins/mock'
import { DefaultReporter, ReportersMap } from '../reporters'

Expand Down Expand Up @@ -407,6 +408,7 @@ export async function VitestPlugin(options: UserConfig = {}, viteOverrides: Vite
},
},
MocksPlugin(),
GlobalSetupPlugin(),
options.ui
? await UIPlugin()
: null,
Expand Down
87 changes: 87 additions & 0 deletions packages/vitest/src/plugins/globalSetup.ts
@@ -0,0 +1,87 @@
import type { Plugin, ViteDevServer } from 'vite'
import { toArray } from '../utils'

interface GlobalSetupFile {
file: string
setup?: () => Promise<Function|void>|void
teardown?: Function
}

async function loadGlobalSetupFiles(server: ViteDevServer): Promise<GlobalSetupFile[]> {
const globalSetupFiles = toArray(server.config.test?.globalSetup)
return Promise.all(globalSetupFiles.map(file => loadGlobalSetupFile(file, server)))
}

async function loadGlobalSetupFile(file: string, server: ViteDevServer): Promise<GlobalSetupFile> {
const m = await server.ssrLoadModule(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 const GlobalSetupPlugin = (): Plugin => {
let server: ViteDevServer
let globalSetupFiles: GlobalSetupFile[]
return {
name: 'vitest:global-setup-plugin',
enforce: 'pre',

// @ts-expect-error ssr is still flagged as alpha
config(config) {
if (config.test?.globalSetup) {
return {
ssr: {
noExternal: true, // needed so ssrLoadModule call doesn't initialize server._ssrExternals
},
}
}
},

configureServer(_server) {
server = _server
},

async buildStart() {
if (!server.config.test?.globalSetup)
return

globalSetupFiles = await loadGlobalSetupFiles(server)
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
}
},

async buildEnd() {
for (const globalSetupFile of globalSetupFiles.reverse()) {
try {
await globalSetupFile.teardown?.()
}
catch (error) {
console.error(`error during global teardown of ${globalSetupFile.file}`, error)
}
}
},
}
}
5 changes: 5 additions & 0 deletions packages/vitest/src/types/config.ts
Expand Up @@ -164,6 +164,11 @@ export interface InlineConfig {
*/
setupFiles?: string | string[]

/**
* Path to global setup files
*/
globalSetup?: string | string[]

/**
* Pattern of file paths to be ignore from triggering watch rerun
*
Expand Down
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions test/global-setup/package.json
@@ -0,0 +1,12 @@
{
"name": "@vitest/test-global-setup",
"private": true,
"type": "module",
"scripts": {
"test": "vitest",
"coverage": "vitest --coverage"
},
"devDependencies": {
"vitest": "workspace:*"
}
}
19 changes: 19 additions & 0 deletions test/global-setup/setupFiles/default-export.js
@@ -0,0 +1,19 @@
const sleep = async n => new Promise(resolve => setTimeout(resolve, n))

export default async function() {
// setup something eg start a server, db or whatever
// const server = await start()
// console.log('globalSetup default-export.js')
// const start = Date.now()
await sleep(25)

return async() => {
// tear it down here
// await server.close()
await sleep(25)
// const duration = Date.now() - start
// console.log(`globalTeardown default-export.js, took ${(duration)}ms`)
// if (duration > 2000)
// throw new Error('error from teardown in globalSetup default-export.js')
}
}
21 changes: 21 additions & 0 deletions test/global-setup/setupFiles/named-exports.js
@@ -0,0 +1,21 @@
const sleep = async n => new Promise(resolve => setTimeout(resolve, n))

// let start

export async function setup() {
// setup something eg start a server, db or whatever
// const server = await start()
// console.log('globalSetup named-exports.js')
// start = Date.now()
await sleep(25)
}

export async function teardown() {
// tear it down here
// await server.close()
await sleep(25)
// const duration = Date.now() - start
// console.log(`globalTeardown named-exports.js, took ${(duration)}ms`)
// if (duration > 4000)
// throw new Error('error from teardown in globalSetup named-exports.js')
}
11 changes: 11 additions & 0 deletions test/global-setup/setupFiles/server.ts
@@ -0,0 +1,11 @@
import http from 'http'

export async function startServer(host: string, port: number): Promise<http.Server> {
return new Promise((resolve) => {
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('Hello Vitest\n')
})
server.listen(port, host, () => resolve(server))
})
}
6 changes: 6 additions & 0 deletions test/global-setup/setupFiles/ts-with-imports.ts
@@ -0,0 +1,6 @@
import { startServer } from './server'

export default async function() {
const server = await startServer('127.0.0.1', 9876)
return async() => new Promise<void>(resolve => server.close(() => resolve()))
}
16 changes: 16 additions & 0 deletions test/global-setup/test/global-setup.test.ts
@@ -0,0 +1,16 @@
import http from 'http'

async function sendRequest(host: string, port: number) {
return new Promise<string>((resolve) => {
http.request({ host, port }, (res) => {
let data = ''
res.on('data', d => data += d)
res.on('end', () => resolve(data))
}).end()
})
}

test('server running', async() => {
const res = await sendRequest('127.0.0.1', 9876)
expect(res).toBe('Hello Vitest\n')
})
14 changes: 14 additions & 0 deletions test/global-setup/vitest.config.ts
@@ -0,0 +1,14 @@
/// <reference types="vitest" />

import { defineConfig } from 'vite'

export default defineConfig({
test: {
global: true,
globalSetup: [
'./setupFiles/default-export.js',
'./setupFiles/named-exports.js',
'./setupFiles/ts-with-imports.ts',
],
},
})

0 comments on commit eaa119f

Please sign in to comment.