import path from 'node:path'
import { expect, test, vi, beforeEach } from 'vitest'
import LocalRunner from '../src/index.js'
const sleep = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms))
beforeEach(async () => {
vi.clearAllMocks()
})
vi.mock(
'@wdio/logger',
() => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))
)
vi.mock('@wdio/xvfb', () => {
const childProcessMock = {
on: vi.fn(),
send: vi.fn(),
kill: vi.fn(),
}
return {
ProcessFactory: vi.fn().mockImplementation(() => ({
createWorkerProcess: vi.fn().mockResolvedValue(childProcessMock)
})),
XvfbManager: vi.fn().mockImplementation(() => ({
init: vi.fn().mockResolvedValue(true),
shouldRun: vi.fn().mockReturnValue(true)
})),
default: vi.fn()
}
})
test("should pass xvfbAutoInstall:'sudo' to XvfbManager", async () => {
const xvfb = await import('@wdio/xvfb')
const runner = new LocalRunner(
{} as never,
{
xvfbAutoInstall: 'sudo',
autoXvfb: true,
xvfbAutoInstallMode: undefined,
xvfbAutoInstallCommand: undefined
} as any
)
await runner.run({
cid: 'auto-3',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
expect(vi.mocked(xvfb.XvfbManager)).toHaveBeenCalledWith(
expect.objectContaining({ autoInstall: 'sudo', autoInstallMode: undefined })
)
})
test('should pass object-form xvfbAutoInstall to XvfbManager', async () => {
const xvfb = await import('@wdio/xvfb')
const runner = new LocalRunner(
{} as never,
{
xvfbAutoInstall: { mode: 'sudo', command: 'echo install' },
autoXvfb: true,
xvfbAutoInstallMode: undefined,
xvfbAutoInstallCommand: undefined
} as any
)
await runner.run({
cid: 'auto-4',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
expect(vi.mocked(xvfb.XvfbManager)).toHaveBeenCalledWith(
expect.objectContaining({ autoInstall: { mode: 'sudo', command: 'echo install' }, autoInstallMode: undefined, autoInstallCommand: undefined })
)
})
test('should fork a new process', async () => {
const runner = new LocalRunner(
{} as never,
{
outputDir: '/foo/bar',
runnerEnv: { FORCE_COLOR: 1 },
autoXvfb: true,
xvfbAutoInstall: undefined,
xvfbAutoInstallMode: undefined,
xvfbAutoInstallCommand: undefined
} as any
)
const worker = await runner.run({
cid: '0-5',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
worker['_handleMessage']({ name: 'ready' } as any)
await sleep()
expect(worker.isBusy).toBe(true)
expect(worker.childProcess?.on).toHaveBeenCalled()
expect(worker.childProcess?.send).toHaveBeenCalledWith({
args: {},
caps: {},
cid: '0-5',
command: 'run',
configFile: '/path/to/wdio.conf.js',
retries: 0,
specs: ['/foo/bar.test.js'],
})
await worker.postMessage('runAgain', { foo: 'bar' } as any)
})
test('should shut down worker processes', async () => {
const runner = new LocalRunner(
{} as never,
{
outputDir: '/foo/bar',
runnerEnv: { FORCE_COLOR: 1 },
autoXvfb: true,
xvfbAutoInstall: undefined,
xvfbAutoInstallMode: undefined,
xvfbAutoInstallCommand: undefined
} as any
)
const worker1 = await runner.run({
cid: '0-4',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar2.test.js'],
execArgv: [],
retries: 0,
})
worker1['_handleMessage']({ name: 'ready' } as any)
await sleep()
const worker2 = await runner.run({
cid: '0-5',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
worker2['_handleMessage']({ name: 'ready' } as any)
await sleep()
setTimeout(() => {
worker1.isBusy = false
setTimeout(() => {
worker2.isBusy = false
}, 260)
}, 260)
const before = Date.now()
await runner.shutdown()
const after = Date.now()
expect(after - before).toBeGreaterThanOrEqual(750)
const call1: any = vi
.mocked(worker1.childProcess?.send)!
.mock.calls.pop()![0]
expect(call1.cid).toBe('0-5')
expect(call1.command).toBe('endSession')
const call2: any = vi
.mocked(worker1.childProcess?.send)!
.mock.calls.pop()![0]
expect(call2.cid).toBe('0-4')
expect(call2.command).toBe('endSession')
})
test('should avoid shutting down if worker is not busy', async () => {
const runner = new LocalRunner(
{} as never,
{
outputDir: '/foo/bar',
runnerEnv: { FORCE_COLOR: 1 },
autoXvfb: true,
xvfbAutoInstall: undefined,
xvfbAutoInstallMode: undefined,
xvfbAutoInstallCommand: undefined
} as any
)
await runner.run({
cid: '0-8',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: { sessionId: 'abc' } as any,
caps: {},
specs: ['/foo/bar2.test.js'],
execArgv: [],
retries: 0,
})
runner.workerPool['0-8'].isBusy = false
await runner.shutdown()
expect(runner.workerPool['0-8']).toBeFalsy()
})
test('should shut down worker processes in watch mode - regular', async () => {
const runner = new LocalRunner(
{} as never,
{
outputDir: '/foo/bar',
runnerEnv: { FORCE_COLOR: 1 },
watch: true,
autoXvfb: true,
xvfbAutoInstall: undefined,
xvfbAutoInstallMode: undefined,
xvfbAutoInstallCommand: undefined
} as any
)
const worker = await runner.run({
cid: '0-6',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: { sessionId: 'abc' } as any,
caps: {},
specs: ['/foo/bar2.test.js'],
execArgv: [],
retries: 0,
})
worker['_handleMessage']({ name: 'ready' } as any)
runner.workerPool['0-6'].sessionId = 'abc'
runner.workerPool['0-6'].server = { host: 'foo' }
runner.workerPool['0-6'].caps = { browser: 'chrome' } as any
setTimeout(() => {
worker.isBusy = false
}, 260)
const before = Date.now()
await runner.shutdown()
const after = Date.now()
expect(after - before).toBeGreaterThanOrEqual(300)
const call: any = vi
.mocked(worker.childProcess?.send)!
.mock.calls.pop()![0]
expect(call.cid).toBe('0-6')
expect(call.command).toBe('endSession')
expect(call.args.watch).toBe(true)
expect(call.args.isMultiremote).toBeFalsy()
expect(call.args.config.sessionId).toBe('abc')
expect(call.args.config.host).toEqual('foo')
})
test('should shut down worker processes in watch mode - mutliremote', async () => {
const runner = new LocalRunner(
{} as never,
{
outputDir: '/foo/bar',
runnerEnv: { FORCE_COLOR: 1 },
watch: true,
autoXvfb: true,
xvfbAutoInstall: undefined,
xvfbAutoInstallMode: undefined,
xvfbAutoInstallCommand: undefined
} as any
)
const worker = await runner.run({
cid: '0-7',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
worker['_handleMessage']({ name: 'ready' } as any)
runner.workerPool['0-7'].isMultiremote = true
runner.workerPool['0-7'].instances = { foo: { sessionId: '123' } }
runner.workerPool['0-7'].caps = {
foo: {
capabilities: { browser: 'chrome' },
},
} as any
setTimeout(() => {
worker.isBusy = false
}, 260)
const before = Date.now()
await runner.shutdown()
const after = Date.now()
expect(after - before).toBeGreaterThanOrEqual(300)
const call: any = vi
.mocked(worker.childProcess?.send)!
.mock.calls.pop()![0]
expect(call.cid).toBe('0-7')
expect(call.command).toBe('endSession')
expect(call.args.watch).toBe(true)
expect(call.args.isMultiremote).toBe(true)
expect(call.args.instances).toEqual({ foo: { sessionId: '123' } })
})
test('should avoid shutting down if worker is not busy', async () => {
const runner = new LocalRunner({} as never, {
autoXvfb: true,
xvfbAutoInstall: undefined,
xvfbAutoInstallMode: undefined,
xvfbAutoInstallCommand: undefined
} as any)
expect(await runner.initialize()).toBe(undefined)
})
test('should initialize xvfb lazily during first run when needed', async () => {
const runner = new LocalRunner({} as never, { autoXvfb: true } as any)
const mockInit = vi.fn().mockResolvedValue(true)
runner['xvfbManager'] = { init: mockInit } as any
await runner.initialize()
expect(mockInit).not.toHaveBeenCalled()
await runner.run({
cid: '0-1',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: { 'goog:chromeOptions': { args: ['--headless'] } },
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
expect(mockInit).toHaveBeenCalledWith({ 'goog:chromeOptions': { args: ['--headless'] } })
})
test('should not initialize xvfb during run when not needed', async () => {
const runner = new LocalRunner({} as never, { autoXvfb: true } as any)
const mockInit = vi.fn().mockResolvedValue(false)
runner['xvfbManager'] = { init: mockInit } as any
await runner.initialize()
await runner.run({
cid: '0-2',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
expect(mockInit).toHaveBeenCalled()
const initResult = await mockInit.mock.results[0].value
expect(initResult).toBe(false)
})
test('should handle xvfb initialization failure gracefully', async () => {
const runner = new LocalRunner({} as never, { autoXvfb: true } as any)
const mockInit = vi.fn().mockResolvedValue(true)
runner['xvfbManager'] = { init: mockInit } as any
await expect(runner.run({
cid: '0-3',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})).resolves.toBeDefined()
expect(mockInit).toHaveBeenCalled()
})
test('should only initialize xvfb once across multiple runs', async () => {
const runner = new LocalRunner({} as never, { autoXvfb: true } as any)
const mockInit = vi.fn().mockResolvedValue(true)
runner['xvfbManager'] = { init: mockInit } as any
await runner.run({
cid: '0-4',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar1.test.js'],
execArgv: [],
retries: 0,
})
await runner.run({
cid: '0-5',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar2.test.js'],
execArgv: [],
retries: 0,
} as any)
expect(mockInit).toHaveBeenCalledTimes(1)
})
test('should handle xvfb operations with existing workers', async () => {
const runner = new LocalRunner(
{} as never,
{
outputDir: '/foo/bar',
runnerEnv: { FORCE_COLOR: 1 },
autoXvfb: true,
xvfbAutoInstall: undefined,
xvfbAutoInstallMode: undefined,
xvfbAutoInstallCommand: undefined
} as any
)
const mockInit = vi.fn().mockResolvedValue(true)
const mockShouldRun = vi.fn().mockReturnValue(true)
runner['xvfbManager'] = { init: mockInit, shouldRun: mockShouldRun } as any
const worker = await runner.run({
cid: '0-9',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
worker['_handleMessage']({ name: 'ready' } as any)
setTimeout(() => {
worker.isBusy = false
}, 100)
await runner.shutdown()
expect(mockInit).toHaveBeenCalled()
})
test('should skip xvfb initialization when disabled in config', async () => {
const runner = new LocalRunner(
{} as never,
{
autoXvfb: false
} as any
)
const mockInit = vi.fn().mockResolvedValue(true)
runner['xvfbManager'] = { init: mockInit } as any
await runner.run({
cid: '0-10',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
expect(mockInit).not.toHaveBeenCalled()
})
test('should pass xvfbAutoInstall:true to XvfbManager', async () => {
const xvfb = await import('@wdio/xvfb')
const runner = new LocalRunner(
{} as never,
{
xvfbAutoInstall: true,
autoXvfb: true
} as any
)
await runner.run({
cid: 'auto-1',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
expect(vi.mocked(xvfb.XvfbManager)).toHaveBeenCalledWith(
expect.objectContaining({ autoInstall: true })
)
})
test('should pass xvfbAutoInstall:false to XvfbManager', async () => {
const xvfb = await import('@wdio/xvfb')
const runner = new LocalRunner(
{} as never,
{
xvfbAutoInstall: false,
autoXvfb: true
} as any
)
await runner.run({
cid: 'auto-2',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
expect(vi.mocked(xvfb.XvfbManager)).toHaveBeenCalledWith(
expect.objectContaining({ autoInstall: false })
)
})
test('should pass enabled:true to XvfbManager by default', async () => {
const xvfb = await import('@wdio/xvfb')
const runner = new LocalRunner(
{} as never,
{
autoXvfb: true
} as any
)
await runner.run({
cid: 'en-1',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
expect(vi.mocked(xvfb.XvfbManager)).toHaveBeenCalledWith(
expect.objectContaining({ enabled: true })
)
})
test('should pass enabled:false to XvfbManager when autoXvfb is false', async () => {
const xvfb = await import('@wdio/xvfb')
const runner = new LocalRunner(
{} as never,
{
autoXvfb: false
} as any
)
await runner.run({
cid: 'en-2',
command: 'run',
configFile: '/path/to/wdio.conf.js',
args: {},
caps: {},
specs: ['/foo/bar.test.js'],
execArgv: [],
retries: 0,
})
expect(vi.mocked(xvfb.XvfbManager)).toHaveBeenCalledWith(
expect.objectContaining({ enabled: false })
)
})