From b1ce21d161afb02e4fa476ecd5265da1e4e630d6 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:51:50 -0500 Subject: [PATCH] fix(@angular/build): dynamically select Vitest DOM environment This change enhances the Vitest unit test builder to intelligently select the default DOM test environment (`jsdom` or `happy-dom`) based on which package is installed in the user's project. Previously, the builder strictly defaulted to `jsdom`. With this update: - The `validateDependencies` function now checks for the presence of either `jsdom` or `happy-dom` when browser-based tests are not configured, providing a more flexible dependency requirement. - A new `findTestEnvironment` helper function is introduced to detect the available DOM environment (`happy-dom` is preferred if installed, otherwise `jsdom`). - The `createVitestConfigPlugin` now uses this helper to dynamically set the `environment` in the Vitest configuration, ensuring a smart default if the user has not explicitly specified one. This improves the out-of-the-box experience by adapting to common project setups and offering more choice in DOM emulation. --- .../unit-test/runners/vitest/executor.ts | 1 + .../unit-test/runners/vitest/index.ts | 7 ++-- .../unit-test/runners/vitest/plugins.ts | 33 +++++++++++++++++-- 3 files changed, 36 insertions(+), 5 deletions(-) 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,