Skip to content

Commit 69450f7

Browse files
committed
feat(@angular/build): support custom Playwright connectOptions for browser unit tests
When the `browsers` option is used, the Vitest unit-test builder constructs its own Playwright browser provider. This overrode any `connectOptions` (such as a custom `wsEndpoint`) defined in a user's Vitest configuration file, making it impossible to run browser-based unit tests against a remote or shared Playwright browser server. A new `connectOptions` builder option is now forwarded to the Playwright provider's `connectOptions`, allowing a custom `wsEndpoint` (and other Playwright connection settings) to be configured directly from `angular.json`. The option is ignored, with an informational message, when no browsers are configured or a non-Playwright provider is used. Closes #33115
1 parent 10dc30f commit 69450f7

6 files changed

Lines changed: 156 additions & 4 deletions

File tree

goldens/public-api/angular/build/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ export type UnitTestBuilderOptions = {
221221
browserViewport?: string;
222222
browsers?: string[];
223223
buildTarget?: string;
224+
connectOptions?: ConnectOptions;
224225
coverage?: boolean;
225226
coverageExclude?: string[];
226227
coverageInclude?: string[];

packages/angular/build/src/builders/unit-test/options.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ export async function normalizeOptions(
136136
outputFile: options.outputFile,
137137
browsers: browsers?.length ? browsers : undefined,
138138
browserViewport: width && height ? { width, height } : undefined,
139+
connectOptions: options.connectOptions,
139140
watch,
140141
debug: options.debug ?? false,
141142
ui: process.env['CI'] ? false : ui,

packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
} from 'vitest/node';
1414
import { assertIsError } from '../../../../utils/error';
1515
import { createProjectResolver } from '../../../../utils/resolve-project';
16+
import type { NormalizedUnitTestBuilderOptions } from '../../options';
1617

1718
export interface BrowserConfiguration {
1819
browser?: BrowserConfigOptions;
@@ -119,6 +120,7 @@ export function applyHeadlessConfiguration(
119120
* @param debug Whether the builder is running in watch or debug mode.
120121
* @param projectSourceRoot The root directory of the project being tested for resolving installed packages.
121122
* @param viewport Optional viewport dimensions to apply to the launched browser instances.
123+
* @param connectOptions Optional Playwright connection options (e.g. `wsEndpoint`) forwarded to the provider.
122124
* @returns A fully resolved Vitest browser configuration object alongside any generated warning or error messages.
123125
*/
124126
export async function setupBrowserConfiguration(
@@ -127,15 +129,18 @@ export async function setupBrowserConfiguration(
127129
debug: boolean,
128130
projectSourceRoot: string,
129131
viewport: { width: number; height: number } | undefined,
132+
connectOptions?: NormalizedUnitTestBuilderOptions['connectOptions'],
130133
): Promise<BrowserConfiguration> {
131134
if (browsers === undefined) {
135+
const messages: string[] = [];
132136
if (headless !== undefined) {
133-
return {
134-
messages: ['The "headless" option is ignored when no browsers are configured.'],
135-
};
137+
messages.push('The "headless" option is ignored when no browsers are configured.');
138+
}
139+
if (connectOptions) {
140+
messages.push('The "connectOptions" option is ignored when no browsers are configured.');
136141
}
137142

138-
return {};
143+
return messages.length > 0 ? { messages } : {};
139144
}
140145

141146
const projectResolver = createProjectResolver(projectSourceRoot);
@@ -168,6 +173,9 @@ export async function setupBrowserConfiguration(
168173
// Enables `prefer-color-scheme` for Vitest browser instead of `light`
169174
colorScheme: null,
170175
},
176+
// Forward user-provided Playwright connection options (e.g. a custom `wsEndpoint`)
177+
// so tests can run against a remote or shared browser server.
178+
...(connectOptions ? { connectOptions } : {}),
171179
};
172180

173181
provider = providerFactory(baseOptions);
@@ -216,6 +224,12 @@ export async function setupBrowserConfiguration(
216224
const isCI = !!process.env['CI'];
217225
const messages = applyHeadlessConfiguration(instances, providerName, headless, isCI);
218226

227+
if (connectOptions && providerName !== 'playwright') {
228+
messages.push(
229+
'The "connectOptions" option is only supported by the Playwright browser provider and will be ignored.',
230+
);
231+
}
232+
219233
const browser = {
220234
enabled: true,
221235
provider,

packages/angular/build/src/builders/unit-test/runners/vitest/browser-provider_spec.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,87 @@ describe('setupBrowserConfiguration', () => {
154154
);
155155
});
156156

157+
describe('connectOptions', () => {
158+
it('should forward connectOptions to the Playwright provider', async () => {
159+
const connectOptions = {
160+
wsEndpoint: 'ws://localhost:1234/playwright',
161+
exposeNetwork: '*',
162+
};
163+
164+
const { browser } = await setupBrowserConfiguration(
165+
['chromium'],
166+
undefined,
167+
false,
168+
workspaceRoot,
169+
undefined,
170+
connectOptions,
171+
);
172+
173+
expect(browser?.provider?.options).toEqual(jasmine.objectContaining({ connectOptions }));
174+
});
175+
176+
it('should not set connectOptions when none are provided', async () => {
177+
const { browser } = await setupBrowserConfiguration(
178+
['chromium'],
179+
undefined,
180+
false,
181+
workspaceRoot,
182+
undefined,
183+
);
184+
185+
expect(browser?.provider?.options).not.toEqual(
186+
jasmine.objectContaining({ connectOptions: jasmine.anything() }),
187+
);
188+
});
189+
190+
it('should ignore connectOptions and warn when no browsers are configured', async () => {
191+
const { browser, messages } = await setupBrowserConfiguration(
192+
undefined,
193+
undefined,
194+
false,
195+
workspaceRoot,
196+
undefined,
197+
{ wsEndpoint: 'ws://localhost:1234/playwright' },
198+
);
199+
200+
expect(browser).toBeUndefined();
201+
expect(messages).toEqual([
202+
'The "connectOptions" option is ignored when no browsers are configured.',
203+
]);
204+
});
205+
206+
it('should warn when connectOptions is used with a non-Playwright provider', async () => {
207+
// Swap the Playwright mock for a WebdriverIO mock so a non-Playwright provider is resolved.
208+
await rm(join(workspaceRoot, 'node_modules/@vitest/browser-playwright'), {
209+
recursive: true,
210+
force: true,
211+
});
212+
const wdioPkgPath = join(workspaceRoot, 'node_modules/@vitest/browser-webdriverio');
213+
await mkdir(wdioPkgPath, { recursive: true });
214+
await writeFile(
215+
join(wdioPkgPath, 'package.json'),
216+
JSON.stringify({ name: '@vitest/browser-webdriverio', main: 'index.js' }),
217+
);
218+
await writeFile(
219+
join(wdioPkgPath, 'index.js'),
220+
'module.exports = { webdriverio: (options) => ({ name: "webdriverio", options }) };',
221+
);
222+
223+
const { messages } = await setupBrowserConfiguration(
224+
['chrome'],
225+
undefined,
226+
false,
227+
workspaceRoot,
228+
undefined,
229+
{ wsEndpoint: 'ws://localhost:1234/playwright' },
230+
);
231+
232+
expect(messages).toContain(
233+
'The "connectOptions" option is only supported by the Playwright browser provider and will be ignored.',
234+
);
235+
});
236+
});
237+
157238
it('should support Preview provider forcing headless false', async () => {
158239
// Create mock preview package
159240
const previewPkgPath = join(workspaceRoot, 'node_modules/@vitest/browser-preview');
@@ -278,6 +359,28 @@ describe('setupBrowserConfiguration', () => {
278359
// Verify firefox does not
279360
expect(browser?.instances?.[1]?.provider).toBeUndefined();
280361
});
362+
363+
it('should forward connectOptions alongside executablePath on per-instance providers', async () => {
364+
const connectOptions = { wsEndpoint: 'ws://localhost:1234/playwright' };
365+
const { browser } = await setupBrowserConfiguration(
366+
['ChromeHeadless'],
367+
undefined,
368+
false,
369+
workspaceRoot,
370+
undefined,
371+
connectOptions,
372+
);
373+
374+
// The per-instance provider should carry both the connect options and the executable path.
375+
expect(
376+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
377+
(browser?.instances?.[0]?.provider as any)?.options?.connectOptions,
378+
).toEqual(connectOptions);
379+
expect(
380+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
381+
(browser?.instances?.[0]?.provider as any)?.options?.launchOptions?.executablePath,
382+
).toBe('/custom/path/to/chrome');
383+
});
281384
});
282385

283386
describe('applyHeadlessConfiguration', () => {

packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ export class VitestExecutor implements TestExecutor {
272272
debug,
273273
projectSourceRoot,
274274
browserViewport,
275+
this.options.connectOptions,
275276
);
276277
if (browserOptions.errors?.length) {
277278
this.debugLog(DebugLogLevel.Info, 'Browser configuration errors found.', {

packages/angular/build/src/builders/unit-test/schema.json

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,38 @@
3737
"type": "string",
3838
"pattern": "^\\d+x\\d+$"
3939
},
40+
"connectOptions": {
41+
"type": "object",
42+
"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).",
43+
"properties": {
44+
"wsEndpoint": {
45+
"type": "string",
46+
"description": "A browser WebSocket endpoint to connect to.",
47+
"minLength": 1
48+
},
49+
"exposeNetwork": {
50+
"type": "string",
51+
"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."
52+
},
53+
"headers": {
54+
"type": "object",
55+
"description": "Additional HTTP headers to be sent with the connection request.",
56+
"additionalProperties": {
57+
"type": "string"
58+
}
59+
},
60+
"timeout": {
61+
"type": "number",
62+
"description": "Maximum time in milliseconds to wait for the connection to be established. Defaults to `0` (no timeout)."
63+
},
64+
"slowMo": {
65+
"type": "number",
66+
"description": "Slows down Playwright operations by the specified number of milliseconds."
67+
}
68+
},
69+
"required": ["wsEndpoint"],
70+
"additionalProperties": true
71+
},
4072
"include": {
4173
"type": "array",
4274
"items": {

0 commit comments

Comments
 (0)