import path from 'node:path' import logger from '@wdio/logger' import SauceLabs from 'saucelabs' import type { Capabilities, Options } from '@wdio/types' import { expect, test, vi, beforeEach } from 'vitest' import SauceServiceLauncher from '../src/launcher.js' import type { SauceServiceConfig } from '../src/types.js' vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) vi.mock('saucelabs', () => ({ default: { default: class SauceLabsMock { static instances: SauceLabsMock[] = [] stop: Function startSauceConnect: Function constructor (public options: SauceServiceConfig) { this.stop = vi.fn() this.startSauceConnect = vi.fn().mockReturnValue(this) SauceLabsMock.instances.push(this) } } } })) const log = logger('') test('onPrepare w/ SauceConnect w/ tunnelName w/ JWP', async () => { const options: SauceServiceConfig = { sauceConnect: true, sauceConnectOpts: { tunnelName: 'my-tunnel' } } const caps = [{}] as WebdriverIO.Capabilities[] const config = { user: 'foobaruser', key: '12345' } as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) expect(service['_sauceConnectProcess']).toBeUndefined() await service.onPrepare(config, caps) expect(caps).toEqual([{ 'sauce:options': { tunnelName: 'my-tunnel' } }]) // @ts-ignore mock feature expect(SauceLabs.default.instances).toHaveLength(1) // @ts-ignore mock feature expect(SauceLabs.default.instances[0].startSauceConnect).toBeCalledTimes(1) expect(service['_sauceConnectProcess']).not.toBeUndefined() }) test('onPrepare sets runner in metadata', async () => { const options: SauceServiceConfig = { sauceConnect: true, sauceConnectOpts: {}, } const caps = [{}] as WebdriverIO.Capabilities[] const config = {} as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) const startTunnelMock = vi.fn() service.startTunnel = startTunnelMock await service.onPrepare(config, caps) expect(startTunnelMock.mock.calls[0][0].metadata).toBe('runner=webdriverio') }) test('onPrepare w/ SauceConnect w/o tunnelName w/ JWP', async () => { const options: SauceServiceConfig = { sauceConnect: true, sauceConnectOpts: {} } const caps = [{}] as WebdriverIO.Capabilities[] const config = { user: 'foobaruser', key: '12345' } as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) expect(service['_sauceConnectProcess']).toBeUndefined() await service.onPrepare(config, caps) expect(caps[0]['sauce:options']?.tunnelName).toContain('SC-tunnel-') // @ts-ignore mock feature expect(SauceLabs.default.instances).toHaveLength(1) // @ts-ignore mock feature expect(SauceLabs.default.instances[0].startSauceConnect).toBeCalledTimes(1) expect(service['_sauceConnectProcess']).not.toBeUndefined() }) test('onPrepare w/ SauceConnect w/ tunnelName w/ W3C', async () => { const options: SauceServiceConfig = { sauceConnect: true, sauceConnectOpts: { tunnelName: 'my-tunnel' } } const caps = [{ 'sauce:options': { extendedDebugging: false } }] as WebdriverIO.Capabilities[] const config = { user: 'foobaruser', key: '12345' } as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) expect(service['_sauceConnectProcess']).toBeUndefined() await service.onPrepare(config, caps) expect(caps[0]['sauce:options']?.tunnelName).toContain('my-tunnel') expect(service['_sauceConnectProcess']).not.toBeUndefined() // @ts-ignore mock feature expect(SauceLabs.default.instances).toHaveLength(1) // @ts-ignore mock feature expect(SauceLabs.default.instances[0].startSauceConnect).toBeCalledTimes(1) await new Promise((resolve) => setTimeout(resolve, 100)) expect(vi.mocked(log.info).mock.calls[2][0]).toContain('Sauce Connect successfully started after') }) test('onPrepare w/ SauceConnect w/o identifier w/ W3C', async () => { const options: SauceServiceConfig = { sauceConnect: true } const caps = [{ 'appium:deviceName': 'Samsung Galaxy S10' }] as WebdriverIO.Capabilities[] const config = { user: 'foobaruser', key: '12345' } as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) expect(service['_sauceConnectProcess']).toBeUndefined() await service.onPrepare(config, caps) expect(caps[0]['sauce:options']?.tunnelName).toContain('SC-tunnel-') expect(service['_sauceConnectProcess']).not.toBeUndefined() // @ts-ignore mock feature expect(SauceLabs.default.instances).toHaveLength(1) // @ts-ignore mock feature expect(SauceLabs.default.instances[0].startSauceConnect).toBeCalledTimes(1) await new Promise((resolve) => setTimeout(resolve, 100)) expect(vi.mocked(log.info).mock.calls[2][0]).toContain('Sauce Connect successfully started after') }) test('onPrepare w/ SauceConnect', async () => { const options: SauceServiceConfig = { sauceConnect: true } const caps = [{}] as WebdriverIO.Capabilities[] const config = { user: 'foobaruser', key: '12345' } as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) expect(service['_sauceConnectProcess']).toBeUndefined() await service.onPrepare(config, caps) expect(service['_sauceConnectProcess']).not.toBeUndefined() expect(config.port).toBe(undefined) expect(config.protocol).toBe(undefined) expect(config.hostname).toBe(undefined) }) test('onPrepare w/ SauceConnect w/ region EU', async () => { const options: SauceServiceConfig = { sauceConnect: true } const caps = [{}] as WebdriverIO.Capabilities[] const config = { user: 'foobaruser', key: '12345', region: 'eu' } as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) expect(service['_sauceConnectProcess']).toBeUndefined() await service.onPrepare(config, caps) expect(service['_sauceConnectProcess']).not.toBeUndefined() // @ts-ignore mock feature expect(SauceLabs.default.instances).toHaveLength(1) // @ts-ignore mock feature expect(SauceLabs.default.instances[0].options.region).toBe('eu') }) test('onPrepare multiremote', async () => { const options: SauceServiceConfig = { sauceConnect: true, sauceConnectOpts: { tunnelName: 'my-tunnel' } } const caps: Capabilities.RequestedMultiremoteCapabilities = { browserA: { capabilities: { browserName: 'chrome' } }, browserB: { capabilities: { browserName: 'firefox', 'sauce:options': { tunnelName: 'fish' } } } } const config = { user: 'foobaruser', key: '12345' } as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) expect(service['_sauceConnectProcess']).toBeUndefined() await service.onPrepare(config, caps) expect(caps).toEqual({ browserA: { capabilities: { browserName: 'chrome', 'sauce:options': { tunnelName: 'my-tunnel' } } }, browserB: { capabilities: { browserName: 'firefox', 'sauce:options': { tunnelName: 'fish' } }, } }) expect(service['_sauceConnectProcess']).not.toBeUndefined() }) test('onPrepare parallel multiremote', async () => { const options: SauceServiceConfig = { sauceConnect: true, sauceConnectOpts: { tunnelName: 'my-tunnel' } } const caps: Capabilities.RequestedMultiremoteCapabilities[] = [{ browserA: { capabilities: { browserName: 'chrome' } }, browserB: { capabilities: { browserName: 'firefox', 'sauce:options': { tunnelName: 'fish' } } } }, { browserC: { capabilities: { browserName: 'chrome' } }, browserD: { capabilities: { browserName: 'firefox', 'sauce:options': { tunnelName: 'fish' } } } }] const config = { user: 'foobaruser', key: '12345' } as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) expect(service['_sauceConnectProcess']).toBeUndefined() await service.onPrepare(config, caps) expect(caps).toEqual([{ browserA: { capabilities: { browserName: 'chrome', 'sauce:options': { tunnelName: 'my-tunnel' } } }, browserB: { capabilities: { browserName: 'firefox', 'sauce:options': { tunnelName: 'fish' } }, } }, { browserC: { capabilities: { browserName: 'chrome', 'sauce:options': { tunnelName: 'my-tunnel' } } }, browserD: { capabilities: { browserName: 'firefox', 'sauce:options': { tunnelName: 'fish' } }, } }]) expect(service['_sauceConnectProcess']).not.toBeUndefined() }) test('onPrepare if sauceTunnel is not set', async () => { const options: SauceServiceConfig = { sauceConnectOpts: { tunnelName: 'my-tunnel' } } const caps = [{}] as WebdriverIO.Capabilities[] const config = { user: 'foobaruser', key: '12345' } as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) expect(service['_sauceConnectProcess']).toBeUndefined() await service.onPrepare(config, caps) expect(caps).toEqual([{}]) expect(service['_sauceConnectProcess']).toBeUndefined() // @ts-ignore mock feature expect(SauceLabs.default.instances).toHaveLength(1) // @ts-ignore mock feature expect(SauceLabs.default.instances[0].startSauceConnect).toBeCalledTimes(0) }) test('onPrepare multiremote with tunnel identifier and with w3c caps ', async () => { const options: SauceServiceConfig = { sauceConnect: true, sauceConnectOpts: { tunnelName: 'my-tunnel' } } const caps: Capabilities.RequestedMultiremoteCapabilities = { browserA: { capabilities: { browserName: 'chrome', 'sauce:options': { commandTimeout: 600 } } }, browserB: { capabilities: { browserName: 'firefox', 'sauce:options': { commandTimeout: 600, tunnelName: 'fish' } } } } const config = { user: 'foobaruser', key: '12345' } as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) expect(service['_sauceConnectProcess']).toBeUndefined() await service.onPrepare(config, caps) expect(caps).toEqual({ browserA: { capabilities: { browserName: 'chrome', 'sauce:options': { commandTimeout: 600, tunnelName: 'my-tunnel' } } }, browserB: { capabilities: { browserName: 'firefox', 'sauce:options': { commandTimeout: 600, tunnelName: 'fish' } } } }) expect(service['_sauceConnectProcess']).not.toBeUndefined() }) test('onPrepare without tunnel identifier and with w3c caps ', async () => { const options: SauceServiceConfig = { sauceConnect: false } const caps: WebdriverIO.Capabilities[] = [{ browserName: 'chrome', 'sauce:options': { commandTimeout: 600, tunnelName: 'fish' } }, { browserName: 'firefox', 'sauce:options': { commandTimeout: 600 } }] const config = { user: 'foobaruser', key: '12345' } as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) expect(service['_sauceConnectProcess']).toBeUndefined() await service.onPrepare(config, caps) expect(caps).toEqual([{ browserName: 'chrome', 'sauce:options': { commandTimeout: 600, tunnelName: 'fish' } }, { browserName: 'firefox', 'sauce:options': { commandTimeout: 600 } }]) expect(service['_sauceConnectProcess']).toBeUndefined() }) test('onPrepare with tunnel identifier and with w3c caps ', async () => { const options: SauceServiceConfig = { sauceConnect: true, sauceConnectOpts: { tunnelName: 'my-tunnel' } } const caps: WebdriverIO.Capabilities[] = [{ browserName: 'chrome', 'sauce:options': { commandTimeout: 600, tunnelName: 'fish' } }, { browserName: 'firefox', 'sauce:options': { commandTimeout: 600 } }] const config = { user: 'foobaruser', key: '12345' } as Options.Testrunner const service = new SauceServiceLauncher(options, caps as never, config) expect(service['_sauceConnectProcess']).toBeUndefined() await service.onPrepare(config, caps) expect(caps).toEqual([{ browserName: 'chrome', 'sauce:options': { commandTimeout: 600, tunnelName: 'fish' } }, { browserName: 'firefox', 'sauce:options': { commandTimeout: 600, tunnelName: 'my-tunnel' } }]) expect(service['_sauceConnectProcess']).not.toBeUndefined() }) test('startTunnel fail twice and recover', async ()=> { const options: SauceServiceConfig = { sauceConnect: true, sauceConnectOpts: { tunnelName: 'my-tunnel' } } const service = new SauceServiceLauncher(options, undefined as never, {} as Options.Testrunner) vi.mocked(service['_api'].startSauceConnect) .mockRejectedValueOnce(new Error('ENOENT')) .mockRejectedValueOnce(new Error('ENOENT')) await service.startTunnel({}) // @ts-ignore mock feature expect(SauceLabs.default.instances[0].startSauceConnect).toBeCalledTimes(3) }) test('startTunnel fail three and throws error', async ()=> { const options: SauceServiceConfig = { sauceConnect: true, sauceConnectOpts: { tunnelName: 'my-tunnel' } } const service = new SauceServiceLauncher(options, undefined as never, {} as Options.Testrunner) vi.mocked(service['_api'].startSauceConnect) .mockRejectedValueOnce(new Error('ENOENT')) .mockRejectedValueOnce(new Error('ENOENT')) .mockRejectedValueOnce(new Error('ENOENT')) expect(async () => { await service.startTunnel({}) }).rejects.toThrowError('ENOENT') }) test('onComplete', async () => { const service = new SauceServiceLauncher({}, undefined as never, {} as Options.Testrunner) expect(service.onComplete()).toBeUndefined() service['_sauceConnectProcess'] = { close: vi.fn() } as any service.onComplete() expect(service['_sauceConnectProcess']?.close).toBeCalled() }) beforeEach(async () => { vi.mocked(log.info).mockClear() // @ts-ignore mock feature SauceLabs.default.instances = [] })