diff --git a/src/env/happy-dom.ts b/src/env/happy-dom.ts new file mode 100644 index 000000000..dcbf9f71f --- /dev/null +++ b/src/env/happy-dom.ts @@ -0,0 +1,25 @@ +import { Environment } from '../types' +import { KEYS } from './jsdom-keys' + +export default ({ + name: 'happy-dom', + async setup(global) { + const { Window } = await import('happy-dom') + const win = new Window() + + const keys = KEYS.concat(Object.getOwnPropertyNames(win)) + .filter(k => !k.startsWith('_')) + .filter(k => !(k in global)) + + for (const key of keys) + // @ts-expect-error + global[key] = win[key] + + return { + teardown(global) { + win.happyDOM.cancelAsync() + keys.forEach(key => delete global[key]) + }, + } + }, +}) diff --git a/src/env/index.ts b/src/env/index.ts new file mode 100644 index 000000000..911d7308e --- /dev/null +++ b/src/env/index.ts @@ -0,0 +1,9 @@ +import node from './node' +import jsdom from './jsdom' +import happy from './happy-dom' + +export const environments = { + node, + jsdom, + 'happy-dom': happy, +} diff --git a/src/integrations/dom/keys.ts b/src/env/jsdom-keys.ts similarity index 100% rename from src/integrations/dom/keys.ts rename to src/env/jsdom-keys.ts diff --git a/src/env/jsdom.ts b/src/env/jsdom.ts new file mode 100644 index 000000000..44aae77ad --- /dev/null +++ b/src/env/jsdom.ts @@ -0,0 +1,30 @@ +import { Environment } from '../types' +import { KEYS } from './jsdom-keys' + +export default ({ + name: 'jsdom', + async setup(global) { + const { JSDOM } = await import('jsdom') + const dom = new JSDOM('', + { + pretendToBeVisual: true, + runScripts: 'dangerously', + // TODO: options + url: 'http://localhost:3000', + }, + ) + + const keys = KEYS.concat(Object.getOwnPropertyNames(dom.window)) + .filter(k => !k.startsWith('_')) + .filter(k => !(k in global)) + + for (const key of keys) + global[key] = dom.window[key] + + return { + teardown(global) { + keys.forEach(key => delete global[key]) + }, + } + }, +}) diff --git a/src/env/node.ts b/src/env/node.ts new file mode 100644 index 000000000..e59e0e49b --- /dev/null +++ b/src/env/node.ts @@ -0,0 +1,11 @@ +import { Environment } from '../types' + +export default ({ + name: 'node', + async setup() { + return { + teardown() { + }, + } + }, +}) diff --git a/src/integrations/dom/happy-dom.ts b/src/integrations/dom/happy-dom.ts deleted file mode 100644 index ea3e18e5b..000000000 --- a/src/integrations/dom/happy-dom.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Window } from 'happy-dom' -import { KEYS } from './keys' - -export function setupHappyDOM(global: any) { - const win = new Window() - - const keys = KEYS.concat(Object.getOwnPropertyNames(win)) - .filter(k => !k.startsWith('_')) - .filter(k => !(k in global)) - - for (const key of keys) - // @ts-expect-error - global[key] = win[key] - - return { - dom: win, - restore() { - win.happyDOM.cancelAsync() - keys.forEach(key => delete global[key]) - }, - } -} diff --git a/src/integrations/dom/jsdom.ts b/src/integrations/dom/jsdom.ts deleted file mode 100644 index d95a6e9b3..000000000 --- a/src/integrations/dom/jsdom.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { JSDOM } from 'jsdom' -import { KEYS } from './keys' - -export function setupJSDOM(global: any) { - const dom = new JSDOM('', - { - pretendToBeVisual: true, - runScripts: 'dangerously', - // TODO: options - url: 'http://localhost:3000', - }, - ) - - const keys = KEYS.concat(Object.getOwnPropertyNames(dom.window)) - .filter(k => !k.startsWith('_')) - .filter(k => !(k in global)) - - for (const key of keys) - global[key] = dom.window[key] - - return { - dom, - restore() { - keys.forEach(key => delete global[key]) - }, - } -} diff --git a/src/node/cli.ts b/src/node/cli.ts index 8cde41d6a..6c637fea1 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -18,13 +18,17 @@ sade('vitest [filter]', true) .option('-w, --watch', 'watch mode', false) .option('-u, --update', 'update snapshot', false) .option('--global', 'inject apis globally', false) - .option('--dom', 'mock browser api using jsdom or happy-dom', '') - .action(async(cliFilters, argv: CliOptions) => { + .option('--dom', 'mock browser api happy-dom', false) + .option('--environment', 'runner environment', '') + .action(async(cliFilters, argv: CliOptions & { dom?: boolean }) => { process.env.VITEST = 'true' console.log(c.magenta(c.bold('\nVitest is in closed beta exclusively for Sponsors'))) console.log(c.yellow('Learn more at https://vitest.dev\n')) + if (argv.dom) + argv.environment = 'happy-dom' + const { config, server } = await initViteServer({ ...argv, cliFilters }) const ctx = process.__vitest__ = { diff --git a/src/node/init.ts b/src/node/init.ts index 7a1e3395b..4a7456841 100644 --- a/src/node/init.ts +++ b/src/node/init.ts @@ -49,9 +49,10 @@ export async function initViteServer(options: CliOptions = {}) { Object.assign(resolved, server.config.test) - resolved.depsInline = server.config.test?.deps?.inline || [] - resolved.depsExternal = server.config.test?.deps?.external || [] + resolved.depsInline = resolved.deps?.inline || [] + resolved.depsExternal = resolved.deps?.external || [] + resolved.environment = resolved.environment || 'node' resolved.threads = resolved.threads ?? true resolved.interpretDefault = resolved.interpretDefault ?? true diff --git a/src/runtime/entry.ts b/src/runtime/entry.ts index c1f0824f0..abbbf442d 100644 --- a/src/runtime/entry.ts +++ b/src/runtime/entry.ts @@ -1,11 +1,9 @@ import { ResolvedConfig } from '../types' -import { setupEnv } from './env' +import { setupGlobalEnv, withEnv } from './env' import { startTests } from './run' export async function run(files: string[], config: ResolvedConfig): Promise { - const restore = await setupEnv(config) + await setupGlobalEnv(config) - await startTests(files) - - restore?.() + await withEnv(config.environment, () => startTests(files)) } diff --git a/src/runtime/env.ts b/src/runtime/env.ts index 028f95e00..0ebd295e2 100644 --- a/src/runtime/env.ts +++ b/src/runtime/env.ts @@ -1,15 +1,20 @@ +import { environments } from '../env' import { setupChai } from '../integrations/chai/setup' import { ResolvedConfig } from '../types' -export async function setupEnv(config: ResolvedConfig) { +export async function setupGlobalEnv(config: ResolvedConfig) { await setupChai() if (config.global) (await import('../integrations/global')).registerApiGlobally() - - // TODO: rework this - if (config.dom === 'happy-dom') - return (await import('../integrations/dom/happy-dom')).setupHappyDOM(globalThis).restore - else if (config.dom) - return (await import('../integrations/dom/jsdom')).setupJSDOM(globalThis).restore +} + +export async function withEnv(name: ResolvedConfig['environment'], fn: () => Promise) { + const env = await environments[name].setup(globalThis) + try { + await fn() + } + finally { + await env.teardown(globalThis) + } } diff --git a/src/types.ts b/src/types.ts index 73ac7e45b..2cf64960a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -50,11 +50,13 @@ export interface UserOptions { global?: boolean /** - * Use `jsdom` or `happy-dom` to mock browser APIs + * Running environment * - * @default false + * Supports 'node', 'jsdom', 'happy-dom' + * + * @default 'node' */ - dom?: boolean | 'jsdom' | 'happy-dom' + environment?: 'node' | 'jsdom' | 'happy-dom' /** * Update snapshot files @@ -253,6 +255,15 @@ export interface ModuleCache { transformResult?: TransformResult } +export interface EnvironmentReturn { + teardown: (global: any) => Awaitable +} + +export interface Environment { + name: string + setup(global: any): Awaitable +} + export interface WorkerContext { port: MessagePort config: ResolvedConfig @@ -260,6 +271,14 @@ export interface WorkerContext { invalidates?: string[] } +export interface VitestContext { + config: ResolvedConfig + server: ViteDevServer + state: StateManager + snapshot: SnapshotManager + reporter: Reporter +} + export interface RpcMap { workerReady: [[], void] fetch: [[id: string], TransformResult | null | undefined] @@ -277,11 +296,3 @@ export type RpcCall = (method: T, ...args: RpcMap[T][0]) export type RpcSend = (method: T, ...args: RpcMap[T][0]) => void export type RpcPayload = { id: string; method: T; args: RpcMap[T][0] } - -export interface VitestContext { - config: ResolvedConfig - server: ViteDevServer - state: StateManager - snapshot: SnapshotManager - reporter: Reporter -} diff --git a/test/lit/vite.config.ts b/test/lit/vite.config.ts index 4cfaaf585..14cc71b11 100644 --- a/test/lit/vite.config.ts +++ b/test/lit/vite.config.ts @@ -6,6 +6,6 @@ import { defineConfig } from 'vite' export default defineConfig({ test: { global: true, - dom: 'happy-dom', + environment: 'happy-dom', }, }) diff --git a/test/react/vitest.config.ts b/test/react/vitest.config.ts index 39a08a056..1a64f36fb 100644 --- a/test/react/vitest.config.ts +++ b/test/react/vitest.config.ts @@ -5,6 +5,6 @@ import { defineConfig } from 'vite' export default defineConfig({ test: { global: true, - dom: 'happy-dom', + environment: 'happy-dom', }, }) diff --git a/test/svelte/vitest.config.ts b/test/svelte/vitest.config.ts index b2bcdbb65..30af7bb10 100644 --- a/test/svelte/vitest.config.ts +++ b/test/svelte/vitest.config.ts @@ -7,6 +7,6 @@ export default defineConfig({ ], test: { global: true, - dom: 'jsdom', + environment: 'jsdom', }, }) diff --git a/test/testing-lib-react/vite.config.ts b/test/testing-lib-react/vite.config.ts index 24324e67d..2c71d148d 100644 --- a/test/testing-lib-react/vite.config.ts +++ b/test/testing-lib-react/vite.config.ts @@ -9,6 +9,6 @@ export default defineConfig({ plugins: [react()], test: { global: true, - dom: 'happy-dom', + environment: 'happy-dom', }, }) diff --git a/test/vitesse/vite.config.ts b/test/vitesse/vite.config.ts index ecf1bc65f..3a383ec42 100644 --- a/test/vitesse/vite.config.ts +++ b/test/vitesse/vite.config.ts @@ -20,6 +20,6 @@ export default defineConfig({ ], test: { global: true, - dom: 'happy-dom', + environment: 'happy-dom', }, }) diff --git a/test/vue/vitest.config.ts b/test/vue/vitest.config.ts index 0e0952135..e08cb402e 100644 --- a/test/vue/vitest.config.ts +++ b/test/vue/vitest.config.ts @@ -7,6 +7,6 @@ export default defineConfig({ ], test: { global: true, - dom: 'jsdom', + environment: 'happy-dom', }, })