Skip to content

Commit 8dd341e

Browse files
committed
fix(@angular/build): scope CHROME_BIN executable path to individual playwright instances
Previously, if CHROME_BIN was set in the environment and a user ran tests targeting the Playwright provider, the path was applied to the global Playwright launch options. This caused tests to crash if a user requested non-Chromium browsers (like Firefox) alongside Chromium, because Playwright would incorrectly attempt to launch the Chrome binary for the Firefox instance. This commit updates the browser configuration to map instances before providers are initialized, and selectively injects `launchOptions: { executablePath: process.env.CHROME_BIN }` at the individual instance level for chrome and chromium only. This restores parity where users can maintain CHROME_BIN variables while safely invoking alternative browsers.
1 parent 54c96c8 commit 8dd341e

File tree

2 files changed

+94
-15
lines changed

2 files changed

+94
-15
lines changed

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

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,13 @@ function findBrowserProvider(
3737
return undefined;
3838
}
3939

40-
function normalizeBrowserName(browserName: string): { browser: string; headless: boolean } {
40+
export interface BrowserInstanceConfiguration {
41+
browser: string;
42+
headless: boolean;
43+
provider?: import('vitest/node').BrowserProviderOption;
44+
}
45+
46+
function normalizeBrowserName(browserName: string): BrowserInstanceConfiguration {
4147
// Normalize browser names to match Vitest's expectations for headless but also supports karma's names
4248
// e.g., 'ChromeHeadless' -> 'chrome', 'FirefoxHeadless' -> 'firefox'
4349
// and 'Chrome' -> 'chrome', 'Firefox' -> 'firefox'.
@@ -79,6 +85,8 @@ export async function setupBrowserConfiguration(
7985
);
8086
}
8187

88+
const instances = browsers.map(normalizeBrowserName);
89+
8290
let provider: import('vitest/node').BrowserProviderOption | undefined;
8391
if (providerName) {
8492
const providerPackage = `@vitest/browser-${providerName}`;
@@ -90,17 +98,25 @@ export async function setupBrowserConfiguration(
9098
if (typeof providerFactory === 'function') {
9199
if (providerName === 'playwright') {
92100
const executablePath = process.env['CHROME_BIN'];
93-
provider = providerFactory({
94-
launchOptions: executablePath
95-
? {
96-
executablePath,
97-
}
98-
: undefined,
101+
const baseOptions = {
99102
contextOptions: {
100103
// Enables `prefer-color-scheme` for Vitest browser instead of `light`
101104
colorScheme: null,
102105
},
103-
});
106+
};
107+
108+
provider = providerFactory(baseOptions);
109+
110+
if (executablePath) {
111+
for (const instance of instances) {
112+
if (instance.browser === 'chrome' || instance.browser === 'chromium') {
113+
instance.provider = providerFactory({
114+
...baseOptions,
115+
launchOptions: { executablePath },
116+
});
117+
}
118+
}
119+
}
104120
} else {
105121
provider = providerFactory();
106122
}
@@ -133,7 +149,6 @@ export async function setupBrowserConfiguration(
133149
}
134150

135151
const isCI = !!process.env['CI'];
136-
const instances = browsers.map(normalizeBrowserName);
137152
const messages: string[] = [];
138153

139154
if (providerName === 'preview') {

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

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ describe('setupBrowserConfiguration', () => {
4747

4848
expect(browser?.enabled).toBeTrue();
4949
expect(browser?.instances).toEqual([
50-
{ browser: 'chrome', headless: true },
51-
{ browser: 'firefox', headless: false },
50+
jasmine.objectContaining({ browser: 'chrome', headless: true }),
51+
jasmine.objectContaining({ browser: 'firefox', headless: false }),
5252
]);
5353
});
5454

@@ -66,8 +66,8 @@ describe('setupBrowserConfiguration', () => {
6666
);
6767

6868
expect(browser?.instances).toEqual([
69-
{ browser: 'chrome', headless: true },
70-
{ browser: 'firefox', headless: true },
69+
jasmine.objectContaining({ browser: 'chrome', headless: true }),
70+
jasmine.objectContaining({ browser: 'firefox', headless: true }),
7171
]);
7272
} finally {
7373
if (originalCI === undefined) {
@@ -196,8 +196,8 @@ describe('setupBrowserConfiguration', () => {
196196
);
197197

198198
expect(browser?.instances).toEqual([
199-
{ browser: 'chrome', headless: true },
200-
{ browser: 'firefox', headless: true },
199+
jasmine.objectContaining({ browser: 'chrome', headless: true }),
200+
jasmine.objectContaining({ browser: 'firefox', headless: true }),
201201
]);
202202
expect(messages).toEqual([]);
203203
});
@@ -215,4 +215,68 @@ describe('setupBrowserConfiguration', () => {
215215
'The "headless" option is unnecessary as all browsers are already configured to run in headless mode.',
216216
]);
217217
});
218+
219+
describe('CHROME_BIN usage', () => {
220+
let originalChromeBin: string | undefined;
221+
222+
beforeEach(() => {
223+
originalChromeBin = process.env['CHROME_BIN'];
224+
process.env['CHROME_BIN'] = '/custom/path/to/chrome';
225+
});
226+
227+
afterEach(() => {
228+
if (originalChromeBin === undefined) {
229+
delete process.env['CHROME_BIN'];
230+
} else {
231+
process.env['CHROME_BIN'] = originalChromeBin;
232+
}
233+
});
234+
235+
it('should set executablePath on the individual chrome instance', async () => {
236+
const { browser } = await setupBrowserConfiguration(
237+
['ChromeHeadless', 'Chromium'],
238+
undefined,
239+
false,
240+
workspaceRoot,
241+
undefined,
242+
);
243+
244+
// Verify the global provider does NOT have executablePath
245+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
246+
expect((browser?.provider as any)?.options?.launchOptions?.executablePath).toBeUndefined();
247+
248+
// Verify the individual instances have executablePath
249+
expect(
250+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
251+
(browser?.instances?.[0]?.provider as any)?.options?.launchOptions?.executablePath,
252+
).toBe('/custom/path/to/chrome');
253+
expect(
254+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
255+
(browser?.instances?.[1]?.provider as any)?.options?.launchOptions?.executablePath,
256+
).toBe('/custom/path/to/chrome');
257+
});
258+
259+
it('should set executablePath for chrome instances but not for others when mixed browsers are requested', async () => {
260+
const { browser } = await setupBrowserConfiguration(
261+
['ChromeHeadless', 'Firefox'],
262+
undefined,
263+
false,
264+
workspaceRoot,
265+
undefined,
266+
);
267+
268+
// Verify the global provider does NOT have executablePath
269+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
270+
expect((browser?.provider as any)?.options?.launchOptions?.executablePath).toBeUndefined();
271+
272+
// Verify chrome gets it
273+
expect(
274+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
275+
(browser?.instances?.[0]?.provider as any)?.options?.launchOptions?.executablePath,
276+
).toBe('/custom/path/to/chrome');
277+
278+
// Verify firefox does not
279+
expect(browser?.instances?.[1]?.provider).toBeUndefined();
280+
});
281+
});
218282
});

0 commit comments

Comments
 (0)