diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts index 3f9640142e37..950a96f2adac 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts @@ -219,6 +219,7 @@ export class VitestExecutor implements TestExecutor { browser: browserOptions.browser, coverage, projectName, + projectSourceRoot: this.options.projectSourceRoot, reporters, setupFiles: testSetupFiles, projectPlugins, diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/index.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/index.ts index 6ff67a56563c..fed814bdd78e 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/index.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/index.ts @@ -33,8 +33,11 @@ const VitestTestRunner: TestRunner = { ); } } else { - // JSDOM is used when no browsers are specified - checker.check('jsdom'); + // DOM emulation is used when no browsers are specified + checker.checkAny( + ['jsdom', 'happy-dom'], + 'A DOM environment is required for non-browser tests. Please install either "jsdom" or "happy-dom".', + ); } if (options.coverage.enabled) { diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts index 22f3eeb77922..9772b6294089 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/plugins.ts @@ -8,6 +8,7 @@ import assert from 'node:assert'; import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; import path from 'node:path'; import type { BrowserConfigOptions, @@ -36,20 +37,44 @@ interface VitestConfigPluginOptions { browser: BrowserConfigOptions | undefined; coverage: NormalizedUnitTestBuilderOptions['coverage']; projectName: string; + projectSourceRoot: string; reporters?: string[] | [string, object][]; setupFiles: string[]; projectPlugins: VitestPlugins; include: string[]; } +async function findTestEnvironment( + projectResolver: NodeJS.RequireResolve, +): Promise<'jsdom' | 'happy-dom'> { + try { + projectResolver('happy-dom'); + + return 'happy-dom'; + } catch { + // happy-dom is not installed, fallback to jsdom + return 'jsdom'; + } +} + export function createVitestConfigPlugin(options: VitestConfigPluginOptions): VitestPlugins[0] { - const { include, browser, projectName, reporters, setupFiles, projectPlugins } = options; + const { + include, + browser, + projectName, + reporters, + setupFiles, + projectPlugins, + projectSourceRoot, + } = options; return { name: 'angular:vitest-configuration', async config(config) { const testConfig = config.test; + const projectResolver = createRequire(projectSourceRoot + '/').resolve; + const projectConfig: UserWorkspaceConfig = { test: { ...testConfig, @@ -58,8 +83,10 @@ export function createVitestConfigPlugin(options: VitestConfigPluginOptions): Vi include, globals: testConfig?.globals ?? true, ...(browser ? { browser } : {}), - // If the user has not specified an environment, use `jsdom`. - ...(!testConfig?.environment ? { environment: 'jsdom' } : {}), + // If the user has not specified an environment, use a smart default. + ...(!testConfig?.environment + ? { environment: await findTestEnvironment(projectResolver) } + : {}), }, optimizeDeps: { noDiscovery: true,