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() }) })