@wdio/xvfb
Version:
A standalone utility to manage Xvfb (X Virtual Framebuffer) for headless testing
835 lines (665 loc) • 37.6 kB
text/typescript
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
// Use vi.hoisted to ensure mocks are set up before imports
const mockExecAsync = vi.hoisted(() => vi.fn())
const mockPlatform = vi.hoisted(() => vi.fn())
const mockIsCI = vi.hoisted(() => ({ value: false }))
// Mock all the modules before importing anything else
vi.mock('node:child_process', () => ({
exec: vi.fn()
}))
vi.mock('node:util', () => ({
promisify: vi.fn(() => mockExecAsync)
}))
vi.mock('node:os', () => ({
default: {
platform: mockPlatform
}
}))
vi.mock('is-ci', () => ({
get default() {
return mockIsCI.value
}
}))
vi.mock('@wdio/logger', () => ({
default: vi.fn(() => ({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn()
}))
}))
// Import after mocks are set up
const { XvfbManager } = await import('../src/XvfbManager.js')
describe('XvfbManager', () => {
let manager: InstanceType<typeof XvfbManager>
beforeEach(() => {
vi.clearAllMocks()
manager = new XvfbManager()
// Reset environment
delete process.env.DISPLAY
mockIsCI.value = false
mockPlatform.mockReturnValue('linux')
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('constructor', () => {
it('should create instance with default options', () => {
const manager = new XvfbManager()
expect(manager).toBeInstanceOf(XvfbManager)
})
it('should create instance with custom options', () => {
const manager = new XvfbManager({
force: true,
packageManager: 'apt',
xvfbMaxRetries: 5,
xvfbRetryDelay: 2000
})
expect(manager).toBeInstanceOf(XvfbManager)
})
})
describe('shouldRun', () => {
it('should return true when forced', () => {
const manager = new XvfbManager({ force: true })
mockPlatform.mockReturnValue('darwin') // Non-Linux platform
expect(manager.shouldRun()).toBe(true)
})
it('should return false on non-Linux platforms', () => {
mockPlatform.mockReturnValue('darwin')
expect(manager.shouldRun()).toBe(false)
})
it('should return true on Linux without DISPLAY', () => {
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
expect(manager.shouldRun()).toBe(true)
})
it('should return false on Linux when DISPLAY is set', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = true
expect(manager.shouldRun()).toBe(false)
})
it('should return false on Linux with existing DISPLAY', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = false
expect(manager.shouldRun()).toBe(false)
})
it('should return false when disabled via enabled:false', () => {
const disabledManager = new XvfbManager({ enabled: false })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
expect(disabledManager.shouldRun()).toBe(false)
})
it('should return true when Chrome headless flag is detected', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = false
const capabilities = {
'goog:chromeOptions': {
args: ['--headless']
}
} as unknown as WebdriverIO.Config['capabilities']
expect(manager.shouldRun(capabilities)).toBe(true)
})
it('should return true when Chrome headless=new flag is detected', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = false
const capabilities = {
'goog:chromeOptions': {
args: ['--headless=new']
}
} as unknown as WebdriverIO.Config['capabilities']
expect(manager.shouldRun(capabilities)).toBe(true)
})
it('should return true when Chrome headless=old flag is detected', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = false
const capabilities = {
'goog:chromeOptions': {
args: ['--headless=old']
}
} as unknown as WebdriverIO.Config['capabilities']
expect(manager.shouldRun(capabilities)).toBe(true)
})
it('should return true when Firefox headless flag is detected', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = false
const capabilities = {
'moz:firefoxOptions': {
args: ['--headless']
}
} as unknown as WebdriverIO.Config['capabilities']
expect(manager.shouldRun(capabilities)).toBe(true)
})
it('should return true when Firefox -headless flag is detected', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = false
const capabilities = {
'moz:firefoxOptions': {
args: ['-headless']
}
} as unknown as WebdriverIO.Config['capabilities']
expect(manager.shouldRun(capabilities)).toBe(true)
})
it('should handle array of capabilities (multiremote)', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = false
const capabilities = {
browser1: {
capabilities: {
'goog:chromeOptions': {
args: ['--headless']
}
}
},
browser2: {
capabilities: {
'moz:firefoxOptions': {
args: ['--disable-gpu']
}
}
}
} as unknown as WebdriverIO.Config['capabilities']
expect(manager.shouldRun(capabilities)).toBe(true)
})
it('should return false when no headless flags in capabilities', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = false
const capabilities = {
'goog:chromeOptions': {
args: ['--disable-gpu']
}
} as unknown as WebdriverIO.Config['capabilities']
expect(manager.shouldRun(capabilities)).toBe(false)
})
it('should handle capabilities without args', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = false
const capabilities = {
'goog:chromeOptions': {}
} as unknown as WebdriverIO.Config['capabilities']
expect(manager.shouldRun(capabilities)).toBe(false)
})
it('should handle empty capabilities', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = false
expect(manager.shouldRun(undefined)).toBe(false)
})
it('should handle undefined capabilities', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = false
expect(manager.shouldRun(undefined)).toBe(false)
})
it('should return true when Edge headless flag is detected (ms:edgeOptions)', () => {
mockPlatform.mockReturnValue('linux')
process.env.DISPLAY = ':0'
mockIsCI.value = false
const capabilities = {
'ms:edgeOptions': {
args: ['--headless']
}
} as unknown as WebdriverIO.Config['capabilities']
expect(manager.shouldRun(capabilities)).toBe(true)
})
})
describe('init', () => {
beforeEach(() => {
mockPlatform.mockReturnValue('linux')
})
it('should setup xvfb-run when needed', async () => {
mockExecAsync.mockResolvedValue({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
const result = await manager.init()
expect(result).toBe(true)
expect(mockExecAsync).toHaveBeenCalledWith('which xvfb-run')
})
it('should not setup when not needed', async () => {
mockPlatform.mockReturnValue('darwin')
const result = await manager.init()
expect(result).toBe(false)
})
it('should setup xvfb-run when headless capabilities are provided', async () => {
process.env.DISPLAY = ':0'
mockIsCI.value = false
mockExecAsync.mockResolvedValue({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
const capabilities = {
'goog:chromeOptions': {
args: ['--headless']
}
} as unknown as WebdriverIO.Config['capabilities']
const result = await manager.init(capabilities)
expect(result).toBe(true)
})
it('should return false and skip setup when disabled via enabled:false', async () => {
const disabledManager = new XvfbManager({ enabled: false })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await disabledManager.init()
expect(result).toBe(false)
expect(mockExecAsync).not.toHaveBeenCalled()
})
describe('autoInstall', () => {
it('should install xvfb with sudo -n when allowed and available (non-root, apt)', async () => {
// Sequence: which xvfb-run -> which apt-get -> which sudo -> run install -> which xvfb-run (verify)
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run (initial)
.mockResolvedValueOnce({ stdout: '/usr/bin/apt-get', stderr: '' }) // which apt-get
.mockResolvedValueOnce({ stdout: '/usr/bin/sudo', stderr: '' }) // which sudo
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' }) // install
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' }) // which xvfb-run (verify)
// Mock getuid to return non-root (1000) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(1000)
const manager = new XvfbManager({ autoInstall: true, autoInstallMode: 'sudo' })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await manager.init()
expect(result).toBe(true)
expect(mockExecAsync).toHaveBeenCalledWith('which xvfb-run')
expect(mockExecAsync).toHaveBeenCalledWith('which apt-get')
expect(mockExecAsync).toHaveBeenCalledWith('which sudo')
expect(mockExecAsync).toHaveBeenCalledWith(
'sudo -n DEBIAN_FRONTEND=noninteractive apt-get update -qq && sudo -n DEBIAN_FRONTEND=noninteractive apt-get install -y xvfb',
{ timeout: 240000 }
)
})
it('should not install and return false when xvfb-run is not available and autoInstall is disabled', async () => {
// Mock xvfb-run not found
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found'))
const manager = new XvfbManager()
// Mock platform and environment
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await manager.init()
expect(result).toBe(false)
// Should only check for xvfb-run
expect(mockExecAsync).toHaveBeenCalledWith('which xvfb-run')
// And should not attempt any package manager detection or install
expect(mockExecAsync).not.toHaveBeenCalledWith('which apt-get')
expect(mockExecAsync).not.toHaveBeenCalledWith('which dnf')
expect(mockExecAsync).not.toHaveBeenCalledWith('which yum')
expect(mockExecAsync).not.toHaveBeenCalledWith('which zypper')
expect(mockExecAsync).not.toHaveBeenCalledWith('which pacman')
expect(mockExecAsync).not.toHaveBeenCalledWith('which apk')
expect(mockExecAsync).not.toHaveBeenCalledWith('which xbps-install')
})
describe('cross-distribution support', () => {
beforeEach(() => {
mockPlatform.mockReturnValue('linux')
})
it('should detect Ubuntu distribution and install without sudo when running as root', async () => {
// Mock xvfb-run not found, then package manager detection
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found'))
.mockResolvedValueOnce({ stdout: '/usr/bin/apt-get', stderr: '' })
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' })
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
// Mock getuid to return root (0) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(0)
const manager = new XvfbManager({ autoInstall: true })
delete process.env.DISPLAY
await manager.init()
expect(mockExecAsync).toHaveBeenCalledWith('which apt-get')
expect(mockExecAsync).toHaveBeenCalledWith(
'DEBIAN_FRONTEND=noninteractive apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y xvfb',
{ timeout: 240000 }
)
})
it('should detect dnf package manager', async () => {
// Mock xvfb-run not found, then package manager detection
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found'))
.mockRejectedValueOnce(new Error('apt-get not found'))
.mockResolvedValueOnce({ stdout: '/usr/bin/dnf', stderr: '' })
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' })
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
// Mock getuid to return root (0) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(0)
const manager = new XvfbManager({ autoInstall: true })
delete process.env.DISPLAY
await manager.init()
expect(mockExecAsync).toHaveBeenCalledWith('which dnf')
expect(mockExecAsync).toHaveBeenCalledWith(
'dnf -y makecache && dnf -y install xorg-x11-server-Xvfb',
{ timeout: 240000 }
)
})
it('should detect pacman package manager', async () => {
// Mock xvfb-run not found, then package manager detection
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found'))
.mockRejectedValueOnce(new Error('apt-get not found'))
.mockRejectedValueOnce(new Error('dnf not found'))
.mockRejectedValueOnce(new Error('yum not found'))
.mockRejectedValueOnce(new Error('zypper not found'))
.mockResolvedValueOnce({ stdout: '/usr/bin/pacman', stderr: '' })
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' })
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
// Mock getuid to return root (0) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(0)
const manager = new XvfbManager({ autoInstall: true })
delete process.env.DISPLAY
await manager.init()
expect(mockExecAsync).toHaveBeenCalledWith('which pacman')
expect(mockExecAsync).toHaveBeenCalledWith(
'pacman -Sy --noconfirm xorg-server-xvfb',
{ timeout: 240000 }
)
})
it('should detect dnf when apt-get is not available', async () => {
// Mock xvfb-run not found, sudo available, then package manager detection
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run
.mockResolvedValueOnce({ stdout: '/usr/bin/sudo', stderr: '' }) // which sudo
.mockRejectedValueOnce(new Error('apt-get not found')) // which apt-get
.mockResolvedValueOnce({ stdout: '/usr/bin/dnf', stderr: '' }) // which dnf
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' }) // dnf install
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' }) // verify which xvfb-run
// Mock getuid to return non-root (1000) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(1000)
const manager = new XvfbManager({ autoInstall: true, autoInstallMode: 'sudo' })
delete process.env.DISPLAY
await manager.init()
expect(mockExecAsync).toHaveBeenCalledWith('which xvfb-run')
expect(mockExecAsync).toHaveBeenCalledWith('which apt-get')
expect(mockExecAsync).toHaveBeenCalledWith('which dnf')
expect(mockExecAsync).toHaveBeenCalledWith('which sudo')
})
it('should handle unsupported package managers gracefully', async () => {
// Mock xvfb-run not found, then all package managers as not found
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run
.mockRejectedValueOnce(new Error('apt-get not found')) // which apt-get
.mockRejectedValueOnce(new Error('dnf not found')) // which dnf
.mockRejectedValueOnce(new Error('yum not found')) // which yum
.mockRejectedValueOnce(new Error('zypper not found')) // which zypper
.mockRejectedValueOnce(new Error('pacman not found')) // which pacman
.mockRejectedValueOnce(new Error('apk not found')) // which apk
.mockRejectedValueOnce(new Error('xbps-install not found')) // which xbps-install
// Mock as root so installation is attempted
// Mock getuid to return root (0) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(0)
const manager = new XvfbManager({ autoInstall: true })
delete process.env.DISPLAY
await expect(manager.init()).rejects.toThrow(
'Unsupported package manager: unknown. Please install Xvfb manually.'
)
})
})
it("should skip install in 'sudo' mode when sudo is not present (non-root)", async () => {
// Mock xvfb-run not found, then sudo not found (should skip installation)
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run
.mockRejectedValueOnce(new Error('sudo not found')) // which sudo
// Mock getuid to return non-root (1000) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(1000)
const manager = new XvfbManager({ autoInstall: true, autoInstallMode: 'sudo' })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await manager.init()
expect(result).toBe(false)
expect(mockExecAsync).toHaveBeenCalledWith('which xvfb-run')
expect(mockExecAsync).toHaveBeenCalledWith('which sudo')
// Should not check package managers since installation is skipped
expect(mockExecAsync).not.toHaveBeenCalledWith('which apt-get')
// Should not attempt installation
expect(mockExecAsync).not.toHaveBeenCalledWith('DEBIAN_FRONTEND=noninteractive apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y xvfb')
})
it('should treat empty object form as root-only: installs when root', async () => {
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run
.mockResolvedValueOnce({ stdout: '/usr/bin/apt-get', stderr: '' }) // which apt-get
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' }) // install
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' }) // verify
// Mock getuid to return root (0) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(0)
const manager = new XvfbManager({ autoInstall: true, autoInstallMode: 'root' })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await manager.init()
expect(result).toBe(true)
expect(mockExecAsync).toHaveBeenCalledWith(
'DEBIAN_FRONTEND=noninteractive apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y xvfb',
{ timeout: 240000 }
)
})
it('should treat empty object form as root-only: skips when not root', async () => {
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run
.mockResolvedValueOnce({ stdout: '/usr/bin/apt-get', stderr: '' }) // which apt-get
// Mock getuid to return non-root (1000) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(1000)
const manager = new XvfbManager({ autoInstall: true, autoInstallMode: 'root' })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await manager.init()
expect(result).toBe(false)
// no sudo check, no install
expect(mockExecAsync).not.toHaveBeenCalledWith('which sudo')
})
it("should not use sudo prefix when in 'sudo' mode but running as root", async () => {
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run
.mockResolvedValueOnce({ stdout: '/usr/bin/apt-get', stderr: '' }) // which apt-get
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' }) // install
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' }) // verify
// Mock getuid to return root (0) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(0)
const manager = new XvfbManager({ autoInstall: true, autoInstallMode: 'sudo' })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await manager.init()
expect(result).toBe(true)
expect(mockExecAsync).toHaveBeenCalledWith(
'DEBIAN_FRONTEND=noninteractive apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y xvfb',
{ timeout: 240000 }
)
})
it("should check for sudo even with custom command in 'sudo' mode (non-root)", async () => {
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run
.mockResolvedValueOnce({ stdout: '/usr/bin/apt-get', stderr: '' }) // PM detection
.mockResolvedValueOnce({ stdout: '/usr/bin/sudo', stderr: '' }) // which sudo
.mockResolvedValueOnce({ stdout: 'ok', stderr: '' }) // run custom
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' }) // verify
// Mock getuid to return non-root (1000) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(1000)
const manager = new XvfbManager({ autoInstall: true, autoInstallMode: 'sudo', autoInstallCommand: 'echo install' })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await manager.init()
expect(result).toBe(true)
expect(mockExecAsync).toHaveBeenCalledWith('which sudo')
expect(mockExecAsync).toHaveBeenCalledWith('echo install', { timeout: 240000 })
})
it('should handle zypper install flags (root)', async () => {
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run
.mockRejectedValueOnce(new Error('apt-get not found'))
.mockRejectedValueOnce(new Error('dnf not found'))
.mockRejectedValueOnce(new Error('yum not found'))
.mockResolvedValueOnce({ stdout: '/usr/bin/zypper', stderr: '' }) // zypper
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' })
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
// Mock getuid to return root (0) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(0)
const manager = new XvfbManager({ autoInstall: true })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
await manager.init()
expect(mockExecAsync).toHaveBeenCalledWith(
'zypper --non-interactive refresh && zypper --non-interactive install -y xvfb-run',
{ timeout: 240000 }
)
})
it('should skip availability check when forceInstall is true and perform install', async () => {
mockExecAsync
.mockResolvedValueOnce({ stdout: '/usr/bin/apt-get', stderr: '' }) // which apt-get
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' })
// no post-verify since forceInstall skips it
// Mock getuid to return root (0) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(0)
const manager = new XvfbManager({ autoInstall: true, forceInstall: true })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await manager.init()
expect(result).toBe(true)
// ensure no initial which xvfb-run call
expect(mockExecAsync).not.toHaveBeenCalledWith('which xvfb-run')
// but install was performed
expect(mockExecAsync).toHaveBeenCalledWith(
'DEBIAN_FRONTEND=noninteractive apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y xvfb',
{ timeout: 240000 }
)
})
it('should skip installation when not root and sudo is not allowed', async () => {
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run (initial)
// Mock getuid to return non-root (1000) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(1000)
const manager = new XvfbManager({ autoInstall: true, autoInstallMode: 'root' })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await manager.init()
expect(result).toBe(false)
expect(mockExecAsync).toHaveBeenCalledWith('which xvfb-run')
// should not call package manager detection since installation is skipped
expect(mockExecAsync).not.toHaveBeenCalledWith('which apt-get')
// should not check sudo
expect(mockExecAsync).not.toHaveBeenCalledWith('which sudo')
})
it('should use custom install command as-is (no sudo prefix)', async () => {
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run (initial)
.mockResolvedValueOnce({ stdout: '/usr/bin/apt-get', stderr: '' }) // PM detection still runs
.mockResolvedValueOnce({ stdout: 'custom ok', stderr: '' }) // install
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' }) // verify
// Mock getuid to return non-root (1000) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(1000)
const manager = new XvfbManager({ autoInstall: true, autoInstallMode: 'sudo', autoInstallCommand: 'my-custom-install' })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await manager.init()
expect(result).toBe(true)
expect(mockExecAsync).toHaveBeenCalledWith(
'my-custom-install',
{ timeout: 240000 }
)
})
it('should handle object format with array commands', async () => {
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run
.mockResolvedValueOnce({ stdout: '/usr/bin/sudo', stderr: '' }) // which sudo
.mockResolvedValueOnce({ stdout: 'array command ok', stderr: '' }) // install
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' }) // verify
// Mock getuid to return non-root (1000) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(1000)
const manager = new XvfbManager({
autoInstall: true,
autoInstallMode: 'sudo',
autoInstallCommand: ['custom', 'install', 'command']
})
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await manager.init()
expect(result).toBe(true)
expect(mockExecAsync).toHaveBeenCalledWith(
'custom install command',
{ timeout: 240000 }
)
})
it('should handle object format with mode only (defaults to sudo behavior)', async () => {
mockExecAsync
.mockRejectedValueOnce(new Error('Command not found')) // which xvfb-run
.mockResolvedValueOnce({ stdout: '/usr/bin/sudo', stderr: '' }) // which sudo
.mockRejectedValueOnce(new Error('apt-get not found')) // which apt-get
.mockResolvedValueOnce({ stdout: '/usr/bin/dnf', stderr: '' }) // which dnf
.mockResolvedValueOnce({ stdout: 'dnf install ok', stderr: '' }) // install
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' }) // verify
// Mock getuid to return non-root (1000) - works on all platforms
;(process as any).getuid = vi.fn().mockReturnValue(1000)
const manager = new XvfbManager({ autoInstall: true, autoInstallMode: 'sudo' })
mockPlatform.mockReturnValue('linux')
delete process.env.DISPLAY
const result = await manager.init()
expect(result).toBe(true)
expect(mockExecAsync).toHaveBeenCalledWith('which sudo')
})
})
})
describe('executeWithRetry', () => {
beforeEach(() => {
mockPlatform.mockReturnValue('linux')
})
it('should succeed on first attempt', async () => {
const mockFn = vi.fn().mockResolvedValue('success')
const result = await manager.executeWithRetry(mockFn, 'test operation')
expect(result).toBe('success')
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('should retry on xvfb-related errors', async () => {
const manager = new XvfbManager({ xvfbMaxRetries: 2, xvfbRetryDelay: 10 })
const mockFn = vi.fn()
.mockRejectedValueOnce(new Error('xvfb-run: error: Xvfb failed to start'))
.mockResolvedValueOnce('success')
const result = await manager.executeWithRetry(mockFn, 'test operation')
expect(result).toBe('success')
expect(mockFn).toHaveBeenCalledTimes(2)
})
it('should retry with progressive delay', async () => {
const manager = new XvfbManager({ xvfbMaxRetries: 3, xvfbRetryDelay: 100 })
const mockFn = vi.fn()
.mockRejectedValueOnce(new Error('Xvfb failed to start'))
.mockRejectedValueOnce(new Error('xvfb-run: error:'))
.mockResolvedValueOnce('success')
const startTime = Date.now()
const result = await manager.executeWithRetry(mockFn, 'test operation')
const endTime = Date.now()
expect(result).toBe('success')
expect(mockFn).toHaveBeenCalledTimes(3)
// Should have waited at least 100ms + 200ms = 300ms for two retries
expect(endTime - startTime).toBeGreaterThan(280)
})
it('should not retry on non-xvfb errors', async () => {
const mockFn = vi.fn().mockRejectedValue(new Error('Regular error'))
await expect(manager.executeWithRetry(mockFn, 'test operation')).rejects.toThrow('Regular error')
expect(mockFn).toHaveBeenCalledTimes(1)
})
it('should exhaust retries and throw last error', async () => {
const manager = new XvfbManager({ xvfbMaxRetries: 2, xvfbRetryDelay: 10 })
const mockFn = vi.fn()
.mockRejectedValueOnce(new Error('xvfb-run: error: Xvfb failed to start'))
.mockRejectedValueOnce(new Error('X server died'))
await expect(manager.executeWithRetry(mockFn, 'test operation')).rejects.toThrow('X server died')
expect(mockFn).toHaveBeenCalledTimes(2)
})
it('should detect various xvfb error patterns', async () => {
const manager = new XvfbManager({ xvfbMaxRetries: 1, xvfbRetryDelay: 10 })
const errorPatterns = [
'xvfb-run: error: Xvfb failed to start',
'Xvfb failed to start',
'xvfb-run: error: something else',
'X server died'
]
for (const errorMessage of errorPatterns) {
const mockFn = vi.fn().mockRejectedValue(new Error(errorMessage))
await expect(manager.executeWithRetry(mockFn, 'test')).rejects.toThrow(errorMessage)
expect(mockFn).toHaveBeenCalledTimes(1)
}
})
it('should handle case insensitive error matching', async () => {
const manager = new XvfbManager({ xvfbMaxRetries: 1, xvfbRetryDelay: 10 })
const mockFn = vi.fn().mockRejectedValue(new Error('XVFB-RUN: ERROR: XVFB FAILED TO START'))
await expect(manager.executeWithRetry(mockFn, 'test')).rejects.toThrow('XVFB-RUN: ERROR: XVFB FAILED TO START')
expect(mockFn).toHaveBeenCalledTimes(1)
})
})
})