@wdio/xvfb
Version:
A standalone utility to manage Xvfb (X Virtual Framebuffer) for headless testing
248 lines (203 loc) • 9.14 kB
text/typescript
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
import type { ChildProcess } from 'node:child_process'
// Use vi.hoisted to ensure mocks are set up before imports
const mockSpawn = vi.hoisted(() => vi.fn())
const mockFork = vi.hoisted(() => vi.fn())
const mockExecSync = vi.hoisted(() => vi.fn())
// Mock child_process module
vi.mock('node:child_process', () => ({
spawn: mockSpawn,
fork: mockFork,
execSync: mockExecSync
}))
// Mock logger
vi.mock('@wdio/logger', () => ({
default: vi.fn(() => ({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn()
}))
}))
// Mock XvfbManager
const mockXvfbManager = {
shouldRun: vi.fn(),
init: vi.fn(),
executeWithRetry: vi.fn()
}
vi.mock('../src/XvfbManager.js', () => ({
XvfbManager: vi.fn(() => mockXvfbManager)
}))
// Import after mocks are set up
const { ProcessFactory } = await import('../src/ProcessFactory.js')
describe('ProcessFactory', () => {
let processFactory: InstanceType<typeof ProcessFactory>
const mockChildProcess = {
on: vi.fn(),
send: vi.fn(),
kill: vi.fn(),
pid: 12345,
stdout: null,
stderr: null,
stdin: null
} as unknown as ChildProcess
beforeEach(() => {
vi.clearAllMocks()
processFactory = new ProcessFactory()
// Reset mock implementations
mockSpawn.mockReturnValue(mockChildProcess)
mockFork.mockReturnValue(mockChildProcess)
mockExecSync.mockReturnValue('success')
mockXvfbManager.shouldRun.mockReturnValue(false)
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('constructor', () => {
it('should create instance with default XvfbManager', () => {
const factory = new ProcessFactory()
expect(factory).toBeInstanceOf(ProcessFactory)
})
it('should create instance with custom XvfbManager', () => {
const customManager = mockXvfbManager
const factory = new ProcessFactory(customManager as any)
expect(factory).toBeInstanceOf(ProcessFactory)
})
})
describe('createWorkerProcess', () => {
const scriptPath = '/path/to/script.js'
const args = ['--arg1', '--arg2']
const options = {
cwd: '/working/dir',
env: { NODE_ENV: 'test' },
execArgv: ['--inspect'],
stdio: ['inherit', 'pipe', 'pipe', 'ipc'] as ('inherit' | 'pipe' | 'ignore' | 'ipc')[]
}
describe('when xvfb should not run', () => {
beforeEach(() => {
mockXvfbManager.shouldRun.mockReturnValue(false)
})
it('should use regular fork when xvfb is not needed', async () => {
const result = await processFactory.createWorkerProcess(scriptPath, args, options)
expect(mockXvfbManager.shouldRun).toHaveBeenCalled()
expect(mockFork).toHaveBeenCalledWith(scriptPath, args, {
cwd: options.cwd,
env: options.env,
execArgv: options.execArgv,
stdio: options.stdio
})
expect(mockSpawn).not.toHaveBeenCalled()
expect(result).toBe(mockChildProcess)
})
it('should not wrap with xvfb-run when explicitly disabled', async () => {
mockXvfbManager.shouldRun.mockReturnValue(false) // disabled -> shouldRun false
// even if xvfb-run exists, we shouldn't check for it when shouldRun is false
mockExecSync.mockReturnValue('/usr/bin/xvfb-run')
await processFactory.createWorkerProcess(scriptPath, args, options)
// we still call execSync('which xvfb-run') before deciding, so we only assert we didn't spawn xvfb-run
expect(mockSpawn).not.toHaveBeenCalled()
expect(mockFork).toHaveBeenCalled()
})
it('should use fork with default execArgv when not provided', async () => {
const { execArgv: _ignored, ...optionsWithoutExecArgv } = options
await processFactory.createWorkerProcess(scriptPath, args, optionsWithoutExecArgv)
expect(mockFork).toHaveBeenCalledWith(scriptPath, args, {
cwd: options.cwd,
env: options.env,
execArgv: [],
stdio: options.stdio
})
})
})
describe('when xvfb should run', () => {
beforeEach(() => {
mockXvfbManager.shouldRun.mockReturnValue(true)
mockXvfbManager.executeWithRetry.mockImplementation(async (fn) => await fn())
})
it('should use executeWithRetry when xvfb is available', async () => {
mockExecSync.mockReturnValue('/usr/bin/xvfb-run')
const mockProcess = {
...mockChildProcess,
on: vi.fn()
} as unknown as ChildProcess
mockSpawn.mockReturnValue(mockProcess)
const result = await processFactory.createWorkerProcess(scriptPath, args, options)
expect(mockXvfbManager.shouldRun).toHaveBeenCalled()
expect(mockExecSync).toHaveBeenCalledWith('which xvfb-run', { stdio: 'ignore' })
expect(mockXvfbManager.executeWithRetry).toHaveBeenCalledWith(
expect.any(Function),
'xvfb worker process creation'
)
expect(mockFork).not.toHaveBeenCalled()
expect(result).toBe(mockProcess)
})
it('should fallback to fork when xvfb-run is not available', async () => {
mockExecSync.mockImplementation(() => {
throw new Error('Command not found')
})
const result = await processFactory.createWorkerProcess(scriptPath, args, options)
expect(mockXvfbManager.shouldRun).toHaveBeenCalled()
expect(mockExecSync).toHaveBeenCalledWith('which xvfb-run', { stdio: 'ignore' })
expect(mockXvfbManager.executeWithRetry).not.toHaveBeenCalled()
expect(mockFork).toHaveBeenCalledWith(scriptPath, args, {
cwd: options.cwd,
env: options.env,
execArgv: options.execArgv,
stdio: options.stdio
})
expect(mockSpawn).not.toHaveBeenCalled()
expect(result).toBe(mockChildProcess)
})
})
describe('edge cases', () => {
it('should handle minimal options', async () => {
mockXvfbManager.shouldRun.mockReturnValue(false)
const minimalOptions = {}
await processFactory.createWorkerProcess(scriptPath, args, minimalOptions)
expect(mockFork).toHaveBeenCalledWith(scriptPath, args, {
cwd: undefined,
env: undefined,
execArgv: [],
stdio: undefined
})
})
it('should handle empty args array', async () => {
mockXvfbManager.shouldRun.mockReturnValue(false)
await processFactory.createWorkerProcess(scriptPath, [], options)
expect(mockFork).toHaveBeenCalledWith(scriptPath, [], {
cwd: options.cwd,
env: options.env,
execArgv: options.execArgv,
stdio: options.stdio
})
})
})
})
describe('integration with XvfbManager', () => {
it('should call shouldRun on the XvfbManager instance', async () => {
const customManager = {
shouldRun: vi.fn().mockReturnValue(false),
executeWithRetry: vi.fn()
}
const factory = new ProcessFactory(customManager as any)
await factory.createWorkerProcess('/script.js', [], {})
expect(customManager.shouldRun).toHaveBeenCalled()
})
it('should respect XvfbManager decision about whether to run xvfb', async () => {
const customManager = {
shouldRun: vi.fn().mockReturnValue(true),
executeWithRetry: vi.fn().mockImplementation(async (fn) => await fn())
}
const factory = new ProcessFactory(customManager as any)
mockExecSync.mockReturnValue('/usr/bin/xvfb-run')
const mockProcess = {
...mockChildProcess,
on: vi.fn()
} as unknown as ChildProcess
mockSpawn.mockReturnValue(mockProcess)
await factory.createWorkerProcess('/script.js', [], {})
expect(customManager.shouldRun).toHaveBeenCalled()
expect(customManager.executeWithRetry).toHaveBeenCalled()
expect(mockFork).not.toHaveBeenCalled()
})
})
})