diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index a51449319e47..619482e325be 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -231,6 +231,7 @@ export type UnitTestBuilderOptions = { dumpVirtualFiles?: boolean; exclude?: string[]; filter?: string; + headless?: boolean; include?: string[]; listTests?: boolean; outputFile?: string; diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index 7f8f8db182fe..72b5f3eb3e8d 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -96,6 +96,7 @@ export async function normalizeOptions( exclude: options.exclude, filter, runnerName: runner ?? Runner.Vitest, + headless: options.headless, coverage: { enabled: options.coverage, exclude: options.coverageExclude, diff --git a/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts b/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts index 0cee5e11cd6f..66bf203c25c7 100644 --- a/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts +++ b/packages/angular/build/src/builders/unit-test/runners/karma/executor.ts @@ -47,6 +47,12 @@ export class KarmaExecutor implements TestExecutor { ); } + if (unitTestOptions.headless !== undefined) { + context.logger.warn( + 'The "karma" test runner does not support the "headless" option. The option will be ignored.', + ); + } + const buildTargetOptions = (await context.validateOptions( await context.getTargetOptions(unitTestOptions.buildTarget), await context.getBuilderNameForTarget(unitTestOptions.buildTarget), diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts index 3aeeb7afe474..08dd28100d0e 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts @@ -13,6 +13,7 @@ import { assertIsError } from '../../../../utils/error'; export interface BrowserConfiguration { browser?: BrowserConfigOptions; errors?: string[]; + messages?: string[]; } function findBrowserProvider( @@ -51,11 +52,18 @@ function normalizeBrowserName(browserName: string): { browser: string; headless: export async function setupBrowserConfiguration( browsers: string[] | undefined, + headless: boolean | undefined, debug: boolean, projectSourceRoot: string, viewport: { width: number; height: number } | undefined, ): Promise { if (browsers === undefined) { + if (headless !== undefined) { + return { + messages: ['The "headless" option is ignored when no browsers are configured.'], + }; + } + return {}; } @@ -125,10 +133,30 @@ export async function setupBrowserConfiguration( const isCI = !!process.env['CI']; const instances = browsers.map(normalizeBrowserName); + const messages: string[] = []; + if (providerName === 'preview') { instances.forEach((instance) => { + // Preview mode only supports headed execution instance.headless = false; }); + + if (headless) { + messages.push('The "headless" option is ignored when using the "preview" provider.'); + } + } else if (headless !== undefined) { + if (headless) { + const allHeadlessByDefault = isCI || instances.every((i) => i.headless); + if (allHeadlessByDefault) { + messages.push( + 'The "headless" option is unnecessary as all browsers are already configured to run in headless mode.', + ); + } + } + + instances.forEach((instance) => { + instance.headless = headless; + }); } else if (isCI) { instances.forEach((instance) => { instance.headless = true; @@ -143,5 +171,5 @@ export async function setupBrowserConfiguration( instances, } satisfies BrowserConfigOptions; - return { browser }; + return { browser, messages }; } diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts index 2162ffa6ed5e..6f31fc1e7227 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts @@ -39,6 +39,7 @@ describe('setupBrowserConfiguration', () => { it('should configure headless mode for specific browsers based on name', async () => { const { browser } = await setupBrowserConfiguration( ['ChromeHeadless', 'Firefox'], + undefined, false, workspaceRoot, undefined, @@ -58,6 +59,7 @@ describe('setupBrowserConfiguration', () => { try { const { browser } = await setupBrowserConfiguration( ['Chrome', 'FirefoxHeadless'], + undefined, false, workspaceRoot, undefined, @@ -85,6 +87,7 @@ describe('setupBrowserConfiguration', () => { // Case 1: All headless -> UI false let result = await setupBrowserConfiguration( ['ChromeHeadless'], + undefined, false, workspaceRoot, undefined, @@ -94,6 +97,7 @@ describe('setupBrowserConfiguration', () => { // Case 2: Mixed -> UI true result = await setupBrowserConfiguration( ['ChromeHeadless', 'Firefox'], + undefined, false, workspaceRoot, undefined, @@ -113,6 +117,7 @@ describe('setupBrowserConfiguration', () => { try { const { browser } = await setupBrowserConfiguration( ['Chrome'], + undefined, false, workspaceRoot, undefined, @@ -151,6 +156,7 @@ describe('setupBrowserConfiguration', () => { const { browser } = await setupBrowserConfiguration( ['ChromeHeadless'], + undefined, false, workspaceRoot, undefined, @@ -160,4 +166,34 @@ describe('setupBrowserConfiguration', () => { // Preview forces headless false expect(browser?.instances?.[0].headless).toBeFalse(); }); + + it('should force headless mode when headless option is true', async () => { + const { browser, messages } = await setupBrowserConfiguration( + ['Chrome', 'Firefox'], + true, + false, + workspaceRoot, + undefined, + ); + + expect(browser?.instances).toEqual([ + { browser: 'chrome', headless: true }, + { browser: 'firefox', headless: true }, + ]); + expect(messages).toEqual([]); + }); + + it('should return information message when headless option is redundant', async () => { + const { messages } = await setupBrowserConfiguration( + ['ChromeHeadless'], + true, + false, + workspaceRoot, + undefined, + ); + + expect(messages).toEqual([ + 'The "headless" option is unnecessary as all browsers are already configured to run in headless mode.', + ]); + }); }); 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 39584a5844f0..b800ef3f7866 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 @@ -192,6 +192,7 @@ export class VitestExecutor implements TestExecutor { // Setup vitest browser options if configured const browserOptions = await setupBrowserConfiguration( browsers, + this.options.headless, debug, projectSourceRoot, browserViewport, @@ -200,6 +201,12 @@ export class VitestExecutor implements TestExecutor { throw new Error(browserOptions.errors.join('\n')); } + if (browserOptions.messages?.length) { + for (const message of browserOptions.messages) { + this.logger.info(message); + } + } + assert( this.buildResultFiles.size > 0, 'buildResult must be available before initializing vitest', diff --git a/packages/angular/build/src/builders/unit-test/schema.json b/packages/angular/build/src/builders/unit-test/schema.json index 2a3cf27719e8..951fd5a29e73 100644 --- a/packages/angular/build/src/builders/unit-test/schema.json +++ b/packages/angular/build/src/builders/unit-test/schema.json @@ -60,6 +60,10 @@ "type": "boolean", "description": "Enables watch mode, which re-runs tests when source files change. Defaults to `true` in TTY environments and `false` otherwise." }, + "headless": { + "type": "boolean", + "description": "Forces all configured browsers to run in headless mode. When using the Vitest runner, this option is ignored if no browsers are configured. The Karma runner does not support this option." + }, "debug": { "type": "boolean", "description": "Enables debugging mode for tests, allowing the use of the Node Inspector.",