diff --git a/goldens/public-api/angular/build/index.api.md b/goldens/public-api/angular/build/index.api.md index df3f8fe8c777..40d1c4438338 100644 --- a/goldens/public-api/angular/build/index.api.md +++ b/goldens/public-api/angular/build/index.api.md @@ -221,6 +221,7 @@ export type UnitTestBuilderOptions = { browserViewport?: string; browsers?: string[]; buildTarget?: string; + connectOptions?: ConnectOptions; coverage?: boolean; coverageExclude?: string[]; coverageInclude?: string[]; diff --git a/packages/angular/build/src/builders/unit-test/options.ts b/packages/angular/build/src/builders/unit-test/options.ts index d3402b090e0f..98ab0f7733e2 100644 --- a/packages/angular/build/src/builders/unit-test/options.ts +++ b/packages/angular/build/src/builders/unit-test/options.ts @@ -136,6 +136,7 @@ export async function normalizeOptions( outputFile: options.outputFile, browsers: browsers?.length ? browsers : undefined, browserViewport: width && height ? { width, height } : undefined, + connectOptions: options.connectOptions, watch, debug: options.debug ?? false, ui: process.env['CI'] ? false : ui, 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 a8c33f9a79ac..7242b1cae461 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 type { } from 'vitest/node'; import { assertIsError } from '../../../../utils/error'; import { createProjectResolver } from '../../../../utils/resolve-project'; +import type { NormalizedUnitTestBuilderOptions } from '../../options'; export interface BrowserConfiguration { browser?: BrowserConfigOptions; @@ -119,6 +120,7 @@ export function applyHeadlessConfiguration( * @param debug Whether the builder is running in watch or debug mode. * @param projectSourceRoot The root directory of the project being tested for resolving installed packages. * @param viewport Optional viewport dimensions to apply to the launched browser instances. + * @param connectOptions Optional Playwright connection options (e.g. `wsEndpoint`) forwarded to the provider. * @returns A fully resolved Vitest browser configuration object alongside any generated warning or error messages. */ export async function setupBrowserConfiguration( @@ -127,15 +129,18 @@ export async function setupBrowserConfiguration( debug: boolean, projectSourceRoot: string, viewport: { width: number; height: number } | undefined, + connectOptions?: NormalizedUnitTestBuilderOptions['connectOptions'], ): Promise { if (browsers === undefined) { + const messages: string[] = []; if (headless !== undefined) { - return { - messages: ['The "headless" option is ignored when no browsers are configured.'], - }; + messages.push('The "headless" option is ignored when no browsers are configured.'); + } + if (connectOptions) { + messages.push('The "connectOptions" option is ignored when no browsers are configured.'); } - return {}; + return messages.length > 0 ? { messages } : {}; } const projectResolver = createProjectResolver(projectSourceRoot); @@ -168,6 +173,9 @@ export async function setupBrowserConfiguration( // Enables `prefer-color-scheme` for Vitest browser instead of `light` colorScheme: null, }, + // Forward user-provided Playwright connection options (e.g. a custom `wsEndpoint`) + // so tests can run against a remote or shared browser server. + ...(connectOptions ? { connectOptions } : {}), }; provider = providerFactory(baseOptions); @@ -216,6 +224,12 @@ export async function setupBrowserConfiguration( const isCI = !!process.env['CI']; const messages = applyHeadlessConfiguration(instances, providerName, headless, isCI); + if (connectOptions && providerName !== 'playwright') { + messages.push( + 'The "connectOptions" option is only supported by the Playwright browser provider and will be ignored.', + ); + } + const browser = { enabled: true, provider, 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 0dd0778420bd..7073a00514b6 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 @@ -154,6 +154,87 @@ describe('setupBrowserConfiguration', () => { ); }); + describe('connectOptions', () => { + it('should forward connectOptions to the Playwright provider', async () => { + const connectOptions = { + wsEndpoint: 'ws://localhost:1234/playwright', + exposeNetwork: '*', + }; + + const { browser } = await setupBrowserConfiguration( + ['chromium'], + undefined, + false, + workspaceRoot, + undefined, + connectOptions, + ); + + expect(browser?.provider?.options).toEqual(jasmine.objectContaining({ connectOptions })); + }); + + it('should not set connectOptions when none are provided', async () => { + const { browser } = await setupBrowserConfiguration( + ['chromium'], + undefined, + false, + workspaceRoot, + undefined, + ); + + expect(browser?.provider?.options).not.toEqual( + jasmine.objectContaining({ connectOptions: jasmine.anything() }), + ); + }); + + it('should ignore connectOptions and warn when no browsers are configured', async () => { + const { browser, messages } = await setupBrowserConfiguration( + undefined, + undefined, + false, + workspaceRoot, + undefined, + { wsEndpoint: 'ws://localhost:1234/playwright' }, + ); + + expect(browser).toBeUndefined(); + expect(messages).toEqual([ + 'The "connectOptions" option is ignored when no browsers are configured.', + ]); + }); + + it('should warn when connectOptions is used with a non-Playwright provider', async () => { + // Swap the Playwright mock for a WebdriverIO mock so a non-Playwright provider is resolved. + await rm(join(workspaceRoot, 'node_modules/@vitest/browser-playwright'), { + recursive: true, + force: true, + }); + const wdioPkgPath = join(workspaceRoot, 'node_modules/@vitest/browser-webdriverio'); + await mkdir(wdioPkgPath, { recursive: true }); + await writeFile( + join(wdioPkgPath, 'package.json'), + JSON.stringify({ name: '@vitest/browser-webdriverio', main: 'index.js' }), + ); + await writeFile( + join(wdioPkgPath, 'index.js'), + 'module.exports = { webdriverio: (options) => ({ name: "webdriverio", options }) };', + ); + + const { messages } = await setupBrowserConfiguration( + ['chrome'], + undefined, + false, + workspaceRoot, + undefined, + { wsEndpoint: 'ws://localhost:1234/playwright' }, + ); + + expect(messages).toContain( + 'The "connectOptions" option is only supported by the Playwright browser provider and will be ignored.', + ); + }); + }); + it('should support Preview provider forcing headless false', async () => { // Create mock preview package const previewPkgPath = join(workspaceRoot, 'node_modules/@vitest/browser-preview'); @@ -278,6 +359,28 @@ describe('setupBrowserConfiguration', () => { // Verify firefox does not expect(browser?.instances?.[1]?.provider).toBeUndefined(); }); + + it('should forward connectOptions alongside executablePath on per-instance providers', async () => { + const connectOptions = { wsEndpoint: 'ws://localhost:1234/playwright' }; + const { browser } = await setupBrowserConfiguration( + ['ChromeHeadless'], + undefined, + false, + workspaceRoot, + undefined, + connectOptions, + ); + + // The per-instance provider should carry both the connect options and the executable path. + expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (browser?.instances?.[0]?.provider as any)?.options?.connectOptions, + ).toEqual(connectOptions); + expect( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (browser?.instances?.[0]?.provider as any)?.options?.launchOptions?.executablePath, + ).toBe('/custom/path/to/chrome'); + }); }); describe('applyHeadlessConfiguration', () => { 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 9d3760322ecb..3b1f718ef8ac 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 @@ -272,6 +272,7 @@ export class VitestExecutor implements TestExecutor { debug, projectSourceRoot, browserViewport, + this.options.connectOptions, ); if (browserOptions.errors?.length) { this.debugLog(DebugLogLevel.Info, 'Browser configuration errors found.', { diff --git a/packages/angular/build/src/builders/unit-test/schema.json b/packages/angular/build/src/builders/unit-test/schema.json index 0c6c819392f3..7e7b6791a24e 100644 --- a/packages/angular/build/src/builders/unit-test/schema.json +++ b/packages/angular/build/src/builders/unit-test/schema.json @@ -37,6 +37,38 @@ "type": "string", "pattern": "^\\d+x\\d+$" }, + "connectOptions": { + "type": "object", + "description": "Options for connecting to an existing, remote, or preconfigured Playwright browser server. These are passed directly to the Playwright browser provider's `connectOptions`. This only applies when running browser-based tests with the Playwright provider and is useful for running tests against a consistent, shared browser environment (e.g., for stable visual regression testing across different operating systems).", + "properties": { + "wsEndpoint": { + "type": "string", + "description": "A browser WebSocket endpoint to connect to.", + "minLength": 1 + }, + "exposeNetwork": { + "type": "string", + "description": "A rule to expose the network available on the connecting client to the browser being connected to. For example, `*` exposes all addresses, or a comma-separated list of rules can be provided." + }, + "headers": { + "type": "object", + "description": "Additional HTTP headers to be sent with the connection request.", + "additionalProperties": { + "type": "string" + } + }, + "timeout": { + "type": "number", + "description": "Maximum time in milliseconds to wait for the connection to be established. Defaults to `0` (no timeout)." + }, + "slowMo": { + "type": "number", + "description": "Slows down Playwright operations by the specified number of milliseconds." + } + }, + "required": ["wsEndpoint"], + "additionalProperties": true + }, "include": { "type": "array", "items": {