Skip to content
2 changes: 1 addition & 1 deletion packages/launcher/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { detect, detectByPath } from './lib/detect'

import { launch } from './lib/browsers'
import { launch } from './lib/launch'

export {
detect,
Expand Down
54 changes: 0 additions & 54 deletions packages/launcher/lib/browsers.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/launcher/lib/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These were renamed because intellisense was complaining that

/lib/darwin/index.ts was too similar to /lib/platforms/Darwin.ts 🤷

import { notDetectedAtPathErr } from './errors'
import * as linuxHelper from './linux'
import Debug from 'debug'
Expand Down
28 changes: 28 additions & 0 deletions packages/launcher/lib/launch.ts
Original file line number Diff line number Diff line change
@@ -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<null, Readable, Readable>

// 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.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of places explicitly coerce unknown POJOs to FoundBrowser, though, so it's important to keep it

if (!browser.path) {
throw new Error(`Browser ${browser.name} is missing path`)
}

return PlatformFactory.select().launch(browser, url, args, browserEnv)
}
29 changes: 29 additions & 0 deletions packages/launcher/lib/platforms/Darwin.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {}): 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)
}
}
4 changes: 4 additions & 0 deletions packages/launcher/lib/platforms/Linux.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Platform } from './Platform'

export class Linux extends Platform {
}
46 changes: 46 additions & 0 deletions packages/launcher/lib/platforms/Platform.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = {}): 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,
},
}
}
}
20 changes: 20 additions & 0 deletions packages/launcher/lib/platforms/PlatformFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import os from 'os'
import type { Platform } from './Platform'
import { Darwin } from './Darwin'
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()}`)
}
}
}
4 changes: 4 additions & 0 deletions packages/launcher/lib/platforms/Windows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Platform } from './Platform'

export class Windows extends Platform {
}
4 changes: 2 additions & 2 deletions packages/launcher/test/unit/darwin.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ 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 darwinHelper from '../../lib/darwinHelpers'
import * as linuxHelper from '../../lib/linux'
import * as darwinUtil from '../../lib/darwin/util'
import * as darwinUtil from '../../lib/darwinHelpers/util'
import { launch } from '../../lib/browsers'
import { knownBrowsers } from '../../lib/known-browsers'

Expand Down
6 changes: 3 additions & 3 deletions packages/launcher/test/unit/detect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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 {
Expand Down Expand Up @@ -63,7 +63,7 @@ describe('detect', () => {
vi.resetAllMocks()

const { detect: linuxDetectActual } = await vi.importActual<typeof import('../../lib/linux')>('../../lib/linux')
const { detect: darwinDetectActual } = await vi.importActual<typeof import('../../lib/darwin')>('../../lib/darwin')
const { detect: darwinDetectActual } = await vi.importActual<typeof import('../../lib/darwinHelpers')>('../../lib/darwinHelpers')
const { detect: windowsDetectActual } = await vi.importActual<typeof import('../../lib/windows')>('../../lib/windows')

vi.mocked(linuxDetect).mockImplementation(linuxDetectActual)
Expand Down
136 changes: 136 additions & 0 deletions packages/launcher/test/unit/launch.spec.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>
let launchedBrowser: Mocked<ChildProcess>

let arch: ReturnType<typeof os.arch>
let platform: ReturnType<typeof os.platform>

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)
})
})
}
})
2 changes: 1 addition & 1 deletion packages/server/lib/browsers/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,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')

Expand Down
Loading
Loading