From c238b66e1f0cd0cde2c97fe91d49bd36000d81e6 Mon Sep 17 00:00:00 2001 From: Cacie Prins Date: Thu, 9 Oct 2025 15:14:43 -0400 Subject: [PATCH 1/4] chore: tests for launcher/launch, refactor process spawning for readability --- packages/launcher/index.ts | 2 +- packages/launcher/lib/browsers.ts | 54 ----- .../lib/{darwin => darwinHelpers}/index.ts | 0 .../lib/{darwin => darwinHelpers}/util.ts | 0 packages/launcher/lib/detect.ts | 2 +- packages/launcher/lib/launch.ts | 28 +++ packages/launcher/lib/platforms/Darwin.ts | 29 +++ packages/launcher/lib/platforms/Linux.ts | 4 + packages/launcher/lib/platforms/Platform.ts | 46 ++++ .../launcher/lib/platforms/PlatformFactory.ts | 20 ++ packages/launcher/lib/platforms/Windows.ts | 4 + packages/launcher/test/unit/darwin.spec.ts | 208 ------------------ packages/launcher/test/unit/detect.spec.ts | 6 +- packages/launcher/test/unit/launch.spec.ts | 136 ++++++++++++ packages/server/lib/browsers/chrome.ts | 2 +- packages/types/src/browser.ts | 2 +- 16 files changed, 274 insertions(+), 269 deletions(-) delete mode 100644 packages/launcher/lib/browsers.ts rename packages/launcher/lib/{darwin => darwinHelpers}/index.ts (100%) rename packages/launcher/lib/{darwin => darwinHelpers}/util.ts (100%) create mode 100644 packages/launcher/lib/launch.ts create mode 100644 packages/launcher/lib/platforms/Darwin.ts create mode 100644 packages/launcher/lib/platforms/Linux.ts create mode 100644 packages/launcher/lib/platforms/Platform.ts create mode 100644 packages/launcher/lib/platforms/PlatformFactory.ts create mode 100644 packages/launcher/lib/platforms/Windows.ts delete mode 100644 packages/launcher/test/unit/darwin.spec.ts create mode 100644 packages/launcher/test/unit/launch.spec.ts diff --git a/packages/launcher/index.ts b/packages/launcher/index.ts index 2a98e6ff42f..bd18c4fefac 100644 --- a/packages/launcher/index.ts +++ b/packages/launcher/index.ts @@ -1,6 +1,6 @@ import { detect, detectByPath } from './lib/detect' -import { launch } from './lib/browsers' +import { launch } from './lib/launch' export { detect, diff --git a/packages/launcher/lib/browsers.ts b/packages/launcher/lib/browsers.ts deleted file mode 100644 index 3a53865e3ae..00000000000 --- a/packages/launcher/lib/browsers.ts +++ /dev/null @@ -1,54 +0,0 @@ -import Debug from 'debug' -import type * as cp from 'child_process' -import { utils } from './utils' -import type { FoundBrowser } from '@packages/types' -import type { Readable } from 'stream' - -export const debug = Debug('cypress:launcher:browsers') - -/** starts a found browser and opens URL if given one */ -export type LaunchedBrowser = cp.ChildProcessByStdio - -// NOTE: For Firefox, geckodriver is used to launch the browser -export function launch ( - browser: FoundBrowser, - url: string, - debuggingPort: number, - args: string[] = [], - browserEnv = {}, -) { - debug('launching browser %o', { browser, url }) - - if (!browser.path) { - throw new Error(`Browser ${browser.name} is missing path`) - } - - if (url) { - args = [url].concat(args) - } - - const spawnOpts: cp.SpawnOptionsWithStdioTuple = { - stdio: ['ignore', 'pipe', 'pipe'], - // allow setting default env vars - // but only if it's not already set by the environment - env: { ...browserEnv, ...process.env }, - } - - debug('spawning browser with opts %o', { browser, url, spawnOpts }) - - const proc = utils.spawnWithArch(browser.path, args, spawnOpts) - - proc.stdout.on('data', (buf) => { - debug('%s stdout: %s', browser.name, String(buf).trim()) - }) - - proc.stderr.on('data', (buf) => { - debug('%s stderr: %s', browser.name, String(buf).trim()) - }) - - proc.on('exit', (code, signal) => { - debug('%s exited: %o', browser.name, { code, signal }) - }) - - return proc -} diff --git a/packages/launcher/lib/darwin/index.ts b/packages/launcher/lib/darwinHelpers/index.ts similarity index 100% rename from packages/launcher/lib/darwin/index.ts rename to packages/launcher/lib/darwinHelpers/index.ts diff --git a/packages/launcher/lib/darwin/util.ts b/packages/launcher/lib/darwinHelpers/util.ts similarity index 100% rename from packages/launcher/lib/darwin/util.ts rename to packages/launcher/lib/darwinHelpers/util.ts diff --git a/packages/launcher/lib/detect.ts b/packages/launcher/lib/detect.ts index cba982d644f..734fd80b3d1 100644 --- a/packages/launcher/lib/detect.ts +++ b/packages/launcher/lib/detect.ts @@ -3,7 +3,7 @@ import _, { compact, extend, find } from 'lodash' import os from 'os' import { removeDuplicateBrowsers } from '@packages/data-context/src/sources/BrowserDataSource' import { knownBrowsers } from './known-browsers' -import * as darwinHelper from './darwin' +import * as darwinHelper from './darwinHelpers' import { notDetectedAtPathErr } from './errors' import * as linuxHelper from './linux' import Debug from 'debug' diff --git a/packages/launcher/lib/launch.ts b/packages/launcher/lib/launch.ts new file mode 100644 index 00000000000..3d4c33db97b --- /dev/null +++ b/packages/launcher/lib/launch.ts @@ -0,0 +1,28 @@ +import Debug from 'debug' +import type * as cp from 'child_process' +import type { FoundBrowser } from '@packages/types' +import type { Readable } from 'stream' +import { PlatformFactory } from './platforms/PlatformFactory' + +export const debug = Debug('cypress:launcher:browsers') + +/** starts a found browser and opens URL if given one */ +export type LaunchedBrowser = cp.ChildProcessByStdio + +// NOTE: For Firefox, geckodriver is used to launch the browser +export function launch ( + browser: FoundBrowser, + url: string, + args: string[] = [], + browserEnv = {}, +) { + debug('launching browser %o', { browser, url }) + + // We shouldn't need to check this, because FoundBrowser.path is + // not optional. + if (!browser.path) { + throw new Error(`Browser ${browser.name} is missing path`) + } + + return PlatformFactory.select().launch(browser, url, args, browserEnv) +} diff --git a/packages/launcher/lib/platforms/Darwin.ts b/packages/launcher/lib/platforms/Darwin.ts new file mode 100644 index 00000000000..0e14f7a020d --- /dev/null +++ b/packages/launcher/lib/platforms/Darwin.ts @@ -0,0 +1,29 @@ +// this file is named XDarwin because intellisense gets confused with '../darwin/' +import { ChildProcess, spawn } from 'child_process' +import { Platform } from './Platform' +import type { FoundBrowser } from '@packages/types' +import os from 'os' + +export class Darwin extends Platform { + launch (browser: FoundBrowser, url: string, args: string[], env: Record = {}): ChildProcess { + if (os.arch() === 'arm64') { + const proc = spawn( + 'arch', + [browser.path, url, ...args], { + ...Platform.defaultSpawnOpts, + env: { + ARCHPREFERENCE: 'arm64,x86_64', + ...Platform.defaultSpawnOpts.env, + ...env, + }, + }, + ) + + this.addDebugListeners(proc, browser) + + return proc + } + + return super.launch(browser, url, args, env) + } +} diff --git a/packages/launcher/lib/platforms/Linux.ts b/packages/launcher/lib/platforms/Linux.ts new file mode 100644 index 00000000000..c476837199e --- /dev/null +++ b/packages/launcher/lib/platforms/Linux.ts @@ -0,0 +1,4 @@ +import { Platform } from './Platform' + +export class Linux extends Platform { +} diff --git a/packages/launcher/lib/platforms/Platform.ts b/packages/launcher/lib/platforms/Platform.ts new file mode 100644 index 00000000000..3e0bb77907c --- /dev/null +++ b/packages/launcher/lib/platforms/Platform.ts @@ -0,0 +1,46 @@ +import type { FoundBrowser } from '@packages/types' +import { ChildProcess, spawn, SpawnOptions } from 'child_process' +import Debug from 'debug' + +export const debug = Debug('cypress:launcher:browsers') + +export abstract class Platform { + launch (browser: FoundBrowser, url: string, args: string[], env: Record = {}): ChildProcess { + debug('launching browser %o', { browser, url, args, env }) + + const proc = spawn(browser.path, [url, ...args], { + ...Platform.defaultSpawnOpts, + env: { + ...Platform.defaultSpawnOpts.env, + ...env, + }, + }) + + this.addDebugListeners(proc, browser) + + return proc + } + + protected addDebugListeners (proc: ChildProcess, browser: FoundBrowser) { + proc.stdout?.on('data', (buf) => { + debug('%s stdout: %s', browser.name, String(buf).trim()) + }) + + proc.stderr?.on('data', (buf) => { + debug('%s stderr: %s', browser.name, String(buf).trim()) + }) + + proc.on('exit', (code, signal) => { + debug('%s exited: %o', browser.name, { code, signal }) + }) + } + + static get defaultSpawnOpts (): SpawnOptions { + return { + stdio: ['ignore', 'pipe', 'pipe'], + env: { + ...process.env, + }, + } + } +} diff --git a/packages/launcher/lib/platforms/PlatformFactory.ts b/packages/launcher/lib/platforms/PlatformFactory.ts new file mode 100644 index 00000000000..6253128d19f --- /dev/null +++ b/packages/launcher/lib/platforms/PlatformFactory.ts @@ -0,0 +1,20 @@ +import os from 'os' +import type { Platform } from './Platform' +import { Darwin } from './XDarwin' +import { Linux } from './Linux' +import { Windows } from './Windows' + +export class PlatformFactory { + static select (): Platform { + switch (os.platform()) { + case 'darwin': + return new Darwin() + case 'linux': + return new Linux() + case 'win32': + return new Windows() + default: + throw new Error(`Unsupported platform: ${os.platform()} ${os.arch()}`) + } + } +} diff --git a/packages/launcher/lib/platforms/Windows.ts b/packages/launcher/lib/platforms/Windows.ts new file mode 100644 index 00000000000..34e3511a413 --- /dev/null +++ b/packages/launcher/lib/platforms/Windows.ts @@ -0,0 +1,4 @@ +import { Platform } from './Platform' + +export class Windows extends Platform { +} diff --git a/packages/launcher/test/unit/darwin.spec.ts b/packages/launcher/test/unit/darwin.spec.ts deleted file mode 100644 index 71d1467cd60..00000000000 --- a/packages/launcher/test/unit/darwin.spec.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest' -import os from 'os' -import cp from 'child_process' -import fs from 'fs-extra' -import { PassThrough } from 'stream' -import { FoundBrowser } from '@packages/types' -import * as darwinHelper from '../../lib/darwin' -import * as linuxHelper from '../../lib/linux' -import * as darwinUtil from '../../lib/darwin/util' -import { launch } from '../../lib/browsers' -import { knownBrowsers } from '../../lib/known-browsers' - -vi.mock('os', async (importActual) => { - const actual = await importActual() - - return { - default: { - // @ts-expect-error - ...actual.default, - arch: vi.fn(), - platform: vi.fn(), - }, - } -}) - -vi.mock('fs-extra', async (importActual) => { - const actual = await importActual() - - return { - default: { - // @ts-expect-error - ...actual.default, - readFile: vi.fn(), - }, - } -}) - -vi.mock('child_process', async (importActual) => { - const actual = await importActual() - - return { - default: { - // @ts-expect-error - ...actual.default, - spawn: vi.fn(), - }, - } -}) - -function generatePlist (key, value) { - return ` - - - - - ${key} - ${value} - - - ` -} - -describe('darwin browser detection', () => { - beforeEach(() => { - vi.unstubAllEnvs() - vi.resetAllMocks() - vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' }) - }) - - it('detects browsers as expected', async () => { - // this test uses the macOS detectors to stub out the expected calls - const flatFindAppParams: darwinUtil.FindAppParams[] = [] - - for (const browser in darwinHelper.browsers) { - for (const channel in darwinHelper.browsers[browser]) { - flatFindAppParams.push(darwinHelper.browsers[browser][channel]) - } - } - - // @ts-expect-error - vi.mocked(fs.readFile).mockImplementation((file: string, _options: any): Promise => { - const foundAppParams = flatFindAppParams.find((findAppParams) => `/Applications/${findAppParams.appName}/Contents/Info.plist` === file) - - if (foundAppParams) { - return Promise.resolve(generatePlist(foundAppParams.versionProperty, 'someVersion')) - } - - throw new Error('File not found') - }) - - const mappedBrowsers = [] - - for (const browser of knownBrowsers) { - const foundBrowser = await darwinHelper.detect(browser) - const findAppParams = darwinHelper.browsers[browser.name][browser.channel] - - mappedBrowsers.push({ - ...browser, - ...foundBrowser, - findAppParams, - }) - } - - expect(mappedBrowsers).toMatchSnapshot() - }) - - it('getVersionString is re-exported from linuxHelper', () => { - expect(darwinHelper.getVersionString).toEqual(linuxHelper.getVersionString) - }) - - describe('forces correct architecture', () => { - beforeEach(() => { - vi.unstubAllEnvs() - vi.stubEnv('env2', 'false') - vi.stubEnv('env3', 'true') - vi.mocked(os.platform).mockReturnValue('darwin') - vi.mocked(cp.spawn).mockImplementation(() => { - const mock: any = { - on: vi.fn(), - once: vi.fn(), - stdout: new PassThrough(), - stderr: new PassThrough(), - kill: vi.fn(), - } - - mock.on.mockImplementation((event: string, callback: (...args: any[]) => void) => { - if (event === 'exit') { - setTimeout(() => callback(), 0) - } - - if (event === 'close') { - setTimeout(() => callback(), 0) - } - }) - - mock.stderr.end() - mock.stdout.end() - - return mock as cp.ChildProcess - }) - }) - - describe('in version detection', () => { - it('uses arch and ARCHPREFERENCE on arm64', async () => { - vi.mocked(os.arch).mockReturnValue('arm64') - - // this will error since we aren't setting stdout - await (darwinHelper.detect(knownBrowsers[0]).catch(() => {})) - - expect(cp.spawn).toHaveBeenNthCalledWith(1, 'arch', [knownBrowsers[0].binary, '--version'], expect.objectContaining({ - env: expect.objectContaining({ - ARCHPREFERENCE: 'arm64,x86_64', - env2: 'false', - env3: 'true', - }), - })) - }) - - it('does not use `arch` on x64', async () => { - vi.mocked(os.arch).mockReturnValue('x64') - - // this will error since we aren't setting stdout - await (darwinHelper.detect(knownBrowsers[0]).catch(() => {})) - - expect(cp.spawn).toHaveBeenNthCalledWith(1, knownBrowsers[0].binary, ['--version'], expect.objectContaining({ - env: expect.objectContaining({ - env2: 'false', - env3: 'true', - }), - })) - }) - }) - - describe('in browser launching', () => { - it('uses arch and ARCHPREFERENCE on arm64', async () => { - vi.mocked(os.arch).mockReturnValue('arm64') - - await launch({ path: 'chrome' } as unknown as FoundBrowser, 'url', 123, ['arg1'], { env1: 'true', env2: 'true' }) - - expect(cp.spawn).toHaveBeenNthCalledWith(1, 'arch', ['chrome', 'url', 'arg1'], expect.objectContaining({ - env: expect.objectContaining({ - ARCHPREFERENCE: 'arm64,x86_64', - env1: 'true', - env2: 'false', - env3: 'true', - }), - })) - }) - - it('does not use `arch` on x64', async () => { - vi.mocked(os.arch).mockReturnValue('x64') - - await launch({ path: 'chrome' } as unknown as FoundBrowser, 'url', 123, ['arg1'], { env1: 'true', env2: 'true' }) - - expect(cp.spawn).toHaveBeenNthCalledWith(1, 'chrome', ['url', 'arg1'], expect.objectContaining({ - env: expect.objectContaining({ - env1: 'true', - env2: 'false', - env3: 'true', - }), - })) - - // @ts-expect-error - expect(cp.spawn.mock.calls[0][2].env).not.toHaveProperty('ARCHPREFERENCE') - }) - }) - }) -}) diff --git a/packages/launcher/test/unit/detect.spec.ts b/packages/launcher/test/unit/detect.spec.ts index 8ef8f44714c..bef8e77628c 100644 --- a/packages/launcher/test/unit/detect.spec.ts +++ b/packages/launcher/test/unit/detect.spec.ts @@ -7,7 +7,7 @@ import { goalBrowsers } from '../fixtures' import os from 'os' import { log } from '../log' import { detect as linuxDetect } from '../../lib/linux' -import { detect as darwinDetect } from '../../lib/darwin' +import { detect as darwinDetect } from '../../lib/darwinHelpers' import { detect as windowsDetect } from '../../lib/windows' import type { Browser } from '@packages/types' @@ -33,7 +33,7 @@ vi.mock('../../lib/linux', async (importActual) => { } }) -vi.mock('../../lib/darwin', async (importActual) => { +vi.mock('../../lib/darwinHelpers', async (importActual) => { const actual = await importActual() return { @@ -63,7 +63,7 @@ describe('detect', () => { vi.resetAllMocks() const { detect: linuxDetectActual } = await vi.importActual('../../lib/linux') - const { detect: darwinDetectActual } = await vi.importActual('../../lib/darwin') + const { detect: darwinDetectActual } = await vi.importActual('../../lib/darwinHelpers') const { detect: windowsDetectActual } = await vi.importActual('../../lib/windows') vi.mocked(linuxDetect).mockImplementation(linuxDetectActual) diff --git a/packages/launcher/test/unit/launch.spec.ts b/packages/launcher/test/unit/launch.spec.ts new file mode 100644 index 00000000000..938ee6ee4f7 --- /dev/null +++ b/packages/launcher/test/unit/launch.spec.ts @@ -0,0 +1,136 @@ +import { describe, it, expect, vi, Mocked } from 'vitest' +import type { FoundBrowser } from '@packages/types' +import { launch } from '../../lib/launch' +import os from 'os' +import { spawn, ChildProcess } from 'child_process' +import EventEmitter from 'events' + +vi.mock('os', async (importActual) => { + const actual: typeof os = await importActual() + + return { + default: { + ...actual, + platform: vi.fn(), + arch: vi.fn(), + }, + } +}) + +vi.mock('child_process', async (importActual) => { + const actual = await importActual() + + return { + // @ts-expect-error + ...actual, + spawn: vi.fn(), + } +}) + +describe('launch', () => { + let browser: FoundBrowser + let url: string + let args: string[] + let browserEnv: Record + let launchedBrowser: Mocked + + let arch: ReturnType + let platform: ReturnType + + beforeEach(() => { + browser = { + name: 'chrome', + version: '100.0.0', + path: 'chrome', + family: 'chromium', + channel: 'stable', + displayName: 'Chrome', + } + + url = 'https://www.somedomain.test' + args = ['--headless'] + browserEnv = {} + + launchedBrowser = { + on: vi.fn() as any, + // these are streams, but we don't need to test + // stream logic - they do need to implement event + // emission though, because of addDebugListeners + // @ts-expect-error + stdout: new EventEmitter(), + // @ts-expect-error + stderr: new EventEmitter(), + kill: vi.fn(), + } + + vi.mocked(os.arch).mockImplementation(() => arch) + vi.mocked(os.platform).mockImplementation(() => platform) + + vi.mocked(spawn).mockReturnValue(launchedBrowser) + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it('throws when browser.path is missing', () => { + browser.path = undefined + + expect(() => launch(browser, url, args, browserEnv)).toThrow('Browser chrome is missing path') + }) + + describe('when darwin arm64', () => { + beforeEach(() => { + arch = 'arm64' + platform = 'darwin' + }) + + it('launches a browser', () => { + const proc = launch(browser, url, args, browserEnv) + + expect(spawn).toHaveBeenCalledWith( + 'arch', + [browser.path, url, ...args], + expect.objectContaining({ + stdio: ['ignore', 'pipe', 'pipe'], + env: expect.objectContaining({ + ...browserEnv, + ARCHPREFERENCE: 'arm64,x86_64', + }), + }), + ) + + expect(proc).toBe(launchedBrowser) + }) + }) + + for (const [testArch, testPlatform] of [ + ['x64', 'darwin'], + ['x64', 'linux'], + ['arm64', 'linux'], + ['x64', 'win32'], + ['arm64', 'win32'], + ]) { + describe(`when ${testPlatform} ${testArch}`, () => { + beforeEach(() => { + arch = testArch as typeof arch + platform = testPlatform as typeof platform + }) + + it('launches a browser', () => { + const proc = launch(browser, url, args, browserEnv) + + expect(spawn).toHaveBeenCalledWith( + browser.path, + [url, ...args], + expect.objectContaining({ + stdio: ['ignore', 'pipe', 'pipe'], + env: expect.any(Object), + }), + ) + + expect(proc).toBe(launchedBrowser) + }) + }) + } +}) diff --git a/packages/server/lib/browsers/chrome.ts b/packages/server/lib/browsers/chrome.ts index 03bd17bd7e9..42a781782ca 100644 --- a/packages/server/lib/browsers/chrome.ts +++ b/packages/server/lib/browsers/chrome.ts @@ -584,7 +584,7 @@ export = { // first allows us to connect the remote interface, // start video recording and then // we will load the actual page - const launchedBrowser = await launch(browser, 'about:blank', port, args, launchOptions.env) as unknown as BrowserInstance & { browserCriClient: BrowserCriClient } + const launchedBrowser = await launch(browser, 'about:blank', args, launchOptions.env) as unknown as BrowserInstance & { browserCriClient: BrowserCriClient } la(launchedBrowser, 'did not get launched browser instance') diff --git a/packages/types/src/browser.ts b/packages/types/src/browser.ts index 7f73d5b1c01..7dcd40dd256 100644 --- a/packages/types/src/browser.ts +++ b/packages/types/src/browser.ts @@ -50,7 +50,7 @@ export type Browser = { /** * Represents a real browser that exists on the user's system. */ -export type FoundBrowser = Omit & { +export interface FoundBrowser extends Omit { path: string version: string majorVersion?: string | null From f8f52fe4e7318a74c26f57ec57a31bd01baeef32 Mon Sep 17 00:00:00 2001 From: Cacie Prins Date: Thu, 9 Oct 2025 15:25:14 -0400 Subject: [PATCH 2/4] how did darwin.spec get lost? oh well --- packages/launcher/test/unit/darwin.spec.ts | 208 +++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 packages/launcher/test/unit/darwin.spec.ts diff --git a/packages/launcher/test/unit/darwin.spec.ts b/packages/launcher/test/unit/darwin.spec.ts new file mode 100644 index 00000000000..e117343f797 --- /dev/null +++ b/packages/launcher/test/unit/darwin.spec.ts @@ -0,0 +1,208 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import os from 'os' +import cp from 'child_process' +import fs from 'fs-extra' +import { PassThrough } from 'stream' +import { FoundBrowser } from '@packages/types' +import * as darwinHelper from '../../lib/darwinHelpers' +import * as linuxHelper from '../../lib/linux' +import * as darwinUtil from '../../lib/darwinHelpers/util' +import { launch } from '../../lib/browsers' +import { knownBrowsers } from '../../lib/known-browsers' + +vi.mock('os', async (importActual) => { + const actual = await importActual() + + return { + default: { + // @ts-expect-error + ...actual.default, + arch: vi.fn(), + platform: vi.fn(), + }, + } +}) + +vi.mock('fs-extra', async (importActual) => { + const actual = await importActual() + + return { + default: { + // @ts-expect-error + ...actual.default, + readFile: vi.fn(), + }, + } +}) + +vi.mock('child_process', async (importActual) => { + const actual = await importActual() + + return { + default: { + // @ts-expect-error + ...actual.default, + spawn: vi.fn(), + }, + } +}) + +function generatePlist (key, value) { + return ` + + + + + ${key} + ${value} + + + ` +} + +describe('darwin browser detection', () => { + beforeEach(() => { + vi.unstubAllEnvs() + vi.resetAllMocks() + vi.mocked(fs.readFile).mockRejectedValue({ code: 'ENOENT' }) + }) + + it('detects browsers as expected', async () => { + // this test uses the macOS detectors to stub out the expected calls + const flatFindAppParams: darwinUtil.FindAppParams[] = [] + + for (const browser in darwinHelper.browsers) { + for (const channel in darwinHelper.browsers[browser]) { + flatFindAppParams.push(darwinHelper.browsers[browser][channel]) + } + } + + // @ts-expect-error + vi.mocked(fs.readFile).mockImplementation((file: string, _options: any): Promise => { + const foundAppParams = flatFindAppParams.find((findAppParams) => `/Applications/${findAppParams.appName}/Contents/Info.plist` === file) + + if (foundAppParams) { + return Promise.resolve(generatePlist(foundAppParams.versionProperty, 'someVersion')) + } + + throw new Error('File not found') + }) + + const mappedBrowsers = [] + + for (const browser of knownBrowsers) { + const foundBrowser = await darwinHelper.detect(browser) + const findAppParams = darwinHelper.browsers[browser.name][browser.channel] + + mappedBrowsers.push({ + ...browser, + ...foundBrowser, + findAppParams, + }) + } + + expect(mappedBrowsers).toMatchSnapshot() + }) + + it('getVersionString is re-exported from linuxHelper', () => { + expect(darwinHelper.getVersionString).toEqual(linuxHelper.getVersionString) + }) + + describe('forces correct architecture', () => { + beforeEach(() => { + vi.unstubAllEnvs() + vi.stubEnv('env2', 'false') + vi.stubEnv('env3', 'true') + vi.mocked(os.platform).mockReturnValue('darwin') + vi.mocked(cp.spawn).mockImplementation(() => { + const mock: any = { + on: vi.fn(), + once: vi.fn(), + stdout: new PassThrough(), + stderr: new PassThrough(), + kill: vi.fn(), + } + + mock.on.mockImplementation((event: string, callback: (...args: any[]) => void) => { + if (event === 'exit') { + setTimeout(() => callback(), 0) + } + + if (event === 'close') { + setTimeout(() => callback(), 0) + } + }) + + mock.stderr.end() + mock.stdout.end() + + return mock as cp.ChildProcess + }) + }) + + describe('in version detection', () => { + it('uses arch and ARCHPREFERENCE on arm64', async () => { + vi.mocked(os.arch).mockReturnValue('arm64') + + // this will error since we aren't setting stdout + await (darwinHelper.detect(knownBrowsers[0]).catch(() => {})) + + expect(cp.spawn).toHaveBeenNthCalledWith(1, 'arch', [knownBrowsers[0].binary, '--version'], expect.objectContaining({ + env: expect.objectContaining({ + ARCHPREFERENCE: 'arm64,x86_64', + env2: 'false', + env3: 'true', + }), + })) + }) + + it('does not use `arch` on x64', async () => { + vi.mocked(os.arch).mockReturnValue('x64') + + // this will error since we aren't setting stdout + await (darwinHelper.detect(knownBrowsers[0]).catch(() => {})) + + expect(cp.spawn).toHaveBeenNthCalledWith(1, knownBrowsers[0].binary, ['--version'], expect.objectContaining({ + env: expect.objectContaining({ + env2: 'false', + env3: 'true', + }), + })) + }) + }) + + describe('in browser launching', () => { + it('uses arch and ARCHPREFERENCE on arm64', async () => { + vi.mocked(os.arch).mockReturnValue('arm64') + + await launch({ path: 'chrome' } as unknown as FoundBrowser, 'url', 123, ['arg1'], { env1: 'true', env2: 'true' }) + + expect(cp.spawn).toHaveBeenNthCalledWith(1, 'arch', ['chrome', 'url', 'arg1'], expect.objectContaining({ + env: expect.objectContaining({ + ARCHPREFERENCE: 'arm64,x86_64', + env1: 'true', + env2: 'false', + env3: 'true', + }), + })) + }) + + it('does not use `arch` on x64', async () => { + vi.mocked(os.arch).mockReturnValue('x64') + + await launch({ path: 'chrome' } as unknown as FoundBrowser, 'url', 123, ['arg1'], { env1: 'true', env2: 'true' }) + + expect(cp.spawn).toHaveBeenNthCalledWith(1, 'chrome', ['url', 'arg1'], expect.objectContaining({ + env: expect.objectContaining({ + env1: 'true', + env2: 'false', + env3: 'true', + }), + })) + + // @ts-expect-error + expect(cp.spawn.mock.calls[0][2].env).not.toHaveProperty('ARCHPREFERENCE') + }) + }) + }) +}) From fca8c2a14ab2138c96ee680a9a44b5ec4a313279 Mon Sep 17 00:00:00 2001 From: Cacie Prins Date: Tue, 14 Oct 2025 11:45:58 -0400 Subject: [PATCH 3/4] fix XDarwin ref --- packages/launcher/lib/platforms/PlatformFactory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/launcher/lib/platforms/PlatformFactory.ts b/packages/launcher/lib/platforms/PlatformFactory.ts index 6253128d19f..489717005b5 100644 --- a/packages/launcher/lib/platforms/PlatformFactory.ts +++ b/packages/launcher/lib/platforms/PlatformFactory.ts @@ -1,6 +1,6 @@ import os from 'os' import type { Platform } from './Platform' -import { Darwin } from './XDarwin' +import { Darwin } from './Darwin' import { Linux } from './Linux' import { Windows } from './Windows' From 9da5e3e4417b7fc932a96847e837728491f85b46 Mon Sep 17 00:00:00 2001 From: Cacie Prins Date: Tue, 14 Oct 2025 14:47:23 -0400 Subject: [PATCH 4/4] fix ref to launcher --- packages/server/lib/browsers/firefox.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/browsers/firefox.ts b/packages/server/lib/browsers/firefox.ts index a3c665c6845..1fb4398b8b5 100644 --- a/packages/server/lib/browsers/firefox.ts +++ b/packages/server/lib/browsers/firefox.ts @@ -5,7 +5,7 @@ import Debug from 'debug' import getPort from 'get-port' import path from 'path' import urlUtil from 'url' -import { debug as launcherDebug } from '@packages/launcher/lib/browsers' +import { debug as launcherDebug } from '@packages/launcher/lib/launch' import { doubleEscape } from '@packages/launcher/lib/windows' import FirefoxProfile from 'firefox-profile' import * as errors from '../errors'