import path from 'node:path' import cp from 'node:child_process' import fs from 'node:fs/promises' import { vi, describe, it, expect, afterEach, beforeEach, test } from 'vitest' import readDir from 'recursive-readdir' import { readPackageUp } from 'read-pkg-up' import { SevereServiceError } from 'webdriverio' import { ConfigParser } from '@wdio/config/node' import { runLauncherHook, runOnCompleteHook, runServiceHook, getRunnerName, findInConfig, getCapabilities, } from '../src/utils.js' vi.mock('recursive-readdir', () => ({ default: vi.fn().mockResolvedValue([ '/foo/bar/loo/page.js.ejs', '/foo/bar/example.e2e.js' ] as any) })) vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) vi.mock('child_process', () => { const m = { execSyncRes: 'APPIUM_MISSING', execSync: () => m.execSyncRes, exec: vi.fn(), spawn: vi.fn().mockReturnValue({ on: vi.fn().mockImplementation((ev, fn) => fn(0)) }) } return { default: m } }) vi.mock('read-pkg-up') vi.mock('node:fs/promises', () => ({ default: { access: vi.fn().mockRejectedValue(new Error('ENOENT')), mkdir: vi.fn(), writeFile: vi.fn().mockReturnValue(Promise.resolve()) } })) vi.mock('@wdio/config/node', () => ({ ConfigParser: class ConfigParserMock { initialize() { } getCapabilities() { } } })) beforeEach(() => { global.console.log = vi.fn() vi.mocked(readPackageUp).mockResolvedValue({ path: '/foo/package.json', packageJson: { name: 'cool-test-module', type: 'module' } }) }) describe('runServiceHook', () => { const hookSuccess = vi.fn() const slowSetupFn = vi.fn() const asyncHookSuccess = vi.fn().mockImplementation(() => new Promise(resolve => { setTimeout(() => { slowSetupFn() resolve() }, 20) })) beforeEach(() => { hookSuccess.mockClear() slowSetupFn.mockClear() asyncHookSuccess.mockClear() }) it('run sync and async hooks successfully', async () => { await runServiceHook([ { onPrepare: hookSuccess }, { onPrepare: asyncHookSuccess }, // @ts-ignore test invalid parameter { onPrepare: 'foobar' }, ], 'onPrepare', 1, true, 'abc') expect(hookSuccess).toBeCalledTimes(1) expect(asyncHookSuccess).toBeCalledTimes(1) expect(slowSetupFn).toBeCalledTimes(1) }) it('executes all hooks and continues after a hook throws error', async () => { const hookFailing = vi.fn().mockImplementation(() => { throw new Error('buhh') }) await runServiceHook([ { onPrepare: hookSuccess }, // @ts-ignore test invalid parameter { onPrepare: 'foobar' }, { onPrepare: asyncHookSuccess }, { onPrepare: hookFailing }, ], 'onPrepare', 1, true, 'abc') expect(hookSuccess).toBeCalledTimes(1) expect(hookFailing).toBeCalledTimes(1) expect(slowSetupFn).toBeCalledTimes(1) expect(asyncHookSuccess).toBeCalledTimes(1) }) it('executes all hooks and stops after a hook throws SevereServiceError', async () => { const hookFailing = vi.fn().mockImplementation(() => { throw new SevereServiceError() }) try { await runServiceHook([ { onPrepare: hookSuccess }, // @ts-ignore test invalid parameter { onPrepare: 'foobar' }, { onPrepare: asyncHookSuccess }, { onPrepare: hookFailing }, ], 'onPrepare', 1, true, 'abc') } catch (err: any) { expect(err.message).toEqual(expect.stringContaining('SevereServiceError')) expect(err.message).toEqual(expect.stringContaining('Stopping runner...')) expect(hookSuccess).toBeCalledTimes(1) expect(hookFailing).toBeCalledTimes(1) expect(slowSetupFn).toBeCalledTimes(1) expect(asyncHookSuccess).toBeCalledTimes(1) } }) }) test('runLauncherHook handles array of functions', () => { const hookSuccess = vi.fn() const hookFailing = vi.fn().mockImplementation(() => { throw new Error('buhh') }) runLauncherHook([hookSuccess, hookFailing], 1, 2, 3, 4, 5, 6) expect(hookSuccess).toBeCalledTimes(1) expect(hookSuccess).toHaveBeenCalledWith(1, 2, 3, 4, 5, 6) expect(hookFailing).toBeCalledTimes(1) expect(hookFailing).toHaveBeenCalledWith(1, 2, 3, 4, 5, 6) }) test('runLauncherHook handles async functions', async () => { const hookSuccess = () => new Promise(resolve => setTimeout(resolve, 31)) const start = Date.now() await runLauncherHook([hookSuccess], {}, {}) expect(Date.now() - start).toBeGreaterThanOrEqual(30) }) test('runLauncherHook handles a single function', () => { const hookSuccess = vi.fn() runLauncherHook(hookSuccess, 1, 2, 3, 4, 5, 6) expect(hookSuccess).toBeCalledTimes(1) expect(hookSuccess).toHaveBeenCalledWith(1, 2, 3, 4, 5, 6) }) test('runOnCompleteHook handles array of functions', () => { const hookSuccess = vi.fn() const secondHook = vi.fn() runOnCompleteHook([hookSuccess, secondHook], { capabilities: [] }, {}, 0, {} as any) expect(hookSuccess).toBeCalledTimes(1) expect(secondHook).toBeCalledTimes(1) }) test('runOnCompleteHook handles async functions', async () => { const hookSuccess = () => new Promise(resolve => setTimeout(resolve, 31)) const start = Date.now() await runOnCompleteHook([hookSuccess], { capabilities: [] }, {}, 0, {} as any) expect(Date.now() - start).toBeGreaterThanOrEqual(30) }) test('runOnCompleteHook handles a single function', () => { const hookSuccess = vi.fn() runOnCompleteHook(hookSuccess, { capabilities: [] }, {}, 0, {} as any) expect(hookSuccess).toBeCalledTimes(1) }) test('runOnCompleteHook with no failure returns 0', async () => { const hookSuccess = vi.fn() const hookFailing = vi.fn() const result = await runOnCompleteHook([hookSuccess, hookFailing], { capabilities: [] }, {}, 0, {} as any) expect(result).not.toContain(1) expect(hookSuccess).toBeCalledTimes(1) expect(hookFailing).toBeCalledTimes(1) }) test('runOnCompleteHook with failure returns 1', async () => { const hookSuccess = vi.fn() const hookFailing = vi.fn().mockImplementation(() => { throw new Error('buhh') }) const result = await runOnCompleteHook([hookSuccess, hookFailing], { capabilities: [] }, {}, 0, {} as any) expect(result).toContain(1) expect(hookSuccess).toBeCalledTimes(1) expect(hookFailing).toBeCalledTimes(1) }) test('runOnCompleteHook fails with SevereServiceError', async () => { const hookSuccess = vi.fn() const hookFailing = vi.fn().mockImplementation(() => { throw new SevereServiceError('buhh') }) const result = await runOnCompleteHook([hookSuccess, hookFailing], { capabilities: [] }, {}, 0, {} as any) .catch(() => 'some error') expect(result).toBe('some error') expect(hookSuccess).toBeCalledTimes(1) expect(hookFailing).toBeCalledTimes(1) }) test('getRunnerName', () => { expect(getRunnerName({ 'appium:appPackage': 'foobar' })).toBe('foobar') expect(getRunnerName({ 'appium:appWaitActivity': 'foobar' })).toBe('foobar') expect(getRunnerName({ 'appium:app': 'foobar' })).toBe('foobar') expect(getRunnerName({ 'appium:platformName': 'foobar' })).toBe('foobar') expect(getRunnerName({ browserName: 'foobar' })).toBe('foobar') expect(getRunnerName({ platformName: 'foobar' })).toBe('foobar') expect(getRunnerName({})).toBe('undefined') expect(getRunnerName()).toBe('undefined') // @ts-ignore test invalid parameter expect(getRunnerName({ foo: {} })).toBe('undefined') // @ts-ignore test invalid parameter expect(getRunnerName({ foo: { capabilities: [] }, bar: {} })).toBe('undefined') // @ts-ignore test invalid parameter expect(getRunnerName({ foo: { capabilities: [] } })).toBe('MultiRemote') }) describe('findInConfig', () => { it('finds text for services', () => { const str = "services: ['foo', 'bar']," expect(findInConfig(str, 'service')).toMatchObject([ 'services: [\'foo\', \'bar\']' ]) }) it('finds text for frameworks', () => { const str = "framework: 'mocha'" expect(findInConfig(str, 'framework')).toMatchObject([ "framework: 'mocha'" ]) }) }) describe('getCapabilities', () => { it('should return driver with capabilities for android', async () => { expect(await getCapabilities({ option: 'foo.apk' } as any)).toMatchSnapshot() expect(await getCapabilities({ option: 'android' } as any)).toMatchSnapshot() }) it('should return driver with capabilities for ios', async () => { expect(await getCapabilities({ option: 'foo.app', deviceName: 'fooName', udid: 'num', platformVersion: 'fooNum' } as any)) .toMatchSnapshot() expect(await getCapabilities({ option: 'ios' } as any)).toMatchSnapshot() }) it('should return driver with capabilities for desktop', async () => { expect(await getCapabilities({ option: 'chrome' } as any)).toMatchSnapshot() }) it('should throw config not found error', async () => { const initializeMock = vi.spyOn(ConfigParser.prototype, 'initialize') initializeMock.mockImplementationOnce(() => { const error: any = new Error('ups') error.code = 'MODULE_NOT_FOUND' return Promise.reject(error) }) await expect(() => getCapabilities({ option: './test.js', capabilities: 2 } as any)) .rejects.toThrowErrorMatchingSnapshot() initializeMock.mockImplementationOnce(async () => { throw new Error('ups') }) await expect(() => getCapabilities({ option: './test.js', capabilities: 2 } as any)) .rejects.toThrowErrorMatchingSnapshot() }) it('should throw capability not provided', async () => { await expect(() => getCapabilities({ option: '/path/to/config.js' } as any)) .rejects.toThrowErrorMatchingSnapshot() }) it('should through capability not found', async () => { const cap = { browserName: 'chrome' } const getCapabilitiesMock = vi.spyOn(ConfigParser.prototype, 'getCapabilities') getCapabilitiesMock.mockReturnValue([cap, cap, cap, cap, cap]) await expect(() => getCapabilities({ option: '/path/to/config.js', capabilities: 5 } as any)) .rejects.toThrowErrorMatchingSnapshot() }) it('should get capability from wdio.conf.js', async () => { const autoCompileMock = vi.spyOn(ConfigParser.prototype, 'initialize') const getCapabilitiesMock = vi.spyOn(ConfigParser.prototype, 'getCapabilities') getCapabilitiesMock.mockReturnValue([ { browserName: 'chrome' }, { browserName: 'firefox', specs: ['/path/to/some/specs.js'] }, { maxInstances: 5, browserName: 'chrome', 'goog:chromeOptions': { 'args': ['window-size=8000,1200'] } } ] as WebdriverIO.Capabilities) expect(await getCapabilities({ option: '/path/to/config.js', capabilities: 2 } as any)) .toMatchSnapshot() expect(autoCompileMock).toBeCalledTimes(1) }) }) afterEach(() => { vi.mocked(console.log).mockRestore() vi.mocked(readDir).mockClear() vi.mocked(fs.writeFile).mockClear() vi.mocked(cp.spawn).mockClear() vi.mocked(fs.mkdir).mockClear() })