🏡 index : quine-global/webdriverio.git

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)

    // Mock the xvfbManager instance that was created in constructor
    const mockInit = vi.fn().mockResolvedValue(true)
    runner['xvfbManager'] = { init: mockInit } as any

    // Initialize should not call xvfb.init
    await runner.initialize()
    expect(mockInit).not.toHaveBeenCalled()

    // First run should initialize xvfb
    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)

    // Mock the xvfbManager instance that was created in constructor
    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()
    // Verify that xvfb didn't actually initialize (returned false)
    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)

    // Mock the xvfbManager instance that was created in constructor
    const mockInit = vi.fn().mockResolvedValue(true)
    runner['xvfbManager'] = { init: mockInit } as any

    // Should not throw during run, just log the error
    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)

    // Mock the xvfbManager instance that was created in constructor
    const mockInit = vi.fn().mockResolvedValue(true)
    runner['xvfbManager'] = { init: mockInit } as any

    // First run should initialize xvfb
    await runner.run({
        cid: '0-4',
        command: 'run',
        configFile: '/path/to/wdio.conf.js',
        args: {},
        caps: {},
        specs: ['/foo/bar1.test.js'],
        execArgv: [],
        retries: 0,
    })

    // Second run should not initialize xvfb again
    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
    )

    // Mock the xvfbManager instance that was created in constructor
    const mockInit = vi.fn().mockResolvedValue(true)
    const mockShouldRun = vi.fn().mockReturnValue(true)
    runner['xvfbManager'] = { init: mockInit, shouldRun: mockShouldRun } as any

    // Start a worker (should initialize xvfb)
    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
    )

    // Mock the xvfbManager instance that was created in constructor
    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
    )

    // Trigger lazy init to ensure constructor ran
    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 })
    )
})