From 2f8a95fb0b190926203a48966c2a59069c5b8bf2 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 24 Apr 2025 10:53:15 -0400 Subject: [PATCH] feat(@angular/build): add experimental vitest browser support to unit-testing The experimental `unit-test` builder now allows for enabling the experimental vitest browser testing support. A `browsers` option is now available that can list one or more browsers to use for test execution. If the `browsers` option is not specified (the default), then `jsdom` will be used to execute the tests without a browser. To use the browser support, either `playwright` or `webdriverio` must be installed within the project. On startup, the testing process will automatically attempt to discover the browser provider. The browser names present in the `browsers` option must be specified based on the installed provider. Each may have differing names for some browsers. --- .../build/src/builders/unit-test/builder.ts | 45 ++++++++++++++++++- .../build/src/builders/unit-test/options.ts | 3 +- .../build/src/builders/unit-test/schema.json | 8 ++++ 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index 42fa785bd739..fcf1f837a658 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -9,6 +9,7 @@ import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect'; import assert from 'node:assert'; import { randomUUID } from 'node:crypto'; +import { createRequire } from 'node:module'; import path from 'node:path'; import { createVirtualModulePlugin } from '../../tools/esbuild/virtual-module-plugin'; import { loadEsmModule } from '../../utils/load-esm'; @@ -133,6 +134,28 @@ export async function* execute( let instance: import('vitest/node').Vitest | undefined; + // Setup vitest browser options if configured + let browser: import('vitest/node').BrowserConfigOptions | undefined; + if (normalizedOptions.browsers) { + const provider = findBrowserProvider(projectSourceRoot); + if (!provider) { + context.logger.error( + 'The "browsers" option requires either "playwright" or "webdriverio" to be installed within the project.' + + ' Please install one of these packages and rerun the test command.', + ); + + return { success: false }; + } + + browser = { + enabled: true, + provider, + instances: normalizedOptions.browsers.map((browserName) => ({ + browser: browserName, + })), + }; + } + for await (const result of buildApplicationInternal(buildOptions, context, extensions)) { if (result.kind === ResultKind.Failure) { continue; @@ -153,8 +176,11 @@ export async function* execute( test: { root: outputPath, setupFiles, - environment: 'jsdom', + // Use `jsdom` if no browsers are explicitly configured. + // `node` is effectively no "environment" and the default. + environment: browser ? 'node' : 'jsdom', watch: normalizedOptions.watch, + browser, coverage: { enabled: normalizedOptions.codeCoverage, exclude: normalizedOptions.codeCoverageExclude, @@ -169,3 +195,20 @@ export async function* execute( yield { success: testModules.every((testModule) => testModule.ok()) }; } } + +function findBrowserProvider( + projectSourceRoot: string, +): import('vitest/node').BrowserBuiltinProvider | undefined { + const projectResolver = createRequire(projectSourceRoot + '/').resolve; + + // These must be installed in the project to be used + const vitestBuiltinProviders = ['playwright', 'webdriverio'] as const; + + for (const providerName of vitestBuiltinProviders) { + try { + projectResolver(providerName); + + return providerName; + } catch {} + } +} diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index 8504fee02540..9290d2e6a4b7 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -36,7 +36,7 @@ export async function normalizeOptions( const buildTargetSpecifier = options.buildTarget ?? `::development`; const buildTarget = targetFromTargetString(buildTargetSpecifier, projectName, 'build'); - const { codeCoverage, codeCoverageExclude, tsConfig, runner, reporters } = options; + const { codeCoverage, codeCoverageExclude, tsConfig, runner, reporters, browsers } = options; return { // Project/workspace information @@ -53,6 +53,7 @@ export async function normalizeOptions( codeCoverageExclude, tsConfig, reporters, + browsers, // TODO: Implement watch support watch: false, }; diff --git a/packages/angular/build/src/builders/unit-test/schema.json b/packages/angular/build/src/builders/unit-test/schema.json index 44f6b7f73371..1455227a3808 100644 --- a/packages/angular/build/src/builders/unit-test/schema.json +++ b/packages/angular/build/src/builders/unit-test/schema.json @@ -18,6 +18,14 @@ "description": "The name of the test runner to use for test execution.", "enum": ["vitest"] }, + "browsers": { + "description": "A list of browsers to use for test execution. If undefined, jsdom on Node.js will be used instead of a browser.", + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1 + }, "include": { "type": "array", "items": {