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'
}
}])
expect(SauceLabs.default.instances).toHaveLength(1)
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-')
expect(SauceLabs.default.instances).toHaveLength(1)
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()
expect(SauceLabs.default.instances).toHaveLength(1)
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()
expect(SauceLabs.default.instances).toHaveLength(1)
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()
expect(SauceLabs.default.instances).toHaveLength(1)
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()
expect(SauceLabs.default.instances).toHaveLength(1)
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({})
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()
SauceLabs.default.instances = []
})