🏡 index : quine-global/webdriverio.git

import path from 'node:path'
import { vi, describe, it, expect, afterEach, beforeEach } from 'vitest'
import logger from '@wdio/logger'
import { sleep, enableFileLogging } from '@wdio/utils'
import Launcher from '../src/launcher.js'

const caps: WebdriverIO.Capabilities = {
    'wdio:maxInstances': 1,
    browserName: 'chrome'
}

vi.mock('node:fs/promises', () => ({
    default: {
        access: vi.fn().mockRejectedValue(new Error('ENOENT')),
        mkdir: vi.fn()
    }
}))
vi.mock('@wdio/utils', () => import(path.join(process.cwd(), '__mocks__', '@wdio/utils')))
vi.mock('@wdio/config', () => import(path.join(process.cwd(), '__mocks__', '@wdio/config')))
vi.mock('@wdio/config/node', () => import(path.join(process.cwd(), '__mocks__', '@wdio/config/node')))
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
vi.mock('../src/interface', () => ({
    default: class {
        emit = vi.fn()
        on = vi.fn()
        sigintTrigger = vi.fn()
        onMessage = vi.fn()
        logHookError = vi.fn()
        finalise = vi.fn()
    }
}))

const shard = { current: 1, total: 1 }

describe('launcher', () => {
    const emitSpy = vi.spyOn(process, 'emit')
    let launcher: Launcher

    beforeEach(() => {
        global.console.log = vi.fn()
        emitSpy.mockClear()
        launcher = new Launcher('./')
        launcher.interface = {
            onMessage: vi.fn(),
            finalise: vi.fn(),
            emit: vi.fn(),
            sigintTrigger: vi.fn()
        } as any
    })

    it('should have default for the argv parameter', () => {
        expect(launcher['_args']).toEqual({})
    })

    describe('capabilities', () => {
        it('should NOT fail when capabilities are passed', async () => {
            launcher['_runSpecs'] = vi.fn().mockReturnValue(1)
            const exitCode = await launcher['_runMode']({ specs: ['./'] } as any, [caps])
            expect(launcher['_runSpecs']).toBeCalled()
            expect(exitCode).toEqual(0)
            expect(logger('').error).not.toBeCalled()
        })

        it('should fail if no specs were found', async () => {
            launcher['_runSpecs'] = vi.fn()
            launcher.configParser.getSpecs = vi.fn().mockReturnValue([])
            const exitCode = await launcher['_runMode']({ specs: ['./'], shard } as any, [caps])
            expect(launcher['_runSpecs']).toBeCalledTimes(0)
            expect(exitCode).toBe(1)
        })

        it('should fail when no capabilities are passed', async () => {
            launcher['_runSpecs'] = vi.fn().mockReturnValue(1)
            // @ts-ignore test invalid parameter
            const exitCode = await launcher['_runMode']({ specs: ['./'] as any, shard })
            expect(exitCode).toEqual(1)
            expect(logger('').error).toBeCalledWith('Missing capabilities, exiting with failure')
        })

        it('should fail when no capabilities are set', async () => {
            launcher['_runSpecs'] = vi.fn().mockReturnValue(1)
            const exitCode = await launcher['_runMode']({ specs: ['./'], shard } as any)
            expect(exitCode).toEqual(1)
            expect(logger('').error).toBeCalledWith('Missing capabilities, exiting with failure')
        })

        it('should fail when no capabilities are set (empty capabilities array)', async () => {
            launcher['_runSpecs'] = vi.fn().mockReturnValue(1)
            const exitCode = await launcher['_runMode']({ specs: ['./'], shard } as any, [])
            expect(exitCode).toEqual(1)
            expect(logger('').error).toBeCalledWith('Missing capabilities, exiting with failure')
        })

        it('should fail when no capabilities are set (empty capabilities array)', async () => {
            launcher['_runSpecs'] = vi.fn().mockReturnValue(1)
            const exitCode = await launcher['_runMode']({ specs: ['./'], shard } as any, {})
            expect(exitCode).toEqual(1)
            expect(logger('').error).toBeCalledWith('Missing capabilities, exiting with failure')
        })

        it('should start instance in multiremote', () => {
            launcher['_runSpecs'] = vi.fn()
            launcher.isMultiremote = true
            launcher['_runMode'](
                { specs: ['./'], specFileRetries: 2 } as any,
                { foo: { capabilities: { browserName: 'chrome' } } }
            )

            expect(launcher['_schedule']).toHaveLength(1)
            expect(launcher['_schedule'][0].specs[0].retries).toBe(2)

            expect(typeof launcher['_resolve']).toBe('function')
            expect(launcher['_runSpecs']).toBeCalledTimes(1)
        })

        it('should start instances with parallel multiremote', () => {
            launcher['_runSpecs'] = vi.fn()
            launcher.isMultiremote = true
            launcher.isParallelMultiremote = true
            launcher['_runMode'](
                { specs: ['./'], specFileRetries: 2 } as any,
                [
                    { foo: { capabilities: { browserName: 'chrome' } } },
                    { foo: { capabilities: { browserName: 'chrome' } } }
                ]
            )

            expect(launcher['_schedule']).toHaveLength(2)
            expect(launcher['_schedule'][0].specs[0].retries).toBe(2)
            expect(launcher['_schedule'][1].specs[0].retries).toBe(2)

            expect(typeof launcher['_resolve']).toBe('function')
            expect(launcher['_runSpecs']).toBeCalledTimes(1)
        })

        it('should start instance with grouped specs', () => {
            launcher['_runSpecs'] = vi.fn()
            launcher.isMultiremote = false
            launcher['_runMode'](
                { specs: [['/a.js', '/b.js']], specFileRetries: 2 } as any,
                [caps]
            )

            expect(launcher['_schedule']).toHaveLength(1)
            expect(launcher['_schedule'][0].specs[0].retries).toBe(2)

            expect(typeof launcher['_resolve']).toBe('function')
            expect(launcher['_runSpecs']).toBeCalledTimes(1)
        })

        it('should start instance in multiremote with grouped specs', () => {
            launcher['_runSpecs'] = vi.fn()
            launcher.isMultiremote = true
            launcher['_runMode'](
                { specs: [['/a.js', '/b.js']], specFileRetries: 2 } as any,
                { foo: { capabilities: { browserName: 'chrome' } } }
            )

            expect(launcher['_schedule']).toHaveLength(1)
            expect(launcher['_schedule'][0].specs[0].retries).toBe(2)

            expect(typeof launcher['_resolve']).toBe('function')
            expect(launcher['_runSpecs']).toBeCalledTimes(1)
        })

        it('should ignore specFileRetries in watch mode', () => {
            launcher['_runSpecs'] = vi.fn()
            launcher['_isWatchMode'] = true
            launcher['_runMode']({ specs: ['./'], specFileRetries: 2 } as any, [caps, caps])

            expect(launcher['_schedule']).toHaveLength(2)
            expect(launcher['_schedule'][0].specs[0].retries).toBe(0)
            expect(launcher['_schedule'][1].specs[0].retries).toBe(0)

            expect(launcher['_runSpecs']).toBeCalledTimes(1)
        })

        it('should apply maxInstancesPerCapability if maxInstances is not passed', () => {
            launcher['_runSpecs'] = vi.fn()
            launcher['_runMode'](
                { specs: ['./'], specFileRetries: 3, maxInstancesPerCapability: 4 } as any,
                [{ browserName: 'chrome' }]
            )

            expect(launcher['_schedule']).toHaveLength(1)
            expect(launcher['_schedule'][0].specs[0].retries).toBe(3)
            expect(launcher['_schedule'][0].availableInstances).toBe(4)

            expect(launcher['_runSpecs']).toBeCalledTimes(1)
        })

        it('should mixin wdio:maxInstances per capability + precedence over maxInstancesPerCapability', () => {
            const wdioPrefixMaxInstances = [
                {
                    browserName: 'chrome',
                    'wdio:maxInstances': 11
                },
                {
                    browserName: 'safari',
                    'wdio:maxInstances': 22
                }
            ]
            launcher['_runSpecs'] = vi.fn()
            launcher['_runMode'](
                { specs: ['./'], maxInstancesPerCapability: 3 } as any,
                wdioPrefixMaxInstances
            )

            expect(launcher['_schedule']).toHaveLength(2)
            expect(launcher['_schedule'][0].availableInstances).toBe(11)
            expect(launcher['_schedule'][1].availableInstances).toBe(22)
        })
    })

    describe('hasTriggeredExitRoutine', () => {
        it('should return false if there are specs left of running when hasTriggeredExitRoutine is false', () => {
            launcher['_getNumberOfRunningInstances'] = vi.fn().mockReturnValue(1)
            launcher['_hasTriggeredExitRoutine'] = false
            const returnValue = launcher['_runSpecs']()
            expect(returnValue).toEqual(false)
        })

        it('should return true when hasTriggeredExitRoutine is true', () => {
            launcher['_getNumberOfRunningInstances'] = vi.fn().mockReturnValue(1)
            launcher['_hasTriggeredExitRoutine'] = true
            const returnValue = launcher['_runSpecs']()
            expect(returnValue).toEqual(true)
        })
    })

    describe('endHandler', () => {
        const config = { onWorkerEnd: vi.fn() }

        beforeEach(() => {
            launcher['_launcher'] = []
            launcher.configParser.getConfig = vi.fn().mockReturnValue(config)
        })

        it('should emit and resolve failed status', async () => {
            launcher['_getNumberOfRunningInstances'] = vi.fn().mockReturnValue(1)
            launcher['_runSpecs'] = vi.fn().mockReturnValue(1)
            launcher['_schedule'] = [{ cid: 1 } as any, { cid: 2 }]
            launcher['_resolve'] = vi.fn()
            await launcher['_endHandler']({ cid: '0-1', exitCode: 1, specs: [], retries: 0 } as any)
            expect(launcher.interface!.emit).toBeCalledWith('job:end', { cid: '0-1', passed: false, retries: 0 })
            expect(launcher['_resolve']).toBeCalledWith(1)
            expect(config.onWorkerEnd).toBeCalledWith('0-1', 1, [], 0)
        })

        it('should emit and resolve passed status', async () => {
            launcher['_getNumberOfRunningInstances'] = vi.fn().mockReturnValue(1)
            launcher['_runSpecs'] = vi.fn().mockReturnValue(1)
            launcher['_schedule'] = [{ cid: 1 } as any, { cid: 2 }]
            launcher['_resolve'] = vi.fn()
            await launcher['_endHandler']({ cid: '0-1', exitCode: 0 } as any)
            expect(launcher.interface!.emit).toBeCalledWith('job:end', { cid: '0-1', passed: true })
            expect(launcher['_resolve']).toBeCalledWith(0)
        })

        it('should do nothing if not all specs are run', async () => {
            launcher['_getNumberOfRunningInstances'] = vi.fn().mockReturnValue(1)
            launcher['_runSpecs'] = vi.fn().mockReturnValue(0)
            launcher['_schedule'] = [{ cid: 1 } as any, { cid: 2 }]
            launcher['_resolve'] = vi.fn()
            await launcher['_endHandler']({ cid: '0-1', exitCode: 0 } as any)
            expect(launcher.interface!.emit).toBeCalledWith('job:end', { cid: '0-1', passed: true })
            expect(launcher.interface?.finalise).toBeCalledTimes(0)
            expect(launcher['_resolve']).toBeCalledTimes(0)
        })

        it('should do nothing if watch mode is still running', async () => {
            launcher['_getNumberOfRunningInstances'] = vi.fn().mockReturnValue(1)
            launcher['_isWatchMode'] = true
            launcher['_runSpecs'] = vi.fn().mockReturnValue(1)
            launcher['_schedule'] = [{ cid: 1 } as any, { cid: 2 }]
            launcher['_resolve'] = vi.fn()
            await launcher['_endHandler']({ cid: '0-1', exitCode: 1 } as any)
            expect(launcher.interface!.emit).toBeCalledWith('job:end', { cid: '0-1', passed: false })
            expect(launcher.interface?.finalise).toBeCalledTimes(1)
            expect(launcher['_resolve']).toBeCalledTimes(0)
        })

        it('should resolve and not emit on watch mode stop', async () => {
            launcher['_getNumberOfRunningInstances'] = vi.fn().mockReturnValue(1)
            launcher['_isWatchMode'] = true
            launcher['_hasTriggeredExitRoutine'] = true
            launcher['_runSpecs'] = vi.fn().mockReturnValue(1)
            launcher['_schedule'] = [{ cid: 1 } as any, { cid: 2 }]
            launcher['_resolve'] = vi.fn()
            await launcher['_endHandler']({ cid: '0-1', exitCode: 1 } as any)
            expect(launcher.interface!.emit).not.toBeCalled()
            expect(launcher['_resolve']).toBeCalledWith(0)
        })

        it('should reschedule when runner failed and retries remain', async () => {
            launcher['_schedule'] = [{ cid: 0, specs: [] }] as any
            await launcher['_endHandler']({ cid: '0-5', exitCode: 1, retries: 1, specs: ['a.js'] })
            expect(launcher['_schedule']).toMatchObject([{ cid: 0, specs: [{ rid: '0-5', files: ['a.js'], retries: 0 }] }])
        })

        it('should requeue retried specfiles at beginning of queue', async () => {
            launcher.configParser.getConfig = vi.fn().mockReturnValue({ specFileRetriesDeferred: false, onWorkerEnd: vi.fn() })
            launcher['_schedule'] = [{ cid: 0, specs: [{ files: ['b.js'] }] }] as any
            await launcher['_endHandler']({ cid: '0-5', exitCode: 1, retries: 1, specs: ['a.js'] })
            expect(launcher['_schedule']).toMatchObject([{ cid: 0, specs: [{ rid: '0-5', files: ['a.js'], retries: 0 }, { files: ['b.js'] }] }])
        })

        it('should requeue retried specfiles at end of queue', async () => {
            launcher['_schedule'] = [{ cid: 0, specs: [{ files: ['b.js'] }] }] as any
            await launcher['_endHandler']({ cid: '0-5', exitCode: 1, retries: 1, specs: ['a.js'] })
            expect(launcher['_schedule']).toMatchObject([{
                cid: 0,
                specs: [{
                    files: ['a.js']
                }, {
                    files: ['b.js'],
                }]
            }])
        })
    })

    describe('exitHandler', () => {
        it('should do nothing if no callback is given', () => {
            launcher['_hasTriggeredExitRoutine'] = false
            launcher.runner = { shutdown: vi.fn()
                .mockReturnValue(Promise.resolve()) } as any

            launcher['_exitHandler']()

            expect(launcher['_hasTriggeredExitRoutine']).toBe(false)
            expect(launcher.interface!.sigintTrigger).toBeCalledTimes(0)
            expect(launcher.runner?.shutdown).toBeCalledTimes(0)
        })

        it('should do nothing if shutdown was called before', () => {
            launcher['_hasTriggeredExitRoutine'] = true
            launcher.runner = { shutdown: vi.fn().mockReturnValue(Promise.resolve()) } as any

            expect(launcher['_exitHandler'](() => 'foo')).toBe('foo')

            expect(launcher['_hasTriggeredExitRoutine']).toBe(true)
            expect(launcher.interface!.sigintTrigger).toBeCalledTimes(0)
            expect(launcher.runner?.shutdown).toBeCalledTimes(0)
        })

        it('should shutdown', () => {
            launcher['_hasTriggeredExitRoutine'] = false
            launcher.runner = { shutdown: vi.fn().mockReturnValue(Promise.resolve()) } as any

            launcher['_exitHandler'](vi.fn())

            expect(launcher['_hasTriggeredExitRoutine']).toBe(true)
            expect(launcher.interface!.sigintTrigger).toBeCalledTimes(1)
            expect(launcher.runner?.shutdown).toBeCalledTimes(1)
        })
    })

    describe('getRunnerId', () => {
        it('should return increasing cid numbers', () => {
            expect(launcher['_getRunnerId'](0)).toBe('0-0')
            expect(launcher['_getRunnerId'](0)).toBe('0-1')
            expect(launcher['_getRunnerId'](0)).toBe('0-2')
            expect(launcher['_getRunnerId'](2)).toBe('2-0')
            expect(launcher['_getRunnerId'](0)).toBe('0-3')
            expect(launcher['_getRunnerId'](2)).toBe('2-1')
        })
    })

    describe('getNumberOfSpecsLeft', () => {
        it('should return number of spec', () => {
            launcher['_schedule'] = [{ specs: [1, 2, [3, 4, 5]] }, { specs: [1, 2] }] as any
            expect(launcher['_getNumberOfSpecsLeft']()).toBe(5)
        })
    })

    describe('getNumberOfRunningInstances', () => {
        it('should return number of spec', () => {
            launcher['_schedule'] = [{ runningInstances: 3 }, { runningInstances: 2 }] as any
            expect(launcher['_getNumberOfRunningInstances']()).toBe(5)
        })
    })

    describe('formatSpecs', () => {
        it('should return correctly formatted specs', () => {
            // Define a capabilities that is sent to formatSpecs
            // - only used in function call
            const capabilities = { specs: ['/a.js', ['/b.js', '/c.js', '/d.js'], '/e.js'] }
            const specFileRetries = 17
            // Define the golden result
            const expected = [
                { 'files': ['/a.js'], 'retries': 17 },
                { 'files': ['/b.js', '/c.js', '/d.js'], 'retries': 17 },
                { 'files': ['/e.js'], 'retries': 17 },
            ]
            // Mock the return value of getSpecs so we are not doing cross
            // module testing
            launcher.configParser = { getSpecs: vi.fn().mockReturnValue(
                ['/a.js', ['/b.js', '/c.js', '/d.js'], '/e.js']
            ) } as any
            expect(launcher['_formatSpecs'](capabilities as any, specFileRetries)).toStrictEqual(expected)
        })
    })

    describe('_runSpecs', () => {
        beforeEach(() => {
            launcher['_startInstance'] = vi.fn()
        })

        it('should not start running anything if exit routine is triggered', () => {
            launcher.configParser = { getConfig: vi.fn().mockReturnValue({
                maxInstances: 100
            }) } as any
            launcher['_hasTriggeredExitRoutine'] = true
            expect(launcher['_runSpecs']()).toBe(true)
            expect(launcher['_startInstance']).toBeCalledTimes(0)
        })

        it('should run all specs', () => {
            launcher.configParser = { getConfig: vi.fn().mockReturnValue({
                maxInstances: 100
            }) } as any
            launcher['_schedule'] = [{
                cid: 0,
                caps: { browserName: 'chrome' },
                specs: ['/a.js', 'b.js', 'c.js'],
                availableInstances: 50,
                runningInstances: 0,
                seleniumServer: {}
            }, {
                cid: 1,
                caps: { browserName: 'chrome2' },
                specs: ['/a.js', 'b.js', 'c.js', 'd.js'],
                availableInstances: 60,
                runningInstances: 0,
                seleniumServer: {}
            }, {
                cid: 1,
                caps: { browserName: 'chrome2' },
                specs: ['/a.js', 'b.js'],
                availableInstances: 70,
                runningInstances: 0,
                seleniumServer: {}
            }] as any
            expect(launcher['_runSpecs']()).toBe(false)
            expect(launcher['_getNumberOfRunningInstances']()).toBe(9)
            expect(launcher['_getNumberOfSpecsLeft']()).toBe(0)
            expect(launcher['_schedule'][0].runningInstances).toBe(3)
            expect(launcher['_schedule'][0].availableInstances).toBe(47)
            expect(launcher['_schedule'][1].runningInstances).toBe(4)
            expect(launcher['_schedule'][1].availableInstances).toBe(56)
            expect(launcher['_schedule'][2].runningInstances).toBe(2)
            expect(launcher['_schedule'][2].availableInstances).toBe(68)
        })

        it('should run arrayed specs in a single instance', () => {
            launcher.configParser = { getConfig: vi.fn().mockReturnValue({
                maxInstances: 100
            }) } as any
            launcher['_schedule'] = [{
                cid: 0,
                caps: { browserName: 'chrome' },
                specs: ['/a.js', ['b.js', 'c.js']],
                availableInstances: 50,
                runningInstances: 0,
                seleniumServer: {}
            }, {
                cid: 1,
                caps: { browserName: 'chrome2' },
                specs: [['/a.js', 'b.js', 'c.js', 'd.js']],
                availableInstances: 60,
                runningInstances: 0,
                seleniumServer: {}
            }, {
                cid: 1,
                caps: { browserName: 'chrome2' },
                specs: [['/a.js'], 'b.js'],
                availableInstances: 70,
                runningInstances: 0,
                seleniumServer: {}
            }] as any
            expect(launcher['_runSpecs']()).toBe(false)
            expect(launcher['_getNumberOfRunningInstances']()).toBe(5)
            expect(launcher['_getNumberOfSpecsLeft']()).toBe(0)
            expect(launcher['_schedule'][0].runningInstances).toBe(2)
            expect(launcher['_schedule'][0].availableInstances).toBe(48)
            expect(launcher['_schedule'][1].runningInstances).toBe(1)
            expect(launcher['_schedule'][1].availableInstances).toBe(59)
            expect(launcher['_schedule'][2].runningInstances).toBe(2)
            expect(launcher['_schedule'][2].availableInstances).toBe(68)
        })

        it('should not run anything if runner failed', () => {
            launcher['_runnerFailed'] = 2
            launcher.configParser = { getConfig: vi.fn().mockReturnValue({
                maxInstances: 100,
                bail: 1
            }) } as any
            launcher['_schedule'] = [{
                cid: 0,
                caps: { browserName: 'chrome' },
                specs: ['/a.js', 'b.js', 'c.js'],
                availableInstances: 50,
                runningInstances: 0,
                seleniumServer: {}
            }, {
                cid: 1,
                caps: { browserName: 'chrome2' },
                specs: ['/a.js', 'b.js', 'c.js', 'd.js'],
                availableInstances: 60,
                runningInstances: 0,
                seleniumServer: {}
            }, {
                cid: 1,
                caps: { browserName: 'chrome2' },
                specs: ['/a.js', 'b.js'],
                availableInstances: 70,
                runningInstances: 0,
                seleniumServer: {}
            }] as any
            expect(launcher['_runSpecs']()).toBe(true)
            expect(launcher['_getNumberOfRunningInstances']()).toBe(0)
            expect(launcher['_getNumberOfSpecsLeft']()).toBe(0)
        })

        it('should run as much as maxInstances allows', () => {
            launcher.configParser = { getConfig: vi.fn().mockReturnValue({
                maxInstances: 5
            }) } as any
            launcher['_schedule'] = [{
                cid: 0,
                caps: { browserName: 'chrome' },
                specs: ['/a.js', 'b.js', 'c.js'],
                availableInstances: 50,
                runningInstances: 0,
                seleniumServer: {}
            }, {
                cid: 1,
                caps: { browserName: 'chrome2' },
                specs: ['/a.js', 'b.js', 'c.js', 'd.js'],
                availableInstances: 60,
                runningInstances: 0,
                seleniumServer: {}
            }, {
                cid: 2,
                caps: { browserName: 'chrome2' },
                specs: ['/a.js', 'b.js'],
                availableInstances: 70,
                runningInstances: 0,
                seleniumServer: {}
            }] as any
            expect(launcher['_runSpecs']()).toBe(false)
            expect(launcher['_getNumberOfRunningInstances']()).toBe(5)
            expect(launcher['_getNumberOfSpecsLeft']()).toBe(4)
        })

        it('should not allow to schedule more runner if no instances are available', () => {
            launcher.configParser = { getConfig: vi.fn().mockReturnValue({
                maxInstances: 100
            }) } as any
            launcher['_schedule'] = [{
                cid: 0,
                caps: { browserName: 'chrome' },
                specs: ['/a.js', 'b.js', 'c.js'],
                availableInstances: 1,
                runningInstances: 10,
                seleniumServer: {}
            }, {
                cid: 1,
                caps: { browserName: 'chrome2' },
                specs: ['/a.js', 'b.js', 'c.js', 'd.js'],
                availableInstances: 2,
                runningInstances: 4,
                seleniumServer: {}
            }, {
                cid: 1,
                caps: { browserName: 'chrome2' },
                specs: ['/a.js', 'b.js'],
                availableInstances: 0,
                runningInstances: 2,
                seleniumServer: {}
            }] as any
            expect(launcher['_runSpecs']()).toBe(false)
            expect(launcher['_getNumberOfRunningInstances']()).toBe(19)
            expect(launcher['_getNumberOfSpecsLeft']()).toBe(6)
            expect(launcher['_schedule'][0].runningInstances).toBe(11)
            expect(launcher['_schedule'][0].availableInstances).toBe(0)
            expect(launcher['_schedule'][1].runningInstances).toBe(6)
            expect(launcher['_schedule'][1].availableInstances).toBe(0)
            expect(launcher['_schedule'][2].runningInstances).toBe(2)
            expect(launcher['_schedule'][2].availableInstances).toBe(0)
        })

        it('should not run if all specs were executed', () => {
            launcher.configParser = { getConfig: vi.fn().mockReturnValue({
                maxInstances: 100
            }) } as any
            launcher['_schedule'] = [{
                cid: 0,
                caps: { browserName: 'chrome' },
                specs: [],
                availableInstances: 10,
                runningInstances: 0,
                seleniumServer: {}
            }] as any
            expect(launcher['_runSpecs']()).toBe(true)
            expect(launcher['_getNumberOfRunningInstances']()).toBe(0)
            expect(launcher['_getNumberOfSpecsLeft']()).toBe(0)
        })
    })

    describe('startInstance', () => {
        beforeEach(() => {
            // launcher.runner?.run = vi.fn().mockReturnValue({ on: () => {} })
            launcher['_launcher'] = []
            launcher.runner = {
                run: vi.fn().mockReturnValue({
                    on: vi.fn()
                })
            } as any
        })

        it('should start an instance', async () => {
            const onWorkerStartMock = vi.fn()
            const caps = {
                browserName: 'chrome'
            }
            launcher.configParser.getConfig = () => ({ onWorkerStart: onWorkerStartMock }) as any
            launcher['_args'].hostname = '127.0.0.2'

            expect(launcher['_runnerStarted']).toBe(0)
            await launcher['_startInstance'](
                ['/foo.test.js'],
                caps,
                0,
                '0-5',
                0
            )

            expect(sleep).not.toHaveBeenCalled()
            expect(launcher['_runnerStarted']).toBe(1)
            // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
            expect(vi.mocked(launcher.runner?.run!).mock.calls[0][0]).toHaveProperty('cid', '0-5')
            expect(launcher['_getRunnerId'](0)).toBe('0-0')

            expect(onWorkerStartMock).toHaveBeenCalledWith(
                '0-5',
                caps,
                ['/foo.test.js'],
                { hostname: '127.0.0.2' },
                []
            )
        })

        it('should start an instance with different capability worker by worker', async () => {
            const hostname = '127.0.0.2'
            const caps = {
                browserName: 'chrome'
            }
            const expectedCaps = Object.assign({}, caps, {
                'goog:chromeOptions': {
                    args: `--inspect=${hostname}:50000`,
                }
            })
            const onWorkerStartMock = vi.fn().mockImplementation((_runnerId, caps)=>{
                caps['goog:chromeOptions'] = {
                    args: `--inspect=${hostname}:50000`
                }
            })
            launcher.configParser.getConfig = () => ({ onWorkerStart: onWorkerStartMock }) as any
            launcher['_args'].hostname = hostname

            expect(launcher['_runnerStarted']).toBe(0)
            await launcher['_startInstance'](
                ['/foo.test.js'],
                caps,
                0,
                '0-5',
                0
            )

            expect(onWorkerStartMock).toHaveBeenCalledWith(
                '0-5',
                expectedCaps,
                ['/foo.test.js'],
                { hostname: '127.0.0.2' },
                []
            )

            expect(caps).toStrictEqual({
                browserName: 'chrome'
            })
        })

        it('should wait before starting an instance on retry', async () => {
            const onWorkerStartMock = vi.fn()
            const caps = {
                browserName: 'chrome'
            }
            launcher.configParser = { getConfig: vi.fn().mockReturnValue({
                onWorkerStart: onWorkerStartMock,
                specFileRetries: 2,
                specFileRetriesDelay: 0.01
            }) } as any
            launcher['_args'].hostname = '127.0.0.3'

            await launcher['_startInstance'](
                ['/foo.test.js'],
                caps,
                0,
                '0-6',
                3
            )

            expect(sleep).toHaveBeenCalledTimes(1)
            expect(sleep).toHaveBeenCalledWith(10)

            expect(onWorkerStartMock).toHaveBeenCalledWith(
                '0-6',
                caps,
                ['/foo.test.js'],
                { hostname: '127.0.0.3' },
                []
            )
        })

        it('should not wait before starting an instance on the first run', async () => {
            const onWorkerStartMock = vi.fn()
            const caps = {
                browserName: 'chrome'
            }
            launcher.configParser = { getConfig: vi.fn().mockReturnValue({
                onWorkerStart: onWorkerStartMock,
                specFileRetries: 4,
                specFileRetriesDelay: 0.01
            }) } as any
            launcher['_args'].hostname = '127.0.0.4'

            await launcher['_startInstance'](
                ['/foo.test.js'],
                caps,
                0,
                '0-7',
                4
            )

            expect(sleep).not.toHaveBeenCalled()

            expect(onWorkerStartMock).toHaveBeenCalledWith(
                '0-7',
                caps,
                ['/foo.test.js'],
                { hostname: '127.0.0.4' },
                []
            )
        })
    })

    describe('run', () => {
        let config: WebdriverIO.Config = { capabilities: [{}] }

        beforeEach(() => {
            global.console.error = vi.fn()

            config = {
                // ConfigParser.addFileConfig() will return onPrepare and onComplete as arrays of functions
                onPrepare: [vi.fn()],
                onComplete: [vi.fn()],
                capabilities: [{}],
                runner: 'local',
                runnerEnv: {},
                outputDir: 'tempDir',
                shard
            }
            launcher.configParser = {
                getCapabilities: vi.fn().mockReturnValue({}),
                getConfig: vi.fn().mockReturnValue(config),
                initialize: vi.fn()
            } as any
            launcher.runner = { initialize: vi.fn(), shutdown: vi.fn() } as any
            launcher['_runMode'] = vi.fn().mockImplementation(() => 0)
        })

        it('exit code 0', async () => {
            expect(await launcher.run()).toEqual(0)
            expect(launcher['configParser'].initialize).toBeCalledTimes(1)
            expect(launcher.runner!.shutdown).toBeCalled()
            expect(enableFileLogging).toHaveBeenCalledWith('tempDir')

            expect(launcher.configParser.getCapabilities).toBeCalledTimes(2)
            expect(launcher.configParser.getConfig).toBeCalledTimes(1)
            expect(launcher.runner!.initialize).toBeCalledTimes(1)
            // @ts-ignore
            expect(config.onPrepare![0]).toBeCalledTimes(1)
            expect(launcher['_runMode']).toBeCalledTimes(1)
            // @ts-ignore
            expect(config.onPrepare![0]).toBeCalledTimes(1)
            expect(launcher.interface!.finalise).toBeCalledTimes(1)
        })

        it('should not shutdown runner if was called before', async () => {
            launcher['_hasTriggeredExitRoutine'] = true
            expect(await launcher.run()).toEqual(0)
            expect(launcher.runner!.shutdown).not.toBeCalled()
        })

        it('onComplete error', async () => {
            // ConfigParser.addFileConfig() will return onComplete as an array of functions
            config.onComplete = [() => { throw new Error() }]

            expect(await launcher.run()).toEqual(1)
            expect(launcher.runner!.shutdown).toBeCalled()
        })

        it('should shutdown runner on error', async () => {
            logger.waitForBuffer = () => Promise.reject(new Error('ups'))

            let error
            try {
                await launcher.run()
            } catch (err: any) {
                error = err
            }
            expect(launcher.runner!.shutdown).toBeCalled()
            expect(error).toBeInstanceOf(Error)
        })

        afterEach(() => {
            vi.mocked(global.console.error).mockRestore()
        })
    })

    afterEach(() => {
        vi.mocked(global.console.log).mockRestore()
        vi.mocked(sleep).mockClear()
    })
})