import path from 'node:path' import { describe, it, expect, vi, afterEach } from 'vitest' import type { Capabilities, Options } from '@wdio/types' import '../src/browser.js' import { isSuccessfulResponse, getPrototype, getSessionError, startWebDriverSession, setupDirectConnect, validateCapabilities } from '../src/utils.js' import type { Client, RemoteConfig } from '../src/types.js' vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) vi.mock('@wdio/utils') vi.mock('fetch') describe('utils', () => { it('isSuccessfulResponse', () => { expect(isSuccessfulResponse(200, { value: { some: 'result' } })).toBe(true) expect(isSuccessfulResponse(404, { value: { error: new Error('foobar' ) } })).toBe(false) expect(isSuccessfulResponse(404, { value: { error: 'no such element' } })).toBe(true) expect(isSuccessfulResponse(404, { value: { message: 'An element could not be located on the page using the given search parameters.' } })).toBe(true) expect(isSuccessfulResponse(200, { status: 7, value: {} })).toBe(false) expect(isSuccessfulResponse(undefined, { status: 7, value: {} })).toBe(false) expect(isSuccessfulResponse(undefined, { status: 0, value: {} })).toBe(true) expect(isSuccessfulResponse( undefined, { status: 7, value: { message: 'no such element: foobar' } } )).toBe(true) expect(isSuccessfulResponse( 200, { value: { message: 'Unable to find element with xpath == //foobar' } } )).toBe(true) }) it('getPrototype', () => { const isChromium = false const isMobile = false const isSauce = false const isIOS = false const isAndroid = false const isSeleniumStandalone = false const webdriverPrototype = getPrototype({ isW3C: true, isChromium, isMobile, isSauce, isSeleniumStandalone, isIOS, isAndroid }) expect(webdriverPrototype instanceof Object).toBe(true) expect(typeof webdriverPrototype.sendKeys).toBe('undefined') expect(typeof webdriverPrototype.sendCommand).toBe('undefined') expect(typeof webdriverPrototype.performActions.value).toBe('function') expect(typeof webdriverPrototype.lock).toBe('undefined') const chromiumPrototype = getPrototype({ isW3C: false, isChromium: true, isMobile, isSauce, isSeleniumStandalone, isIOS, isAndroid }) expect(chromiumPrototype instanceof Object).toBe(true) expect(typeof chromiumPrototype.sendCommand.value).toBe('function') expect(typeof chromiumPrototype.getElementValue.value).toBe('function') expect(typeof chromiumPrototype.elementSendKeys.value).toBe('function') expect(typeof chromiumPrototype.lock).toBe('undefined') const geckoPrototype = getPrototype({ isW3C: true, isChromium: false, isFirefox: true, isMobile, isSauce, isSeleniumStandalone, isIOS, isAndroid }) expect(geckoPrototype instanceof Object).toBe(true) expect(typeof geckoPrototype.setMozContext.value).toBe('function') expect(typeof geckoPrototype.installAddOn.value).toBe('function') expect(typeof geckoPrototype.elementSendKeys.value).toBe('function') expect(typeof geckoPrototype.lock).toBe('undefined') const mobilePrototype = getPrototype({ isW3C: true, isChromium: false, isMobile: true, isSauce, isSeleniumStandalone, isIOS, isAndroid }) expect(mobilePrototype instanceof Object).toBe(true) expect(typeof mobilePrototype.performActions.value).toBe('function') expect(typeof mobilePrototype.sendKeys.value).toBe('function') expect(typeof mobilePrototype.lock.value).toBe('function') expect(typeof mobilePrototype.getNetworkConnection.value).toBe('function') const mobileChromePrototype = getPrototype({ isW3C: true, isChromium: true, isMobile: true, isSauce, isSeleniumStandalone, isIOS, isAndroid }) expect(mobileChromePrototype instanceof Object).toBe(true) expect(typeof mobileChromePrototype.sendCommand.value).toBe('function') expect(typeof mobileChromePrototype.performActions.value).toBe('function') expect(typeof mobileChromePrototype.sendKeys.value).toBe('function') expect(typeof mobileChromePrototype.lock.value).toBe('function') expect(typeof mobileChromePrototype.getNetworkConnection.value).toBe('function') const saucePrototype = getPrototype({ isW3C: true, isChromium, isMobile, isSauce: true, isSeleniumStandalone, isIOS, isAndroid }) expect(saucePrototype instanceof Object).toBe(true) expect(typeof saucePrototype.getPageLogs.value).toBe('function') }) describe('setupDirectConnect', () => { class TestClient implements Client { // @ts-expect-error sessionId?: string // @ts-expect-error requestedCapabilities?: WebdriverIO.Capabilities | Capabilities.W3CCapabilities constructor( public capabilities: WebdriverIO.Capabilities | Capabilities.W3CCapabilities, public options: Options.WebDriver ) { this.capabilities = capabilities this.options = options } } it('should do nothing if params contain no direct connect caps', function () { const client = new TestClient({ platformName: 'baz' }, { hostname: 'bar' } as any) as Client setupDirectConnect(client) expect(client.options.hostname).toEqual('bar') }) it('should do nothing if params contain incomplete direct connect caps', function () { const client = new TestClient({ platformName: 'baz', 'appium:directConnectHost': 'baz' }, { hostname: 'bar' } as any) as Client setupDirectConnect(client) expect(client.options.hostname).toEqual('bar') }) it('should update connection params if caps contain all direct connect fields', function () { const client = new TestClient({ platformName: 'baz', 'appium:directConnectProtocol': 'https', 'appium:directConnectHost': 'bar', 'appium:directConnectPort': 4321, 'appium:directConnectPath': '/' }, { protocol: 'http', hostname: 'foo', port: 1234, path: '' } as any) as Client setupDirectConnect(client) expect(client.options.protocol).toBe('https') expect(client.options.hostname).toBe('bar') expect(client.options.port).toBe(4321) expect(client.options.path).toBe('/') }) it('should update connection params even if path is empty string', function () { const client = new TestClient({ platformName: 'baz', 'appium:directConnectProtocol': 'https', 'appium:directConnectHost': 'bar', 'appium:directConnectPort': 4321, 'appium:directConnectPath': '' }, { protocol: 'http', hostname: 'foo', port: 1234, path: '' } as any) as Client setupDirectConnect(client) expect(client.options.protocol).toBe('https') expect(client.options.hostname).toBe('bar') expect(client.options.port).toBe(4321) expect(client.options.path).toBe('') }) }) describe('getSessionError', () => { it('should return unchanged message', () => { expect(getSessionError({ message: 'foobar', name: 'Error' }, {})).toEqual('foobar') }) it('should return "more info" if no message', () => { expect(getSessionError({ message: '', name: 'Error' }, {})).toEqual('See wdio.* logs for more information.') }) it('should handle not properly set paths', () => { expect(getSessionError({ message: 'unhandled request', name: 'Error' }, {})) .toContain('Make sure you have set the "path" correctly!') }) it('ECONNREFUSED', () => { expect(getSessionError({ name: 'Some Error', code: 'ECONNREFUSED', message: 'ECONNREFUSED 127.0.0.1:4444' }, { protocol: 'https', hostname: 'foobar', port: 1234, path: '/foo/bar' })).toContain('Unable to connect to "https://foobar:1234/foo/bar"') }) it('path: selenium-standalone path', () => { expect( getSessionError( new Error('Whoops! The URL specified routes to this help page.'), {} ) ).toContain("set `path: '/wd/hub'` in") }) it('path: chromedriver, geckodriver, etc', () => { expect(getSessionError(new Error('HTTP method not allowed'))).toContain("set `path: '/'` in") }) it('edge driver localhost issue', () => { expect( getSessionError(new Error('Bad Request - Invalid Hostname 400
HTTP Error 400')) ).toContain('127.0.0.1 instead of localhost') }) it('illegal w3c cap passed to selenium standalone', () => { const message = getSessionError( new Error('Illegal key values seen in w3c capabilities: [chromeOptions]') ) expect(message).toContain('[chromeOptions]') expect(message).toContain('add vendor prefix') }) it('wrong host port, port in use, illegal w3c cap passed to grid', () => { const message = getSessionError(new Error('Response has empty body')) expect(message).toContain('valid hostname:port or the port is not in use') expect(message).toContain('add vendor prefix') }) it('should hint for region issues for free-trial users', () => { const message = getSessionError( new Error('unknown error: failed serving request POST /wd/hub/session: Unauthorized'), { hostname: 'https://ondemand.eu-central-1.saucelabs.com' }) expect(message).toContain('Ensure this region is set in your configuration') }) }) describe('startWebDriverSession', () => { const mockedFetch = vi.mocked(fetch) afterEach(() => { mockedFetch.mockClear() }) it('attaches capabilities to the params object', async () => { const params: RemoteConfig = { hostname: 'localhost', port: 4444, path: '/', protocol: 'http', logLevel: 'warn', capabilities: { browserName: 'chrome', // @ts-expect-error test invalid cap platform: 'Windows' } } const { sessionId, capabilities } = await startWebDriverSession(params) expect(sessionId).toBe('foobar-123') expect(capabilities.browserName) .toBe('mockBrowser') expect(JSON.parse(mockedFetch.mock.calls[0][1]?.body as string).capabilities.alwaysMatch.webSocketUrl) .toBe(true) }) it('should allow to opt-out from bidi', async () => { const params: RemoteConfig = { hostname: 'localhost', port: 4444, path: '/', protocol: 'http', capabilities: { browserName: 'chrome', 'wdio:enforceWebDriverClassic': true } } await startWebDriverSession(params) expect(JSON.parse(mockedFetch.mock.calls[0][1]?.body as string).capabilities.alwaysMatch.webSocketUrl) .toBe(undefined) }) it('should not opt-in for Safari as it is not supported', async () => { const params: RemoteConfig = { hostname: 'localhost', port: 4444, path: '/', protocol: 'http', capabilities: { browserName: 'safari' } } await startWebDriverSession(params) expect(JSON.parse(mockedFetch.mock.calls[0][1]?.body as string).capabilities.alwaysMatch.webSocketUrl) .toBe(undefined) }) it('should not opt-in for Safari as it is not supported', async () => { const params: RemoteConfig = { hostname: 'localhost', port: 4444, path: '/', protocol: 'http', capabilities: { browserName: 'Safari' } } await startWebDriverSession(params) expect(JSON.parse(mockedFetch.mock.calls[0][1]?.body as string).capabilities.alwaysMatch.webSocketUrl) .toBe(undefined) }) it('should allow to opt-out from bidi when using alwaysMatch', async () => { const params: RemoteConfig = { hostname: 'localhost', port: 4444, path: '/', protocol: 'http', capabilities: { alwaysMatch: { browserName: 'chrome', 'wdio:enforceWebDriverClassic': true }, firstMatch: [] } } await startWebDriverSession(params) expect(JSON.parse(mockedFetch.mock.calls[0][1]?.body as string).capabilities.alwaysMatch.webSocketUrl) .toBe(undefined) }) it('should handle sessionRequest error', async () => { const error = await startWebDriverSession({ logLevel: 'warn', capabilities: {} }).catch((err) => err) expect(error.message).toContain('Invalid URL') }) it('should break if JSONWire and WebDriver caps are mixed together', async () => { const params: RemoteConfig = { hostname: 'localhost', port: 4444, path: '/', protocol: 'http', logLevel: 'warn', capabilities: { browserName: 'chrome', 'sauce:options': {}, // @ts-expect-error test invalid cap platform: 'Windows', // @ts-ignore test invalid cap foo: 'bar' } } const err: Error = await startWebDriverSession(params).catch((err) => err) expect(err.message).toContain( 'Invalid or unsupported WebDriver capabilities found ' + '("platform", "foo").' ) }) }) describe('validateCapabilities', () => { it('should throw an error if incognito is defined', () => { expect(() => { validateCapabilities({ browserName: 'chrome', 'goog:chromeOptions': { args: ['--incognito'] } }) }).toThrow('Please remove "incognito" from `"goog:chromeOptions".args`') }) it('should throw an error if incognito is defined as string', () => { expect(() => { validateCapabilities({ browserName: 'chrome', 'goog:chromeOptions': { args: ['incognito'] } }) }).toThrow('Please remove "incognito" from `"goog:chromeOptions".args`') }) it('should not throw an error if incognito is not defined', () => { expect(() => { validateCapabilities({ browserName: 'chrome' }) }).not.toThrow() }) }) })