import fs from 'node:fs' import os from 'node:os' import path from 'node:path' import { describe, expect, it, vi, beforeEach } from 'vitest' // @ts-expect-error mock feature import { Local, mockStart } from 'browserstack-local' import logger from '@wdio/logger' import { SevereServiceError } from 'webdriverio' import type { Capabilities, Options } from '@wdio/types' import BrowserstackLauncher from '../src/launcher.js' import type { BrowserstackConfig } from '../src/types.js' import * as utils from '../src/util.js' import * as bstackLogger from '../src/bstackLogger.js' import * as FunnelInstrumentation from '../src/instrumentation/funnelInstrumentation.js' import { RERUN_TESTS_ENV, BROWSERSTACK_TESTHUB_UUID, RERUN_ENV } from '../src/constants.js' import * as thUtils from '../src/testHub/utils.js' import TestOpsConfig from '../src/testOps/testOpsConfig.js' vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) vi.mock('browserstack-local') vi.mock('node:fs', () => ({ default: { createReadStream: vi.fn().mockReturnValue({ pipe: vi.fn() }), createWriteStream: vi.fn().mockReturnValue({ pipe: vi.fn() }), stat: vi.fn().mockReturnValue(Promise.resolve({ size: 123 })), readFileSync: vi.fn().mockReturnValue('1234\nsomepath'), existsSync: vi.fn(), truncateSync: vi.fn(), mkdirSync: vi.fn(), writeFileSync: vi.fn() } })) vi.spyOn(utils, 'uploadLogs').mockImplementation((_user, _key, _uuid) => new Promise((resolve) => { resolve('success') })) const bstackLoggerSpy = vi.spyOn(bstackLogger.BStackLogger, 'logToFile') bstackLoggerSpy.mockImplementation(() => {}) vi.spyOn(FunnelInstrumentation, 'sendFinish').mockImplementation(async () => {}) vi.spyOn(FunnelInstrumentation, 'sendStart').mockImplementation(async () => {}) vi.spyOn(bstackLogger.BStackLogger, 'clearLogFile').mockImplementation(() => {}) const instance = TestOpsConfig.getInstance(true, false) // Passing arguments to ensure instance is created const buildHashedId = 'mocktesthubbuilduuid' instance.buildHashedId = buildHashedId vi.spyOn(TestOpsConfig, 'getInstance').mockImplementation(() => instance) const productMap = { 'observability': true, 'accessibility': false, 'percy': false, 'automate': false, 'app_automate': false } vi.spyOn(thUtils, 'getProductMap').mockImplementation(() => { return productMap }) const pkg = await vi.importActual('../package.json') as any const log = logger('test') const error = new Error('I\'m an error!') const sleep = (ms: number = 100) => new Promise((resolve) => setTimeout(resolve, ms)) beforeEach(() => { vi.clearAllMocks() }) describe('onPrepare', () => { const options: BrowserstackConfig = { browserstackLocal: true } const caps: any = [{}] const config = { user: 'foobaruser', key: '12345678901234567890', capabilities: [] } vi.spyOn(utils, 'launchTestSession').mockImplementation(() => {}) vi.spyOn(utils, 'isBStackSession').mockImplementation(() => {return true}) it('should not try to upload app is app is undefined', async () => { const service = new BrowserstackLauncher({ testObservability: false } as any, caps, config) await service.onPrepare(config, caps) expect(log.debug).toHaveBeenCalledWith('app is not defined in browserstack-service config, skipping ...') }) it('should not call local if browserstackLocal is undefined', async () => { const service = new BrowserstackLauncher({ testObservability: false, percy: false } as any, caps, { user: 'foobaruser', key: '12345', capabilities: [] }) await service.onPrepare(config, caps) expect(log.info).toHaveBeenNthCalledWith(1, 'browserstackLocal is not enabled - skipping...') expect(service.browserstackLocal).toBeUndefined() }) it('should not call local if browserstackLocal is false', async () => { const service = new BrowserstackLauncher({ browserstackLocal: false, testObservability: false, percy: false } as any, caps, { user: 'foobaruser', key: '12345', capabilities: [] }) await service.onPrepare(config, caps) expect(log.info).toHaveBeenNthCalledWith(1, 'browserstackLocal is not enabled - skipping...') expect(service.browserstackLocal).toBeUndefined() }) it('should add the "app" property to a multiremote capability if no "bstack:options"', async () => { const options: BrowserstackConfig = { app: 'bs://' } const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = { samsungGalaxy: { capabilities: {} } } await service.onPrepare(config, capabilities) expect(capabilities.samsungGalaxy.capabilities).toEqual({ 'appium:app': 'bs://', 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } }) }) it('should add the "appium:app" property to a multiremote capability if "bstack:options" present', async () => { const options: BrowserstackConfig = { app: 'bs://' } const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = { samsungGalaxy: { capabilities: { 'bstack:options': {} } } } await service.onPrepare(config, capabilities) expect(capabilities.samsungGalaxy.capabilities).toEqual({ 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' }) }) it('should add the "appium:app" property to a multiremote capability if any extension cap present', async () => { const options: BrowserstackConfig = { app: 'bs://' } const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = { samsungGalaxy: { capabilities: { 'appium:chromeOptions': {} } } } await service.onPrepare(config, capabilities as any) expect(capabilities.samsungGalaxy.capabilities).toEqual({ 'appium:app': 'bs://', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } }) }) it('should add the "app" property to an array of capabilities if no "bstack:options"', async () => { const options: BrowserstackConfig = { app: 'bs://' } const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = [{}, {}, {}] await service.onPrepare(config, capabilities) expect(capabilities).toEqual([ { 'app': 'bs://', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }, { 'app': 'bs://', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }, { 'app': 'bs://', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap } ]) }) it('should add the "appium:app" property to an array of capabilities if "bstack:options" present', async () => { const options: BrowserstackConfig = { app: 'bs://' } const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = [{ 'bstack:options': {} }, { 'bstack:options': {} }, { 'bstack:options': {} }] await service.onPrepare(config, capabilities) expect(capabilities).toEqual([ { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' }, { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' }, { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' } ]) }) it('should add the "appium:app" property to an array of capabilities if any extension cap present', async () => { const options: BrowserstackConfig = { app: 'bs://' } const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = [{ 'appium:chromeOptions': {} }, { 'appium:chromeOptions': {} }] await service.onPrepare(config, capabilities as any) expect(capabilities).toEqual([ { 'appium:app': 'bs://', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } }, { 'appium:app': 'bs://', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } } ]) }) it('should add the "appium:app" as custom_id of app to capability object', async () => { const options: BrowserstackConfig = { app: 'custom_id' } const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = [{ 'appium:chromeOptions': {} }, { 'appium:chromeOptions': {} }] await service.onPrepare(config, capabilities as any) expect(capabilities).toEqual([ { 'appium:app': 'custom_id', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } }, { 'appium:app': 'custom_id', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } } ]) }) it('should add the "appium:app" as shareable_id of app to capability object', async () => { const options: BrowserstackConfig = { app: 'user/custom_id' } const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = [{ 'appium:chromeOptions': {} }, { 'appium:chromeOptions': {} }] await service.onPrepare(config, capabilities as any) expect(capabilities).toEqual([ { 'appium:app': 'user/custom_id', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } }, { 'appium:app': 'user/custom_id', 'appium:chromeOptions': {}, 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId } } ]) }) it('should add "appium:app" property with value returned from app upload to capabilities', async () => { const options: BrowserstackConfig = { app: '/some/dummy/file.apk' } const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = [{ 'bstack:options': {} }, { 'bstack:options': {} }, { 'bstack:options': {} }] vi.spyOn(fs, 'existsSync').mockReturnValueOnce(true) vi.spyOn(service, '_uploadApp').mockImplementation(() => Promise.resolve({ app_url: 'bs://' })) await service.onPrepare(config, capabilities) expect(capabilities).toEqual([ { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' }, { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' }, { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' } ]) }) it('should upload app if path property present in appConfig', async() => { const options: BrowserstackConfig = { app: { path: '/path/to/app.apk' } } const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = [{ 'bstack:options': {} }, { 'bstack:options': {} }, { 'bstack:options': {} }] vi.spyOn(fs, 'existsSync').mockReturnValueOnce(true) vi.spyOn(service, '_uploadApp').mockImplementation(() => Promise.resolve({ app_url: 'bs://' })) await service.onPrepare(config, capabilities) expect(capabilities).toEqual([ { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' }, { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' }, { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' } ]) }) it('should upload app along with custom_id if path and custom_id property present in appConfig', async() => { const options: BrowserstackConfig = { app: { path: '/path/to/app.apk', custom_id: 'custom_id' } } const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = [{ 'bstack:options': {} }, { 'bstack:options': {} }, { 'bstack:options': {} }] vi.spyOn(fs, 'existsSync').mockReturnValueOnce(true) vi.spyOn(service, '_uploadApp').mockImplementation(() => Promise.resolve({ app_url: 'bs://', custom_id: 'custom_id', shareable_id: 'foobaruser/custom_id' })) await service.onPrepare(config, capabilities) expect(capabilities).toEqual([ { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' }, { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' }, { 'bstack:options': { buildProductMap: productMap, testhubBuildUuid: buildHashedId }, 'appium:app': 'bs://' } ]) }) it('should throw SevereServiceError if _validateApp fails', async () => { const options = { app: 'bs://' } as BrowserstackConfig & Options.Testrunner const service = new BrowserstackLauncher(options, caps, config) const capabilities = { samsungGalaxy: { capabilities: {} } } vi.spyOn(service, '_validateApp').mockImplementationOnce(() => { throw new Error() } ) try { await service.onPrepare(config, capabilities) } catch (e: any) { expect(e.name).toEqual('SevereServiceError') } }) it('should throw SevereServiceError if fs.existsSync fails', async () => { const options = { app: { path: '/path/to/app.apk', custom_id: 'custom_id' } } as BrowserstackConfig & Options.Testrunner const service = new BrowserstackLauncher(options, caps, config) const capabilities = { samsungGalaxy: { capabilities: {} } } vi.spyOn(service, '_validateApp').mockImplementation(() => Promise.resolve({ app: 'bs://', customId: 'custom_id' })) vi.spyOn(fs, 'existsSync').mockReturnValue(false) try { await service.onPrepare(config, capabilities) } catch (e: any) { expect(e.name).toEqual('SevereServiceError') expect(e.message).toEqual('[Invalid app path] app path ${app.app} is not correct, Provide correct path to app under test') } }) it('should initialize the opts object, and spawn a new Local instance', async () => { const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) await service.onPrepare(config, caps) expect(service.browserstackLocal).toBeDefined() }) it('should add the "browserstack.local" property to a multiremote capability if no "bstack:options"', async () => { const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) const capabilities = { chromeBrowser: { capabilities: {} } } await service.onPrepare(config, capabilities) expect(capabilities.chromeBrowser.capabilities).toEqual({ 'browserstack.local': true, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }) }) it('should add the "browserstack.localIdentifier" property to a multiremote capability if no "bstack:options"', async () => { const service = new BrowserstackLauncher({ browserstackLocal: true, opts: { localIdentifier: 'wdio1' } } as BrowserstackConfig & Options.Testrunner, caps, { user: 'foobaruser', key: '12345', capabilities: [] }) const capabilities = { chromeBrowser: { capabilities: {} } } await service.onPrepare(config, capabilities) expect(capabilities.chromeBrowser.capabilities).toEqual({ 'browserstack.local': true, 'browserstack.localIdentifier': 'wdio1', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }) }) it('should add the "local" property to a multiremote capability inside "bstack:options" if "bstack:options" present', async () => { const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = { chromeBrowser: { capabilities: { 'bstack:options': {} } } } await service.onPrepare(config, capabilities) expect(capabilities.chromeBrowser.capabilities).toEqual({ 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap } }) }) it('should add the "localIdentifier" property to a multiremote capability inside "bstack:options" if "bstack:options" present', async () => { const service = new BrowserstackLauncher({ browserstackLocal: true, opts: { localIdentifier: 'wdio1' } } as BrowserstackConfig & Options.Testrunner, caps, { user: 'foobaruser', key: '12345', capabilities: [] }) const capabilities = { chromeBrowser: { capabilities: { 'bstack:options': {} } } } await service.onPrepare(config, capabilities) expect(capabilities.chromeBrowser.capabilities).toEqual({ 'bstack:options': { local: true, localIdentifier: 'wdio1', testhubBuildUuid: buildHashedId, buildProductMap: productMap } }) }) it('should add the "local" property to a multiremote capability inside "bstack:options" if any extension cap present', async () => { const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = { chromeBrowser: { capabilities: { 'goog:chromeOptions': {} } } } await service.onPrepare(config, capabilities) expect(capabilities.chromeBrowser.capabilities).toEqual({ 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap }, 'goog:chromeOptions': {} }) }) it('should add the "localIdentifier" property to a multiremote capability inside "bstack:options" if any extension cap present', async () => { const service = new BrowserstackLauncher({ browserstackLocal: true, opts: { localIdentifier: 'wdio1' } } as BrowserstackConfig & Options.Testrunner, caps, config) const capabilities = { chromeBrowser: { capabilities: { 'goog:chromeOptions': {} } } } await service.onPrepare(config, capabilities) expect(capabilities.chromeBrowser.capabilities).toEqual({ 'bstack:options': { local: true, localIdentifier: 'wdio1', testhubBuildUuid: buildHashedId, buildProductMap: productMap }, 'goog:chromeOptions': {} }) }) it('should add the "localIdentifier" property to an array of capabilities inside "bstack:options" if "bstack:options" present', async () => { const service = new BrowserstackLauncher({ browserstackLocal: true, opts: { localIdentifier: 'wdio1' } } as BrowserstackConfig & Options.Testrunner, caps, config) const capabilities = [{ 'bstack:options': {} }, { 'bstack:options': {} }, { 'bstack:options': {} }] await service.onPrepare(config, capabilities) expect(capabilities).toEqual([ { 'bstack:options': { local: true, localIdentifier: 'wdio1', testhubBuildUuid: buildHashedId, buildProductMap: productMap } }, { 'bstack:options': { local: true, localIdentifier: 'wdio1', testhubBuildUuid: buildHashedId, buildProductMap: productMap } }, { 'bstack:options': { local: true, localIdentifier: 'wdio1', testhubBuildUuid: buildHashedId, buildProductMap: productMap } } ]) }) it('should add the "browserstack.local" property to an array of capabilities if no "bstack:options"', async () => { const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = [{}, {}, {}] await service.onPrepare(config, capabilities) expect(capabilities).toEqual([ { 'browserstack.local': true, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }, { 'browserstack.local': true, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }, { 'browserstack.local': true, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap } ]) }) it('should add the "browserstack.localIdentifier" property to an array of capabilities if no "bstack:options"', async () => { const service = new BrowserstackLauncher({ browserstackLocal: true, opts: { localIdentifier: 'wdio1' } } as BrowserstackConfig & Options.Testrunner, caps, { user: 'foobaruser', key: '12345', capabilities: [] }) const capabilities = [{}, {}, {}] await service.onPrepare(config, capabilities) expect(capabilities).toEqual([ { 'browserstack.local': true, 'browserstack.localIdentifier': 'wdio1', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }, { 'browserstack.local': true, 'browserstack.localIdentifier': 'wdio1', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }, { 'browserstack.local': true, 'browserstack.localIdentifier': 'wdio1', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap } ]) }) it('should add the "local" property to an array of capabilities inside "bstack:options" if "bstack:options" present', async () => { const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = [{ 'bstack:options': {} }, { 'bstack:options': {} }, { 'bstack:options': {} }] await service.onPrepare(config, capabilities) expect(capabilities).toEqual([ { 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap } }, { 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap } }, { 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap } } ]) }) it('should add the "local" property to an array of capabilities inside "bstack:options" if any extension cap present', async () => { const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = [{ 'ms:edgeOptions': {} }, { 'goog:chromeOptions': {} }, { 'moz:firefoxOptions': {} }] await service.onPrepare(config, capabilities) expect(capabilities).toEqual([ { 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap }, 'ms:edgeOptions': {} }, { 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap }, 'goog:chromeOptions': {} }, { 'bstack:options': { local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap }, 'moz:firefoxOptions': {} } ]) }) it('should add the "buildIdentifier" property to a multiremote capability inside "bstack:options" if "bstack:options" present', async () => { const caps: any = { chromeBrowser: { capabilities: { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } } } } const service = new BrowserstackLauncher({} as any, caps, config) const capabilities = { chromeBrowser: { capabilities: { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } } } } vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null) vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' }) await service.onPrepare(config, capabilities) expect(capabilities.chromeBrowser.capabilities).toEqual({ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#1', testhubBuildUuid: buildHashedId, buildProductMap: productMap } }) }) it('should add the "buildIdentifier" property to an array of capabilities inside "bstack:options" if "bstack:options" present', async () => { const caps: any = [{ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } }, { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } }] const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = [{ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } }, { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } }] vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null) vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' }) await service.onPrepare(config, capabilities) expect(capabilities).toEqual([ { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#1', local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap } }, { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#1', local: true, testhubBuildUuid: buildHashedId, buildProductMap: productMap } }, ]) }) it('should delete the "buildIdentifier" property from Capabilities object', async () => { const caps: any = [{ 'bstack:options': { buildIdentifier: '#${BUILD_NUMBER}' } }] const service = new BrowserstackLauncher({} as any, caps, config) const capabilities = [{ 'bstack:options': { buildIdentifier: '#${BUILD_NUMBER}' } }] vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null) vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' }) await service.onPrepare(config, capabilities as any) expect(capabilities[0]).toEqual({ 'bstack:options': { testhubBuildUuid: buildHashedId, buildProductMap: productMap } }) }) it('should evaluate and set buildIdentifier from service options', async () => { const caps: any = { chromeBrowser: { capabilities: { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: 'test ${BUILD_NUMBER}' } } } } const service = new BrowserstackLauncher({ buildIdentifier: '#${BUILD_NUMBER}' } as BrowserstackConfig & Options.Testrunner, caps, config) const capabilities = { chromeBrowser: { capabilities: { 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: 'test ${BUILD_NUMBER}' } } } } vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null) vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' }) await service.onPrepare(config, capabilities) expect(capabilities.chromeBrowser.capabilities).toEqual({ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#1', testhubBuildUuid: buildHashedId, buildProductMap: productMap } }) }) it('should add "browserstack.buildIdentifier" property in capabilities if no "bstack:options" and buildIdentifier present in capabilities', async () => { const capabilities = [{ build: 'browserstack wdio build', 'browserstack.buildIdentifier': '#${BUILD_NUMBER}' }] const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, capabilities as Capabilities.RemoteCapability, { user: 'foobaruser', key: '12345678901234567890', capabilities: [] }) vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null) vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' }) await service.onPrepare(config, capabilities) expect(capabilities).toEqual([{ build: 'browserstack wdio build', 'browserstack.buildIdentifier': '#1', 'browserstack.local': true, 'browserstack.wdioService': pkg.version, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }]) }) it('should add the "browserstack.buildIdentifier" property in capabilities if no "bstack:options" and passing buildIdentifier in service options', async () => { const capabilities = [{ build: 'browserstack wdio build' }] const service = new BrowserstackLauncher({ buildIdentifier: '#${BUILD_NUMBER}', } as BrowserstackConfig & Options.Testrunner, capabilities as Capabilities.RemoteCapability, { user: 'foobaruser', key: '12345678901234567890', capabilities: [] }) vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null) vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' }) await service.onPrepare(config, capabilities) expect(capabilities).toEqual([{ build: 'browserstack wdio build', 'browserstack.buildIdentifier': '#1', 'browserstack.wdioService': pkg.version, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }]) }) it('should not add "browserstack.buildIdentifier" property in capabilities if no "bstack:options" and "build" not present', async () => { const capabilities = [{}] const service = new BrowserstackLauncher({ buildIdentifier: '#${BUILD_NUMBER}', } as BrowserstackConfig & Options.Testrunner, capabilities as Capabilities.RemoteCapability, { user: 'foobaruser', key: '12345678901234567890', capabilities: [] }) vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' }) await service.onPrepare(config, capabilities) expect(capabilities).toEqual([{ 'browserstack.wdioService': pkg.version, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }]) }) it('should delete "browserstack.buildIdentifier" property from capabilities if no "bstack:options" and "build" not present', async () => { const capabilities = [{ 'browserstack.buildIdentifier': '#${BUILD_NUMBER}' }] const service = new BrowserstackLauncher({ buildIdentifier: '#${BUILD_NUMBER}', } as BrowserstackConfig & Options.Testrunner, capabilities as Capabilities.RemoteCapability, { user: 'foobaruser', key: '12345678901234567890', capabilities: [] }) vi.spyOn(service, '_getLocalBuildNumber').mockImplementation(() => { return '1' }) await service.onPrepare(config, capabilities) expect(capabilities).toEqual([{ 'browserstack.wdioService': pkg.version, 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap }]) }) it('should reject if local.start throws an error', () => { const service = new BrowserstackLauncher(options as any, caps, config) mockStart.mockImplementationOnce((_: never, cb: Function) => cb(error)) return expect(service.onPrepare(config, caps)).rejects.toThrow(error) .then(() => expect(service.browserstackLocal?.start).toHaveBeenCalled()) }) it('should successfully resolve if local.start is successful', async () => { const logInfoMock = vi.spyOn(log, 'info') const options: BrowserstackConfig = { browserstackLocal: true } const service = new BrowserstackLauncher(options as any, caps, config) await service.onPrepare(config, caps) expect(service.browserstackLocal?.start).toHaveBeenCalled() await sleep(100) expect(logInfoMock.mock.calls[1][0]) .toContain('Browserstack Local successfully started after') }) it('should correctly set up this-binding for local.start', async () => { const service = new BrowserstackLauncher(options as any, caps, config) await service.onPrepare(config, caps) expect(mockStart).toHaveBeenCalled() vi.clearAllMocks() }) it('should launch testhub build when accessibility is true', async () => { const launchTestSessionSpy = vi.spyOn(utils, 'launchTestSession') const serviceOptions = { accessibility: true, testObservability: false } const service = new BrowserstackLauncher(serviceOptions as any, caps, config) vi.spyOn(service, '_updateObjectTypeCaps').mockImplementation(() => {}) await service.onPrepare(config, caps) expect(launchTestSessionSpy).toHaveBeenCalledOnce() }) it('should launch testhub build when observability is true', async () => { const launchTestSessionSpy = vi.spyOn(utils, 'launchTestSession') const serviceOptions = { accessibility: false, testObservability: true } const service = new BrowserstackLauncher(serviceOptions as any, caps, config) vi.spyOn(service, '_updateObjectTypeCaps').mockImplementation(() => {}) await service.onPrepare(config, caps) expect(launchTestSessionSpy).toHaveBeenCalledOnce() }) it('should add accessibility options after filtering not allowed caps', async () => { const launchTestSessionSpy = vi.spyOn(utils, 'launchTestSession') const caps: any = [{ 'bstack:options': { buildName: 'browserstack wdio build' } }, { 'bstack:options': { buildName: 'browserstack wdio build' } }] const serviceOptions = { accessibility: true, accessibilityOptions: { wcagVersion: 'wcag2aa', includeTagsInTestingScope: ['@P1'] } } const service = new BrowserstackLauncher(serviceOptions as any, caps, config) const capabilities = [{ 'bstack:options': { buildName: 'browserstack wdio build' } }, { 'bstack:options': { buildName: 'browserstack wdio build' } }] await service.onPrepare(config, capabilities) expect(launchTestSessionSpy).toHaveBeenCalledOnce() expect(capabilities[0]['bstack:options']).toEqual({ buildName: 'browserstack wdio build', accessibilityOptions: { wcagVersion: 'wcag2aa' }, testhubBuildUuid: buildHashedId, buildProductMap: productMap }) vi.clearAllMocks() }) }) describe('onComplete', () => { it('should do nothing if browserstack local is turned on, but not running', () => { const service = new BrowserstackLauncher({} as any, [{}] as any, {} as any) service.browserstackLocal = new Local() const BrowserstackLocalIsRunningSpy = vi.spyOn(service.browserstackLocal, 'isRunning') BrowserstackLocalIsRunningSpy.mockImplementationOnce(() => false) service.onComplete() expect(service.browserstackLocal.stop).not.toHaveBeenCalled() }) it('should kill the process if forcedStop is true', async () => { const service = new BrowserstackLauncher({ forcedStop: true } as any, [{}] as any, {} as any) service.browserstackLocal = new Local() service.browserstackLocal.pid = 102 const killSpy = vi.spyOn(process, 'kill').mockImplementationOnce((pid) => pid as any) expect(await service.onComplete()).toEqual(102) expect(killSpy).toHaveBeenCalled() expect(service.browserstackLocal.stop).not.toHaveBeenCalled() }) it('should reject with an error, if local.stop throws an error', () => { const service = new BrowserstackLauncher({} as any, [{ browserName: '' }] as any, {} as any) service.browserstackLocal = new Local() const BrowserstackLocalStopSpy = vi.spyOn(service.browserstackLocal, 'stop') BrowserstackLocalStopSpy.mockImplementationOnce((cb) => cb(error)) return expect(service.onComplete()).rejects.toThrow(error) .then(() => expect(service.browserstackLocal?.stop).toHaveBeenCalled()) }) it('should properly resolve if everything works', () => { const service = new BrowserstackLauncher({} as any, [{}] as any, {} as any) service.browserstackLocal = new Local() return expect(service.onComplete()).resolves.toBe(undefined) .then(() => expect(service.browserstackLocal?.stop).toHaveBeenCalled()) }) it('should stop accessibility test run on complete', () => { const stopBuildUpstreamSpy = vi.spyOn(utils, 'stopBuildUpstream') vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true) const service = new BrowserstackLauncher({} as any, [{}] as any, {} as any) service.onComplete() expect(stopBuildUpstreamSpy).toHaveBeenCalledTimes(1) }) }) describe('constructor', () => { const options: BrowserstackConfig = { } const config = { user: 'foobaruser', key: '12345678901234567890', capabilities: [], specs: [] } it('should add the "browserstack.wdioService" property to an array of capabilities if no "bstack:options"', async () => { const caps: any = [{}, {}] new BrowserstackLauncher(options as any, caps, config) expect(caps).toEqual([ { 'browserstack.wdioService': pkg.version }, { 'browserstack.wdioService': pkg.version } ]) }) it('should add the "wdioService" property to an array of capabilities inside "bstack:options" if "bstack:options" present', async () => { const caps: any = [{ 'bstack:options': {} }, { 'bstack:options': {} }] new BrowserstackLauncher(options as any, caps, config) expect(caps).toEqual([ { 'bstack:options': { wdioService: pkg.version } }, { 'bstack:options': { wdioService: pkg.version } } ]) }) it('should add the "wdioService" property to an array of capabilities inside "bstack:options" if any extension cap present', async () => { const caps: any = [{ 'moz:firefoxOptions': {} }, { 'goog:chromeOptions': {} }] new BrowserstackLauncher(options as any, caps, config) expect(caps).toEqual([ { 'bstack:options': { wdioService: pkg.version }, 'moz:firefoxOptions': {} }, { 'bstack:options': { wdioService: pkg.version }, 'goog:chromeOptions': {} } ]) }) it('should add the "wdioService" property to object of capabilities inside "bstack:options" if "bstack:options" present', async () => { const caps: any = { browserA: { capabilities: { 'goog:chromeOptions': {}, 'bstack:options': {} } } } new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) expect(caps).toEqual({ 'browserA': { 'capabilities': { 'bstack:options': { 'wdioService': pkg.version }, 'goog:chromeOptions': {} } } }) }) it('should add the "wdioService" property to object of capabilities inside "bstack:options" if any extension cap present', async () => { const caps: any = { browserA: { capabilities: { 'goog:chromeOptions': {} } } } new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) expect(caps).toEqual({ 'browserA': { 'capabilities': { 'bstack:options': { 'wdioService': pkg.version }, 'goog:chromeOptions': {} } } }) }) it('should add the "wdioService" property to object of capabilities inside "bstack:options" if any extension cap not present', async () => { const caps: any = { browserA: { capabilities: {} } } new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) expect(caps).toEqual({ 'browserA': { 'capabilities': { 'browserstack.wdioService': pkg.version } } }) }) it('should add the "accessibility" property to an array of capabilities inside "bstack:options" if "bstack:options" present', async () => { const serviceOptions = { accessibility: true } const caps: any = [{ 'bstack:options': {} }, { 'bstack:options': {} }] new BrowserstackLauncher(serviceOptions as any, caps, config) expect(caps).toEqual([ { 'bstack:options': { wdioService: pkg.version, accessibility: true } }, { 'bstack:options': { wdioService: pkg.version, accessibility: true } } ]) }) it('should add the "accessibility" property to an array of capabilities inside "bstack:options" if any extension cap present', async () => { const serviceOptions = { accessibility: true } const caps: any = [{ 'moz:firefoxOptions': {} }, { 'goog:chromeOptions': {} }] new BrowserstackLauncher(serviceOptions as any, caps, config) expect(caps).toEqual([ { 'bstack:options': { wdioService: pkg.version, accessibility: true }, 'moz:firefoxOptions': {} }, { 'bstack:options': { wdioService: pkg.version, accessibility: true }, 'goog:chromeOptions': {} } ]) }) it('should add the "accessibility" property to object of capabilities inside "bstack:options" if "bstack:options" present', async () => { const serviceOptions = { accessibility: true } const caps: any = { browserA: { capabilities: { 'goog:chromeOptions': {}, 'bstack:options': {} } } } new BrowserstackLauncher(serviceOptions as BrowserstackConfig & Options.Testrunner, caps, config) expect(caps).toEqual({ 'browserA': { 'capabilities': { 'bstack:options': { 'wdioService': pkg.version, 'accessibility': true }, 'goog:chromeOptions': {} } } }) }) it('should add the "accessibility" property to object of capabilities inside "bstack:options" if any extension cap present', async () => { const serviceOptions = { accessibility: true } const caps: any = { browserA: { capabilities: { 'goog:chromeOptions': {} } } } new BrowserstackLauncher(serviceOptions as BrowserstackConfig & Options.Testrunner, caps, config) expect(caps).toEqual({ 'browserA': { 'capabilities': { 'bstack:options': { 'wdioService': pkg.version, 'accessibility': true }, 'goog:chromeOptions': {} } } }) }) it('update spec list if it is a rerun', async () => { process.env[RERUN_ENV] = 'true' process.env[RERUN_TESTS_ENV] = 'demo1.test.js,demo2.test.js' const caps: any = [{ 'bstack:options': {} }, { 'bstack:options': {} }] new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) expect(config.specs).toEqual(['demo1.test.js', 'demo2.test.js']) delete process.env[RERUN_ENV] delete process.env[RERUN_TESTS_ENV] }) describe('#non-bstack session', () => { const spy = vi.spyOn(utils, 'isBStackSession') it('should not add service version to caps', async () => { spy.mockImplementationOnce(() => false) const caps: any = [{}] new BrowserstackLauncher(options as any, caps, config) expect(caps).toEqual([{}]) }) spy.mockImplementation(() => true) }) }) describe('_updateCaps', () => { const options: BrowserstackConfig = { browserstackLocal: true } const caps: any = [{}] const config = { user: 'foobaruser', key: '12345678901234567890', capabilities: [] } it('should throw an error if "capabilities" is not an object/array', () => { const service = new BrowserstackLauncher(options as any, caps, config) const capabilities = 1 expect(() => service._updateCaps(capabilities as any, 'local')) .toThrow(new SevereServiceError('Capabilities should be an object or Array!')) }) it('should update the local cap in capabilities', () => { const options: BrowserstackConfig = { browserstackLocal: true } const service = new BrowserstackLauncher(options as any, caps, config) service._updateCaps(caps, 'local') expect(caps[0]['browserstack.local']).toEqual(true) }) it('should update the localIdentifier cap in capabilities if present in opts', () => { const options: BrowserstackConfig = { browserstackLocal: true, opts: { localIdentifier: 'wdio1' } } const service = new BrowserstackLauncher(options as any, caps, config) service._updateCaps(caps, 'localIdentifier', 'wdio1') expect(caps[0]['browserstack.localIdentifier']).toContain('wdio1') }) it('should update the buildIdentifier cap in capabilities', () => { const options: BrowserstackConfig = { browserstackLocal: true } const service = new BrowserstackLauncher(options as any, caps, config) service._updateCaps(caps, 'buildIdentifier', '#1') expect(caps[0]['browserstack.buildIdentifier']).toEqual('#1') }) it('should update the buildIdentifier cap if bstack:options is not present in caps array', () => { const options: BrowserstackConfig = { browserstackLocal: true } const caps: any = [{ 'ms:edgeOptions': {} }] const service = new BrowserstackLauncher(options as any, caps, config) service._updateCaps(caps, 'buildIdentifier', '#1') expect(caps[0]['bstack:options']['buildIdentifier']).toEqual('#1') }) it('should update buildidentifier in caps object if bstack:options is present', () => { const options: BrowserstackConfig = { browserstackLocal: true } const caps = { chromeBrowser: { capabilities: { 'goog:chromeOptions': {}, 'bstack:options': { buildIdentifier: '123' } } } } const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) service._updateCaps(caps, 'buildIdentifier', '#1') expect(caps.chromeBrowser.capabilities['bstack:options']).toEqual({ 'wdioService': pkg.version, buildIdentifier: '#1' }) }) it('should update buildidentifier in caps object if bstack:options is not present', () => { const options: BrowserstackConfig = { browserstackLocal: true } const caps = { chromeBrowser: { capabilities: {} } } const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) service._updateCaps(caps, 'buildIdentifier', '#1') expect(caps.chromeBrowser.capabilities).toEqual({ 'browserstack.wdioService': pkg.version, 'browserstack.buildIdentifier': '#1' }) }) it('should delete buildidentifier in caps array if value not passed in _updateCaps', () => { const options: BrowserstackConfig = { browserstackLocal: true } const caps = [{ 'bstack:options': { buildIdentifier: '1234' } }] const service = new BrowserstackLauncher(options as any, caps as any, config) service._updateCaps(caps as any, 'buildIdentifier') expect(caps[0]['bstack:options']).toEqual({ 'wdioService': pkg.version }) }) it('should delete buildidentifier in caps object if value not passed in _updateCaps', () => { const options: BrowserstackConfig = { browserstackLocal: true } const caps = { chromeBrowser: { capabilities: { 'bstack:options': { buildIdentifier: '123' } } } } const service = new BrowserstackLauncher(options as any, caps as any, config) service._updateCaps(caps as any, 'buildIdentifier') expect(caps.chromeBrowser.capabilities['bstack:options']).toEqual({ 'wdioService': pkg.version }) }) it('should delete buildidentifier in caps object if value not passed in _updateCaps', () => { const options: BrowserstackConfig = { browserstackLocal: true } const caps = { chromeBrowser: { capabilities: { 'browserstack.buildIdentifier': '#1' } } } const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) service._updateCaps(caps, 'buildIdentifier') expect(caps.chromeBrowser.capabilities).toEqual({ 'browserstack.wdioService': pkg.version }) }) it('should delete buildidentifier in caps object if value not passed in _updateCaps', () => { const options: BrowserstackConfig = { browserstackLocal: true } const caps: any = [{ 'browserstack.buildIdentifier': '#1' }] const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) service._updateCaps(caps, 'buildIdentifier') expect(caps[0]).toEqual({ 'browserstack.wdioService': pkg.version }) }) it('should update localIdentifier in caps object if extension cap is present', () => { const options: BrowserstackConfig = { browserstackLocal: true, opts: { localIdentifier: 'wdio1' } } const caps: any = { chromeBrowser: { capabilities: { 'goog:chromeOptions': {} } } } const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) service._updateCaps(caps, 'localIdentifier', 'wdio1') expect(caps.chromeBrowser.capabilities['bstack:options']).toEqual({ 'wdioService': pkg.version, localIdentifier: 'wdio1' }) }) }) describe('_updateObjectTypeCaps', () => { const options: BrowserstackConfig = { accessibilityOptions: { wcagVersion: 'wcag2a' } } const caps: any = [{}] const config = { user: 'foobaruser', key: '12345678901234567890', capabilities: [] } it('should update the accessibilityOptions cap in capabilities', () => { const service = new BrowserstackLauncher(options as any, caps, config) service._updateObjectTypeCaps(caps, 'accessibilityOptions', { includeIssueType: { bestPractice: true, needsReview: true } }) expect(caps[0]['browserstack.accessibilityOptions'].includeIssueType).toEqual({ bestPractice: true, needsReview: true }) }) it('should update the accessibilityOptions cap if bstack:options is not present in caps array', () => { const caps: any = [{ 'ms:edgeOptions': {} }] const service = new BrowserstackLauncher(options as any, caps, config) service._updateObjectTypeCaps(caps, 'accessibilityOptions', { includeIssueType: { bestPractice: true, needsReview: true } }) expect(caps[0]['bstack:options']['accessibilityOptions'].includeIssueType).toEqual({ bestPractice: true, needsReview: true }) }) it('should update accessibilityOptions in caps object if bstack:options is present', () => { const caps = { chromeBrowser: { capabilities: { 'goog:chromeOptions': {}, 'bstack:options': {} } } } const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) service._updateObjectTypeCaps(caps, 'accessibilityOptions', { includeIssueType: { bestPractice: true, needsReview: true } }) expect(caps.chromeBrowser.capabilities['bstack:options']).toEqual({ 'wdioService': pkg.version, 'accessibilityOptions': { 'includeIssueType': { 'bestPractice': true, 'needsReview': true } } }) }) it('should update accessibilityOptions in caps object if bstack:options is not present', () => { const caps = { chromeBrowser: { capabilities: {} } } const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) service._updateObjectTypeCaps(caps, 'accessibilityOptions', { includeIssueType: { bestPractice: true, needsReview: true } }) expect(caps.chromeBrowser.capabilities).toEqual({ 'browserstack.wdioService': pkg.version, 'browserstack.accessibilityOptions': { includeIssueType: { bestPractice: true, needsReview: true } } }) }) it('should set chromeOptions if capType is goog:chromeOptions and no existing options are present', () => { const value = { args: ['--disable-gpu'] } vi.spyOn(utils, 'validateCapsWithNonBstackA11y').mockImplementation(() => true) const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) service._updateObjectTypeCaps(caps, 'goog:chromeOptions', value) expect(caps[0]['goog:chromeOptions']).toEqual(value) }) it('should merge chromeOptions if capType is goog:chromeOptions and value is provided', () => { const caps: any = [{ 'goog:chromeOptions': { args: ['--headless'] } }] const value = { args: ['--disable-gpu'] } vi.spyOn(utils, 'validateCapsWithNonBstackA11y').mockImplementation(() => true) const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) service._updateObjectTypeCaps(caps, 'goog:chromeOptions', value) expect(caps[0]['goog:chromeOptions']).toEqual({ args: ['--headless', '--disable-gpu'] }) }) it('should update goog:chromeOptions in caps object if value is provided', () => { const caps = { chromeBrowser: { capabilities: { 'goog:chromeOptions': { args: ['--headless'] }, 'bstack:options': {} } } } const value = { args: ['--disable-gpu'] } vi.spyOn(utils, 'validateCapsWithNonBstackA11y').mockImplementation(() => true) const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) service._updateObjectTypeCaps(caps, 'goog:chromeOptions', value) expect(caps.chromeBrowser.capabilities['goog:chromeOptions']).toEqual({ args: ['--headless', '--disable-gpu'] }) }) it('should delete accessibilityOptions in caps array if value not passed in _updateObjectTypeCaps', () => { const caps = [{ 'bstack:options': { accessibilityOptions: { wcagVersion: 'wcag2a' } } }] const service = new BrowserstackLauncher(options as any, caps as any, config) service._updateObjectTypeCaps(caps, 'accessibilityOptions') expect(caps[0]['bstack:options']).toEqual({ 'wdioService': pkg.version }) }) it('should delete accessibilityOptions in caps object if value not passed in _updateObjectTypeCaps', () => { const caps = { chromeBrowser: { capabilities: { 'bstack:options': { accessibilityOptions: { wcagVersion: 'wcag2a' } } } } } const service = new BrowserstackLauncher(options as any, caps as any, config) service._updateObjectTypeCaps(caps, 'accessibilityOptions') expect(caps.chromeBrowser.capabilities['bstack:options']).toEqual({ 'wdioService': pkg.version }) }) it('should delete accessibilityOptions in caps object if value not passed in _updateObjectTypeCaps', () => { const caps = { chromeBrowser: { capabilities: { 'browserstack.accessibilityOptions': { wcagVersion: 'wcag2a' } } } } const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) service._updateObjectTypeCaps(caps, 'accessibilityOptions') expect(caps.chromeBrowser.capabilities).toEqual({ 'browserstack.wdioService': pkg.version }) }) it('should delete accessibilityOptions in caps object if value not passed in _updateCaps', () => { const caps: any = [{ 'browserstack.accessibilityOptions': { wcagVersion: 'wcag2a' } }] const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) service._updateObjectTypeCaps(caps, 'accessibilityOptions') expect(caps[0]).toEqual({ 'browserstack.wdioService': pkg.version }) }) }) describe('_validateApp', () => { const caps: any = [{}] const config = { user: 'foobaruser', key: '12345', capabilities: [] } it('should use id as app value', async() => { const options: BrowserstackConfig = { app: { id: 'bs://' } } const service = new BrowserstackLauncher(options as any, caps, config) const app:any = await service._validateApp(options.app as any) expect(app).toEqual({ app: 'bs://', custom_id: undefined }) }) it('should throw error if more than two property passed in appConfig', async() => { const options: BrowserstackConfig = { app: { custom_id: 'custom_id', id: 'bs://' } } const service = new BrowserstackLauncher(options as any, caps, config) try { await service._validateApp(options.app as any) } catch (err) { const e = err as Error expect(e.message).toEqual(`keys ${Object.keys(options.app as any)} can't co-exist as app values, use any one property from {id, path, custom_id, shareable_id}, only "path" and "custom_id" can co-exist.`) } }) it('should throw error if property not matches path and custom_id in appConfig', async() => { const options: BrowserstackConfig = { app: { custom_id: 'custom_id', id: 'bs://' } } const service = new BrowserstackLauncher(options as any, caps, config) try { await service._validateApp(options.app as any) } catch (err) { const e = err as Error expect(e.message).toEqual(`keys ${Object.keys(options.app as any)} can't co-exist as app values, use any one property from {id, path, custom_id, shareable_id}, only "path" and "custom_id" can co-exist.`) } }) it('should throw error if appConfig is invalid format', async() => { const options: BrowserstackConfig = { app: {} } const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) try { await service._validateApp(options.app as any) } catch (e: any){ expect(e.message).toEqual('[Invalid format] app should be string or an object') } }) it('should throw error if appConfig is invalid format', async() => { const options: BrowserstackConfig = { app: { key1: '2' } } as any const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) try { await service._validateApp(options.app as any) } catch (e: any){ expect(e.message).toEqual(`[Invalid app property] supported properties are {id, path, custom_id, shareable_id}. For more details please visit https://www.browserstack.com/docs/app-automate/appium/set-up-tests/specify-app ')`) } }) }) describe('_uploadApp', () => { const options: BrowserstackConfig = { app: '/path/to/app.apk' } const caps: any = [{}] const config = { user: 'foobaruser', key: '12345', capabilities: [] } it('should upload the app and return app_url', async() => { vi.mocked(fetch).mockReturnValueOnce(Promise.resolve(Response.json({ app_url: 'bs://' }))) const service = new BrowserstackLauncher(options as any, caps, config) const res = await service._uploadApp(options.app as any) expect(res).toEqual({ app_url: 'bs://' }) }) it('throw SevereServiceError if upload fails', async() => { vi.mocked(fetch).mockReturnValueOnce(Promise.resolve(Response.json({}, { status: 500 }))) const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) try { await service._uploadApp(options.app as any) } catch (e: any) { expect(vi.mocked(fetch).mock.calls[0][1]?.method).toEqual('POST') expect(e.name).toEqual('SevereServiceError') } }) }) describe('_handleBuildIdentifier', () => { const options: BrowserstackConfig = { browserstackLocal: true } const config = { user: 'foobaruser', key: '12345678901234567890', capabilities: [] } it('should update ${BUILD_NUMBER}', async() => { const caps: any = [{ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } }] const service = new BrowserstackLauncher(options as any, caps, config) vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null) vi.spyOn(service, '_getLocalBuildNumber').mockReturnValueOnce('1') vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => {}) service._handleBuildIdentifier(caps) expect(caps[0]['bstack:options']?.buildIdentifier).toEqual('#1') }) it('should update ${DATE_TIME}', async() => { const caps: any = [{ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '${DATE_TIME}' } }] const service = new BrowserstackLauncher(options as any, caps, config) vi.spyOn(service, '_getLocalBuildNumber').mockReturnValueOnce(null) vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => {}) service._handleBuildIdentifier(caps) expect(caps[0]['bstack:options']?.buildIdentifier).not.toEqual('${DATE_TIME}') }) it('should update ${DATE_TIME} and ${BUILD_NUMBER}', async() => { const caps: any = [{ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER} ${DATE_TIME}' } }] const service = new BrowserstackLauncher(options as any, caps, config) vi.spyOn(service, '_getLocalBuildNumber').mockReturnValueOnce('1') vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => {}) service._handleBuildIdentifier(caps) expect(caps[0]['bstack:options']?.buildIdentifier).not.toEqual('${DATE_TIME}') expect(caps[0]['bstack:options']?.buildIdentifier).toContain('#1') }) it('should update ${BUILD_NUMBER} in case of CI', async() => { process.env.JENKINS_URL = 'https://jenkins-url' process.env.JENKINS_HOME = '~/.jenkins' process.env.BUILD_NUMBER = '121' const caps: any = [{ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '${BUILD_NUMBER}' } }] const service = new BrowserstackLauncher(options as any, caps, config) service._handleBuildIdentifier(caps) expect(caps[0]['bstack:options']?.buildIdentifier).toContain('CI 121') delete process.env.JENKINS_URL delete process.env.JENKINS_HOME delete process.env.BUILD_NUMBER }) it('should delete buildIdentifier if buildName is not present in caps', async() => { const caps: any = [{ 'bstack:options': { buildIdentifier: '#${BUILD_NUMBER}' } }] const updatedcaps: any = [{ 'bstack:options': { wdioService: pkg.version } }] const service = new BrowserstackLauncher(options as any, caps, config) service._handleBuildIdentifier(caps) expect(caps[0]).toMatchObject(updatedcaps[0]) }) it('should delete buildIdentifier if BROWSERSTACK_BUILD_NAME is defined as env var', async() => { process.env.BROWSERSTACK_BUILD_NAME = 'browserstack wdio build' const caps: any = [{ 'bstack:options': { buildIdentifier: '#${BUILD_NUMBER}' } }] const updatedcaps: any = [{ 'bstack:options': { wdioService: pkg.version } }] const service = new BrowserstackLauncher(options as any, caps, config) service._handleBuildIdentifier(caps) expect(caps[0]).toMatchObject(updatedcaps[0]) delete process.env.BROWSERSTACK_BUILD_NAME }) it('should not evaluate buildIdentifier if buildIdentifier is not present in the caps', async() => { const caps: any = [{}] const updatedcaps: any = [{ 'browserstack.wdioService': pkg.version }] const service = new BrowserstackLauncher(options as any, caps, config) service._handleBuildIdentifier(caps) expect(caps[0]).toMatchObject(updatedcaps[0]) }) it('should return if localBuildNumber is null', async() => { const caps: any = [{ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } }] const service = new BrowserstackLauncher(options as any, caps, config) vi.spyOn(service, '_getLocalBuildNumber').mockReturnValueOnce(null) vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null) service._handleBuildIdentifier(caps) expect(caps[0]['bstack:options']?.buildIdentifier).toEqual('#${BUILD_NUMBER}') }) }) describe('_getLocalBuildNumber', () => { const options: BrowserstackConfig = { browserstackLocal: true } const config = { user: 'foobaruser', key: '12345', capabilities: [] } const caps: any = [{ 'bstack:options': { buildName: 'browserstack wdio build', buildIdentifier: '#${BUILD_NUMBER}' } }] const service = new BrowserstackLauncher(options as any, caps, config) vi.spyOn(fs, 'existsSync').mockReturnValue(true) it('returns 1 in case of buildName key not present in json file', async() => { vi.spyOn(fs, 'existsSync').mockReturnValue(true) vi.spyOn(fs, 'readFileSync').mockReset().mockReturnValue(JSON.stringify({ 'browserstack wdio build test': { 'identifier': 2 } })) vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => {}) const buildNumber = service._getLocalBuildNumber() expect(buildNumber).toEqual('1') }) it('returns new identifier in case of buildName key is present in json file', async() => { vi.spyOn(fs, 'existsSync').mockReturnValue(true) vi.spyOn(fs, 'readFileSync').mockReset().mockReturnValue(JSON.stringify({ 'browserstack wdio build': { 'identifier': 2 } })) vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => {}) const buildNumber = service._getLocalBuildNumber() expect(buildNumber).toEqual('3') }) it('returns null in case of caught exception', async() => { vi.spyOn(fs, 'existsSync').mockReturnValue(true) vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => { throw new Error('Unable to parse JSON file') }) const buildNumber = service._getLocalBuildNumber() expect(buildNumber).toEqual(null) }) }) describe('_updateLocalBuildCache', () => { const options: BrowserstackConfig = { browserstackLocal: true } const config = { user: 'foobaruser', key: '12345', capabilities: [] } const caps: any = [{ 'bstack:options': { buildName: 'browserstack wdio build test', buildIdentifier: '#${BUILD_NUMBER}' } }] const service = new BrowserstackLauncher(options as any, caps, config) it('updates buildIdentifier in json file', async() => { vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {}) vi.spyOn(fs, 'readFileSync').mockReset().mockReturnValue(JSON.stringify({ 'browserstack wdio build test' : { 'identifier' : 3 } })) const browserstackFolderPath = path.join(os.homedir(), '.browserstack') const filePath = path.join(browserstackFolderPath, '.build-name-cache.json') service._updateLocalBuildCache(filePath, 'browserstack wdio build test', 3) const buildCacheFileData = fs.readFileSync(filePath) const parsedBuildCacheFileData = JSON.parse(buildCacheFileData.toString()) expect(parsedBuildCacheFileData['browserstack wdio build test']['identifier']).toEqual(3) }) it('should not update buildIdentifier in json file', async() => { const writeFileSyncSpy = vi.spyOn(fs, 'writeFileSync') writeFileSyncSpy.mockImplementation(() => {}) vi.spyOn(fs, 'readFileSync').mockReset().mockReturnValue(JSON.stringify({ 'browserstack wdio build test' : { 'identifier' : 3 } })) const browserstackFolderPath = path.join(os.homedir(), '.browserstack') const filePath = path.join(browserstackFolderPath, '.build-name-cache.json') service._updateLocalBuildCache(filePath, undefined, 3) expect(writeFileSyncSpy).not.toHaveBeenCalled() }) }) describe('_uploadServiceLogs', () => { const options: BrowserstackConfig = { } const config = { user: 'foobaruser', key: '12345', capabilities: [] } const caps: any = [{ 'bstack:options': { buildName: 'browserstack wdio build test', buildIdentifier: '#${BUILD_NUMBER}' } }] it('get observability build id', async() => { process.env[BROWSERSTACK_TESTHUB_UUID] = 'obs123' expect(service._getClientBuildUuid()).toEqual('obs123') delete process.env[BROWSERSTACK_TESTHUB_UUID] }) const service = new BrowserstackLauncher(options as any, caps, config) }) describe('_getClientBuildUuid', () => { const options: BrowserstackConfig = { } const config = { user: 'foobaruser', key: '12345', capabilities: [] } const caps: any = [{ 'bstack:options': { buildName: 'browserstack wdio build test', buildIdentifier: '#${BUILD_NUMBER}' } }] const service = new BrowserstackLauncher(options as any, caps, config) it('get observability build id', async() => { process.env[BROWSERSTACK_TESTHUB_UUID] = 'obs123' expect(service._getClientBuildUuid()).toEqual('obs123') delete process.env[BROWSERSTACK_TESTHUB_UUID] }) it('get randomly generated id if both the conditions fail', async() => { vi.mock('uuid', () => ({ v4: () => '123456789' })) expect(service._getClientBuildUuid()).toEqual('123456789') }) })