import path from 'node:path' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import logger from '@wdio/logger' import BrowserstackService from '../src/service.js' import * as utils from '../src/util.js' import InsightsHandler from '../src/insights-handler.js' import * as bstackLogger from '../src/bstackLogger.js' const jasmineSuiteTitle = 'Jasmine__TopLevel__Suite' const sessionBaseUrl = 'https://api.browserstack.com/automate/sessions' const sessionId = 'session123' const sessionIdA = 'session456' vi.mock('fetch') vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) vi.useFakeTimers().setSystemTime(new Date('2020-01-01')) vi.mock('uuid', () => ({ v4: () => '123456789' })) const bstackLoggerSpy = vi.spyOn(bstackLogger.BStackLogger, 'logToFile') bstackLoggerSpy.mockImplementation(() => {}) const log = logger('test') let service: BrowserstackService let browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser const user = 'foo' const key = 'bar' const encodedAuth = Buffer.from(`${user}:${key}`, 'utf8').toString('base64') const headers: any = { 'Content-Type': 'application/json; charset=utf-8', Authorization: `Basic ${encodedAuth}`, } beforeEach(() => { vi.mocked(log.info).mockClear() vi.mocked(fetch).mockClear() vi.mocked(fetch).mockReturnValue(Promise.resolve(Response.json({ automation_session: { browser_url: 'https://www.browserstack.com/automate/builds/1/sessions/2' } }))) browser = { execute: vi.fn(), executeScript: vi.fn(), on: vi.fn(), sessionId: sessionId, config: {}, capabilities: { device: '', os: 'OS X', os_version: 'Sierra', browserName: 'chrome' }, instances: ['browserA', 'browserB'], isMultiremote: false, getInstance: vi.fn().mockImplementation((browserName: string) => browser[browserName]), browserA: { sessionId: sessionIdA, capabilities: { 'bstack:options': { device: '', os: 'Windows', osVersion: 10, browserName: 'chrome' } } }, browserB: {}, } as unknown as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser service = new BrowserstackService({ testObservability: false } as any, [] as any, { user: 'foo', key: 'bar' } as any) }) function assertMethodCalls(mock: { mock: { calls: any[] } }, expectedMethod: any, expectedCallCount: any) { const matchingCalls = mock.mock.calls.filter( ([, options]) => options.method === expectedMethod ) expect(matchingCalls.length).toBe(expectedCallCount) } it('should initialize correctly', () => { service = new BrowserstackService({} as any, [] as any, {} as any) expect(service['_failReasons']).toEqual([]) }) describe('onReload()', () => { it('should update and get session', async () => { const isBrowserstackSessionSpy = vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true) const updateSpy = vi.spyOn(service, '_update') service['_browser'] = browser await service.onReload('1', '2') expect(updateSpy).toHaveBeenCalled() expect(vi.mocked(fetch).mock.calls[0][1]?.method).toEqual('PUT') expect(isBrowserstackSessionSpy).toHaveBeenCalled() }) it('should update and get multiremote session', async () => { const isBrowserstackSessionSpy = vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true) browser.isMultiremote = true as any service['_browser'] = browser const updateSpy = vi.spyOn(service, '_update') await service.onReload('1', '2') expect(updateSpy).toHaveBeenCalled() expect(vi.mocked(fetch).mock.calls[0][1]?.method).toEqual('PUT') expect(isBrowserstackSessionSpy).toHaveBeenCalled() }) it('should reset failures', async () => { const updateSpy = vi.spyOn(service, '_update') service['_browser'] = browser service['_failReasons'] = ['Custom Error: Button should be enabled', 'Expected something'] await service.onReload('1', '2') expect(updateSpy).toHaveBeenCalledWith('1', { status: 'failed', reason: 'Custom Error: Button should be enabled' + '\n' + 'Expected something' }) expect(service['_failReasons']).toEqual([]) }) it('should return if no browser object', async () => { const updateSpy = vi.spyOn(service, '_update') service['_browser'] = undefined await service.onReload('1', '2') expect(updateSpy).toBeCalledTimes(0) }) it ('should not reset suiteTitle', async() => { const updateSpy = vi.spyOn(service, '_update') service['_browser'] = browser service['_failReasons'] = [] service['_suiteTitle'] = 'my suite title' await service.onReload('1', '2') expect(updateSpy).toHaveBeenCalledWith('1', { status: 'passed', }) expect(service['_suiteTitle']).toEqual('my suite title') }) }) describe('beforeSession', () => { describe('testObservabilityOpts not passed', () => { it('should set some default to make missing user and key parameter apparent', () => { service.beforeSession({} as any) expect(service['_config']).toEqual({ user: 'NotSetUser', key: 'NotSetKey' }) }) it('should set username default to make missing user parameter apparent', () => { service.beforeSession({ user: 'foo' } as any) expect(service['_config']).toEqual({ user: 'foo', key: 'NotSetKey' }) }) it('should set key default to make missing key parameter apparent', () => { service.beforeSession({ key: 'bar' } as any) expect(service['_config']).toEqual({ user: 'NotSetUser', key: 'bar' }) }) }) describe('testObservabilityOpts passed', () => { it('should not set some default value if user and key in observability options', () => { const observabilityService = new BrowserstackService( { testObservability: true, testObservabilityOptions: { user: 'foo', key: 'random', } } as any, [] as any, { user: 'foo', key: 'bar' } as any ) observabilityService.beforeSession({} as any) expect(observabilityService['_config']).toEqual({ user: undefined, key: undefined }) }) it('should set set some default value if user and key not in observability options', () => { const observabilityService = new BrowserstackService( { testObservability: true, testObservabilityOptions: {} } as any, [] as any, { user: 'foo', key: 'bar' } as any ) observabilityService.beforeSession({} as any) expect(observabilityService['_config']).toEqual({ user: 'NotSetUser', key: 'NotSetKey' }) }) }) }) describe('_multiRemoteAction', () => { it('resolve if no browser object', () => { const tmpService = new BrowserstackService({ testObservability: false }, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: false } } as any) tmpService['_browser'] = undefined expect(tmpService._multiRemoteAction({} as any)).toEqual(Promise.resolve()) }) }) describe('_update', () => { describe('should call fetch with put method', () => { const getCloudProviderSpy = vi.spyOn(utils, 'getCloudProvider').mockReturnValue('browserstack') beforeEach(() => { getCloudProviderSpy.mockClear() }) it('should resolve if not a browserstack session', () => { service['_browser'] = browser service._update('sessionId', {}) expect(vi.mocked(fetch).mock.calls[0][1]?.method).toEqual('PUT') }) afterEach(() => { getCloudProviderSpy.mockClear() }) }) }) describe('_printSessionURL', () => { it('should get and log session details', async () => { browser.isMultiremote = false service['_browser'] = browser const logInfoSpy = vi.spyOn(log, 'info').mockImplementation((string) => string) const isBrowserstackSessionSpy = vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true) await service._printSessionURL() expect(fetch).toHaveBeenCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'GET', headers }) expect(logInfoSpy).toHaveBeenCalled() expect(logInfoSpy).toHaveBeenCalledWith( 'OS X Sierra chrome session: https://www.browserstack.com/automate/builds/1/sessions/2' ) expect(isBrowserstackSessionSpy).toHaveBeenCalled() }) it('should get and log multi remote session details', async () => { browser.isMultiremote = true as any service['_browser'] = browser const logInfoSpy = vi.spyOn(log, 'info').mockImplementation((string) => string) const isBrowserstackSessionSpy = vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true) await service._printSessionURL() expect(fetch).toHaveBeenCalledWith(`${sessionBaseUrl}/${sessionIdA}.json`, { method: 'GET', headers }) expect(logInfoSpy).toHaveBeenCalled() expect(logInfoSpy).toHaveBeenCalledWith( 'Windows 10 chrome session: https://www.browserstack.com/automate/builds/1/sessions/2' ) expect(isBrowserstackSessionSpy).toHaveBeenCalled() }) describe('if cant print', () => { describe('no browser object', () => { const logInfoSpy = vi.spyOn(log, 'info').mockImplementation((string) => string) const isBrowserstackSessionSpy = vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true) beforeEach(() => { logInfoSpy.mockClear() isBrowserstackSessionSpy.mockClear() }) it('should resolve if not no browser object', async () => { service['_browser'] = undefined await service._printSessionURL() expect(isBrowserstackSessionSpy).toBeCalledTimes(0) expect(logInfoSpy).toBeCalledTimes(0) }) afterEach(() => { logInfoSpy.mockClear() isBrowserstackSessionSpy.mockClear() }) }) }) }) describe('_printSessionURL Appium', () => { beforeEach(() => { vi.mocked(fetch).mockReturnValueOnce(Promise.resolve(Response.json({ automation_session: { name: 'Smoke Test', duration: 65, os: 'ios', os_version: '12.1', browser_version: 'app', browser: null, device: 'iPhone XS', status: 'failed', reason: 'CLIENT_STOPPED_SESSION', browser_url: 'https://app-automate.browserstack.com/builds/1/sessions/2' } }))) browser.capabilities = { device: 'iPhone XS', os: 'iOS', os_version: '12.1', browserName: '', } }) it('should get and log session details', async () => { service['_browser'] = browser await service._printSessionURL() expect(log.info).toHaveBeenCalled() expect(log.info).toHaveBeenCalledWith( 'iPhone XS iOS 12.1 session: https://app-automate.browserstack.com/builds/1/sessions/2' ) }) }) describe('_printSessionURL TurboScale', () => { beforeEach(() => { vi.mocked(fetch).mockReturnValueOnce(Promise.resolve(Response.json({ name: 'Smoke Test', duration: 65, browser_version: '116', browser: 'chrome', status: 'failed', reason: 'CLIENT_STOPPED_SESSION', url: 'https://grid.browserstack.com/dashboard/builds/1/sessions/2' }))) browser.capabilities = { browserName: 'chrome', browserVersion: '116' } }) it('should get and log session details', async () => { service['_browser'] = browser service['_turboScale'] = true await service._printSessionURL() expect(log.info).toHaveBeenCalledWith( 'chrome 116 session: https://grid.browserstack.com/dashboard/builds/1/sessions/2' ) }) }) describe('before', () => { it('should set auth to default values if not provided', async () => { let service = new BrowserstackService({} as any, [{}] as any, { capabilities: {} }) await service.beforeSession({} as unknown as any) await service.before(service['_config'] as any, [], browser) expect(service['_failReasons']).toEqual([]) expect(service['_config'].user).toEqual('NotSetUser') expect(service['_config'].key).toEqual('NotSetKey') service = new BrowserstackService({} as any, [{}] as any, { capabilities: {} }) service.beforeSession({ user: 'blah' } as unknown as any) await service.before(service['_config'] as any, [], browser) expect(service['_failReasons']).toEqual([]) expect(service['_config'].user).toEqual('blah') expect(service['_config'].key).toEqual('NotSetKey') service = new BrowserstackService({} as any, [{}] as any, { capabilities: {} }) service.beforeSession({ key: 'blah' } as unknown as any) await service.before(service['_config'] as any, [], browser) expect(service['_failReasons']).toEqual([]) expect(service['_config'].user).toEqual('NotSetUser') expect(service['_config'].key).toEqual('blah') }) it('should initialize correctly', () => { const service = new BrowserstackService({} as any, [{}] as any, { user: 'foo', key: 'bar', capabilities: {} }) service.before(service['_config'] as any, [], browser) expect(service['_failReasons']).toEqual([]) expect(service['_sessionBaseUrl']).toEqual(sessionBaseUrl) }) it('should initialize correctly for multiremote', () => { const service = new BrowserstackService( {} as any, [{}] as any, { user: 'foo', key: 'bar', capabilities: [{}] } ) service.before(service['_config'] as any, [], browser) expect(service['_failReasons']).toEqual([]) expect(service['_sessionBaseUrl']).toEqual(sessionBaseUrl) }) it('should initialize correctly for appium', () => { const service = new BrowserstackService( {} as any, [{ app: 'test-app' }] as any, { user: 'foo', key: 'bar', capabilities: { app: 'test-app' } as any } ) browser.capabilities = { 'appium:app': 'test-app', device: 'iPhone XS', os: 'iOS', os_version: '12.1', browserName: '', } service.before(service['_config'] as any, [], browser) expect(service['_failReasons']).toEqual([]) expect(service['_sessionBaseUrl']).toEqual('https://api-cloud.browserstack.com/app-automate/sessions') }) it('should initialize correctly for appium without global browser capabilities', () => { const service = new BrowserstackService({} as any, { 'appium:app': 'bs://BrowserStackMobileAppId' }, { user: 'foo', key: 'bar', capabilities: { app: 'test-app' as any } }) service.before(service['_config'] as any, [], browser) expect(service['_failReasons']).toEqual([]) expect(service['_sessionBaseUrl']).toEqual('https://api-cloud.browserstack.com/app-automate/sessions') }) it('should initialize correctly for appium if using valid W3C Webdriver capabilities', () => { const service = new BrowserstackService({} as any, { 'appium:app': 'bs://BrowserStackMobileAppId' }, { user: 'foo', key: 'bar', capabilities: { ['appium:app']: 'test-app' } as any }) service.before(service['_config'] as any, [], browser) expect(service['_failReasons']).toEqual([]) expect(service['_sessionBaseUrl']).toEqual('https://api-cloud.browserstack.com/app-automate/sessions') }) it('should log the url', async () => { const service = new BrowserstackService({} as any, [{}] as any, { capabilities: {} }) await service.before(service['_config'] as any, [], browser) expect(log.info).toHaveBeenCalled() expect(log.info).toHaveBeenCalledWith( 'OS X Sierra chrome session: https://www.browserstack.com/automate/builds/1/sessions/2') }) it('should initialize correctly for turboScale when option passed', () => { const service = new BrowserstackService({ turboScale: true } as any, {}, { user: 'foo', key: 'bar', capabilities: {} }) service.before(service['_config'] as any, [], browser) expect(service['_failReasons']).toEqual([]) expect(service['_sessionBaseUrl']).toEqual('https://api.browserstack.com/automate-turboscale/v1/sessions') }) it('should initialize correctly for turboScale when env var is set', () => { process.env.BROWSERSTACK_TURBOSCALE = 'true' const service = new BrowserstackService({} as any, {}, { user: 'foo', key: 'bar', capabilities: {} }) service.before(service['_config'] as any, [], browser) delete process.env.BROWSERSTACK_TURBOSCALE expect(service['_failReasons']).toEqual([]) expect(service['_sessionBaseUrl']).toEqual('https://api.browserstack.com/automate-turboscale/v1/sessions') }) }) describe('beforeHook', () => { service = new BrowserstackService({}, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: false } } as any) it('call insightsHandler.beforeHook', () => { service['_insightsHandler'] = new InsightsHandler(browser) const methodSpy = vi.spyOn(service['_insightsHandler'], 'beforeHook') service.beforeHook({ title: 'foo2', parent: 'bar2' } as any, {} as any) expect(methodSpy).toBeCalled() }) }) describe('afterHook', () => { service = new BrowserstackService({}, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: false } } as any) it('call insightsHandler.afterHook', () => { service['_insightsHandler'] = new InsightsHandler(browser) const methodSpy = vi.spyOn(service['_insightsHandler'], 'afterHook') service.afterHook({ title: 'foo2', parent: 'bar2' } as any, undefined as never, {} as any) expect(methodSpy).toBeCalled() }) }) describe('beforeStep', () => { const service = new BrowserstackService({}, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: false } } as any) it('call insightsHandler.beforeStep', () => { vi.spyOn(utils, 'getUniqueIdentifierForCucumber').mockReturnValue('test title') service['_insightsHandler'] = new InsightsHandler(browser) const methodSpy = vi.spyOn(service['_insightsHandler'], 'beforeStep') service.beforeStep({ keyword: 'Given', text: 'this is a test' } as any, undefined as never) expect(methodSpy).toBeCalled() }) }) describe('afterStep', () => { const service = new BrowserstackService({}, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: false } } as any) it('call insightsHandler.afterStep', () => { vi.spyOn(utils, 'getUniqueIdentifierForCucumber').mockReturnValue('test title') service['_insightsHandler'] = new InsightsHandler(browser) const methodSpy = vi.spyOn(service['_insightsHandler'], 'afterStep') service.afterStep({ title: 'foo2', parent: 'bar2' } as any, undefined as never, {} as any) expect(methodSpy).toBeCalled() }) }) describe('beforeScenario', () => { const service = new BrowserstackService({}, [] as any, { user: 'foo', key: 'bar' } as any) it('call insightsHandler.beforeScenario', async () => { service['_insightsHandler'] = new InsightsHandler(browser) service['_accessibilityHandler'] = undefined vi.spyOn(utils, 'getUniqueIdentifierForCucumber').mockReturnValue('test title') const methodSpy = vi.spyOn(service['_insightsHandler'], 'beforeScenario') await service.beforeScenario({ pickle: { name: '', tags: [] }, gherkinDocument: { uri: '', feature: { name: '', description: '' } } } as any) expect(methodSpy).toBeCalled() }) }) describe('beforeSuite', () => { it('should send request to set the session name as suite name for Mocha tests', async () => { await service.before(service['_config'] as any, [], browser) expect(service['_suiteTitle']).toBeUndefined() expect(service['_fullTitle']).toBeUndefined() await service.beforeSuite({ title: 'foobar' } as any) expect(service['_suiteTitle']).toBe('foobar') expect(service['_fullTitle']).toBe('foobar') expect(fetch).toBeCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ name: 'foobar' }), headers } ) }) it('should not send request to set the session name as suite name for Jasmine tests', async () => { await service.before(service['_config'] as any, [], browser) expect(service['_suiteTitle']).toBeUndefined() expect(service['_fullTitle']).toBeUndefined() await service.beforeSuite({ title: jasmineSuiteTitle } as any) expect(service['_suiteTitle']).toBe(jasmineSuiteTitle) expect(service['_fullTitle']).toBeUndefined() expect(fetch).not.toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ method: 'POST', })) }) it('should not send request to set the session name if option setSessionName is false', async () => { const service = new BrowserstackService({ setSessionName: false } as any, [] as any, { user: 'foo', key: 'bar' } as any) await service.beforeSuite({ title: 'Project Title' } as any) expect(fetch).not.toBeCalled() }) }) describe('beforeTest', () => { it('should not send request to set the session name if option setSessionName is false', async () => { const service = new BrowserstackService({ setSessionName: false } as any, [] as any, { user: 'foo', key: 'bar' } as any) await service.beforeSuite({ title: 'Project Title' } as any) await service.beforeTest({ title: 'Test Title', parent: 'Suite Title' } as any) expect(fetch).not.toBeCalled() }) describe('sessionNamePrependTopLevelSuiteTitle is true', () => { it('should set title for Mocha tests using concatenation of top level suite name, innermost suite name, and test title', async () => { const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser const service = new BrowserstackService({ sessionNamePrependTopLevelSuiteTitle: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeSuite({ title: 'Project Title' } as any) expect(service['_fullTitle']).toBe('Project Title') await service.beforeTest({ title: 'Test Title', parent: 'Suite Title' } as any) expect(service['_fullTitle']).toBe('Project Title - Suite Title - Test Title') assertMethodCalls(vi.mocked(fetch), 'PUT', 2) expect(fetch).toBeCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ name: 'Project Title' }), headers } ) expect(fetch).toBeCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ name: 'Project Title - Suite Title - Test Title' }), headers } ) }) }) describe('sessionNameOmitTestTitle is true', () => { beforeEach(() => { service = new BrowserstackService({ sessionNameOmitTestTitle: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) }) it('should not set title for Mocha tests', async () => { const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeSuite({ title: 'Suite Title' } as any) expect(service['_fullTitle']).toBe('Suite Title') await service.beforeTest({ title: 'bar', parent: 'Suite Title' } as any) expect(service['_fullTitle']).toBe('Suite Title') await service.afterTest({ title: 'bar', parent: 'Suite Title' } as any, undefined as never, {} as any) expect(service['_fullTitle']).toBe('Suite Title') assertMethodCalls(vi.mocked(fetch), 'PUT', 1) expect(fetch).toBeCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ name: 'Suite Title' }), headers } ) }) }) describe('sessionNamePrependTopLevelSuiteTitle is true, sessionNameOmitTestTitle is true', () => { beforeEach(() => { service = new BrowserstackService({ sessionNameOmitTestTitle: true, sessionNamePrependTopLevelSuiteTitle: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) }) it('should set title for Mocha tests using concatenation of top level suite name and innermost suite name', async () => { const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeSuite({ title: 'Project Title' } as any) expect(service['_fullTitle']).toBe('Project Title') await service.beforeTest({ title: 'Test Title', parent: 'Suite Title' } as any) expect(service['_fullTitle']).toBe('Project Title - Suite Title') assertMethodCalls(vi.mocked(fetch), 'PUT', 2) expect(fetch).toBeCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ name: 'Project Title' }), headers } ) expect(fetch).toBeCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ name: 'Project Title - Suite Title' }), headers } ) }) }) describe('sessionNameFormat is defined', () => { beforeEach(() => { service = new BrowserstackService({ sessionNameFormat: (config, caps, suiteTitle, testTitle) => { if (testTitle) { return `${config.region} - ${(caps as any).browserName} - ${suiteTitle} - ${testTitle}` } return `${config.region} - ${(caps as any).browserName} - ${suiteTitle}` } } as any, { browserName: 'foobar' }, { user: 'foo', key: 'bar', region: 'barfoo' } as any) }) it('should set title via sessionNameFormat method', async () => { const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.before(service['_config'] as any, [], browserWithExecuteScript) service['_browser'] = browserWithExecuteScript service['_suiteTitle'] = 'Suite Title' await service.beforeSuite({ title: 'Suite Title' } as any) expect(service['_fullTitle']).toBe('barfoo - foobar - Suite Title') await service.beforeTest({ title: 'Test Title', parent: 'Suite Title' } as any) expect(service['_fullTitle']).toBe('barfoo - foobar - Suite Title - Test Title') assertMethodCalls(vi.mocked(fetch), 'PUT', 2) expect(fetch).toBeCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ name: 'barfoo - foobar - Suite Title' }), headers } ) expect(fetch).toBeCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ name: 'barfoo - foobar - Suite Title - Test Title' }), headers } ) }) }) describe('Jasmine only', () => { it('should set suite name of first test as title', async () => { const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeSuite({ title: jasmineSuiteTitle } as any) await service.beforeTest({ fullName: 'foo bar baz', description: 'baz' } as any) service.afterTest({ fullName: 'foo bar baz', description: 'baz' } as any, undefined as never, {} as any) expect(service['_fullTitle']).toBe('foo bar') expect(fetch).toBeCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ name: 'foo bar' }), headers } ) }) it('should set parent suite name as title', async () => { const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeSuite({ title: jasmineSuiteTitle } as any) await service.beforeTest({ fullName: 'foo bar baz', description: 'baz' } as any) await service.beforeTest({ fullName: 'foo xyz', description: 'xyz' } as any) service.afterTest({ fullName: 'foo bar baz', description: 'baz' } as any, undefined as never, {} as any) service.afterTest({ fullName: 'foo xyz', description: 'xyz' } as any, undefined as never, {} as any) expect(service['_fullTitle']).toBe('foo') expect(fetch).toBeCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ name: 'foo' }), headers } ) }) }) }) describe('afterTest', () => { it('should increment failure reasons on fails', () => { service.before(service['_config'] as any, [], browser) service['_fullTitle'] = '' service.beforeSuite({ title: 'foo' } as any) service.beforeTest({ title: 'foo', parent: 'bar' } as any) service.afterTest( { title: 'foo', parent: 'bar' } as any, undefined as never, { error: { message: 'cool reason' }, result: 1, duration: 5, passed: false } as any) expect(service['_failReasons']).toContain('cool reason') service.beforeTest({ title: 'foo2', parent: 'bar2' } as any) service.afterTest( { title: 'foo2', parent: 'bar2' } as any, undefined as never, { error: { message: 'not so cool reason' }, result: 1, duration: 7, passed: false } as any) expect(service['_failReasons']).toHaveLength(2) expect(service['_failReasons']).toContain('cool reason') expect(service['_failReasons']).toContain('not so cool reason') service.beforeTest({ title: 'foo3', parent: 'bar3' } as any) service.afterTest( { title: 'foo3', parent: 'bar3' } as any, undefined as never, { error: undefined, result: 1, duration: 7, passed: false } as any) expect(service['_fullTitle']).toBe('bar3 - foo3') expect(service['_failReasons']).toHaveLength(3) expect(service['_failReasons']).toContain('cool reason') expect(service['_failReasons']).toContain('not so cool reason') expect(service['_failReasons']).toContain('Unknown Error') }) it('should not increment failure reasons on passes', () => { service.before(service['_config'] as any, [], browser) service.beforeSuite({ title: 'foo' } as any) service.beforeTest({ title: 'foo', parent: 'bar' } as any) service.afterTest( { title: 'foo', parent: 'bar' } as any, undefined as never, { error: { message: 'cool reason' }, result: 1, duration: 5, passed: true } as any) expect(service['_failReasons']).toEqual([]) service.beforeTest({ title: 'foo2', parent: 'bar2' } as any) service.afterTest( { title: 'foo2', parent: 'bar2' } as any, undefined as never, { error: { message: 'not so cool reason' }, result: 1, duration: 5, passed: true } as any) expect(service['_fullTitle']).toBe('bar2 - foo2') expect(service['_failReasons']).toEqual([]) }) }) describe('afterScenario', () => { it('should increment failure reasons on non-passing statuses (strict mode off)', () => { service = new BrowserstackService({ testObservability: false } as any, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: false } } as any) expect(service['_failReasons']).toEqual([]) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'SKIPPED' } }) expect(service['_failReasons']).toEqual([]) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'FAILED', message: 'I am Error, most likely' } }) expect(service['_failReasons']).toEqual(['I am Error, most likely']) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'SKIPPED' } }) expect(service['_failReasons']).toEqual(['I am Error, most likely']) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'FAILED', message: 'I too am Error' } }) expect(service['_failReasons']).toEqual(['I am Error, most likely', 'I too am Error']) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'UNDEFINED', message: 'Step XYZ is undefined' } }) expect(service['_failReasons']).toEqual(['I am Error, most likely', 'I too am Error', 'Step XYZ is undefined']) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'AMBIGUOUS', message: 'Step XYZ2 is ambiguous' } }) expect(service['_failReasons']).toEqual( ['I am Error, most likely', 'I too am Error', 'Step XYZ is undefined', 'Step XYZ2 is ambiguous']) service.afterScenario({ pickle: { name: 'Can do something' }, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'PENDING' } }) expect(service['_failReasons']).toEqual( ['I am Error, most likely', 'I too am Error', 'Step XYZ is undefined', 'Step XYZ2 is ambiguous']) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'SKIPPED' } }) expect(service['_failReasons']).toEqual([ 'I am Error, most likely', 'I too am Error', 'Step XYZ is undefined', 'Step XYZ2 is ambiguous']) }) it('should increment failure reasons on non-passing statuses (strict mode on)', () => { service = new BrowserstackService({ testObservability: false } as any, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: true }, capabilities: {} }) expect(service['_failReasons']).toEqual([]) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'SKIPPED' } }) expect(service['_failReasons']).toEqual([]) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, message: 'I am Error, most likely', status: 'FAILED' } }) expect(service['_failReasons']).toEqual(['I am Error, most likely']) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'SKIPPED' } }) expect(service['_failReasons']).toEqual(['I am Error, most likely']) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'FAILED', message: 'I too am Error' } }) expect(service['_failReasons']).toEqual(['I am Error, most likely', 'I too am Error']) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'UNDEFINED', message: 'Step XYZ is undefined' } }) expect(service['_failReasons']).toEqual(['I am Error, most likely', 'I too am Error', 'Step XYZ is undefined']) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'AMBIGUOUS', message: 'Step XYZ2 is ambiguous' } }) expect(service['_failReasons']).toEqual( ['I am Error, most likely', 'I too am Error', 'Step XYZ is undefined', 'Step XYZ2 is ambiguous']) service.afterScenario({ pickle: { name: 'Can do something' }, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'PENDING' } }) expect(service['_failReasons']).toEqual( ['I am Error, most likely', 'I too am Error', 'Step XYZ is undefined', 'Step XYZ2 is ambiguous', 'Some steps/hooks are pending for scenario "Can do something"']) service.afterScenario({ pickle: {}, result: { duration: { seconds: 0, nanos: 1000000 }, willBeRetried: false, status: 'SKIPPED' } }) expect(service['_failReasons']).toEqual([ 'I am Error, most likely', 'I too am Error', 'Step XYZ is undefined', 'Step XYZ2 is ambiguous', 'Some steps/hooks are pending for scenario "Can do something"']) }) }) describe('after', () => { it('should call _update when session has no errors (exit code 0)', async () => { const updateSpy = vi.spyOn(service, '_update') await service.before(service['_config'] as any, [], browser) service['_failReasons'] = [] service['_fullTitle'] = 'foo - bar' service['_specsRan'] = true await service.after(0) expect(updateSpy).toHaveBeenCalledWith(service['_browser']?.sessionId, { status: 'passed', name: 'foo - bar' }) expect(fetch).toHaveBeenCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ status: 'passed', name: 'foo - bar' }), headers }) }) it('should call _update when session has errors (exit code 1)', async () => { const updateSpy = vi.spyOn(service, '_update') await service.before(service['_config'] as any, [], browser) service['_fullTitle'] = 'foo - bar' service['_failReasons'] = ['I am failure'] await service.after(1) expect(updateSpy).toHaveBeenCalledWith(service['_browser']?.sessionId, { status: 'failed', name: 'foo - bar', reason: 'I am failure' }) expect(fetch).toHaveBeenCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ status: 'failed', name: 'foo - bar', reason: 'I am failure' }), headers }) }) it('should call _update with failed when session has no errors (exit code 0) but no tests ran', async () => { const updateSpy = vi.spyOn(service, '_update') await service.before(service['_config'] as any, [], browser) service['_failReasons'] = [] service['_fullTitle'] = 'foo - bar' await service.after(0) expect(updateSpy).toHaveBeenCalledWith(service['_browser']?.sessionId, { status: 'failed', name: 'foo - bar' }) expect(fetch).toHaveBeenCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ status: 'failed', name: 'foo - bar' }), headers }) }) it('should not set session status if option setSessionStatus is false', async () => { const service = new BrowserstackService({ setSessionStatus: false } as any, [] as any, { user: 'foo', key: 'bar' } as any) const updateSpy = vi.spyOn(service, '_update') await service.before(service['_config'] as any, [], browser) service['_fullTitle'] = 'foo - bar' service['_failReasons'] = ['I am failure'] await service.after(1) expect(updateSpy).not.toHaveBeenCalled() expect(fetch).not.toHaveBeenCalledWith(expect.any(String), expect.objectContaining({ method: 'POST', })) }) it('should not set session name if option setSessionName is false', async () => { const service = new BrowserstackService({ setSessionName: false } as any, [] as any, { user: 'foo', key: 'bar' } as any) const updateSpy = vi.spyOn(service, '_update') await service.before(service['_config'] as any, [], browser) service['_failReasons'] = [] service['_fullTitle'] = 'foo - bar' service['_specsRan'] = true await service.after(0) expect(updateSpy).toHaveBeenCalledWith(service['_browser']?.sessionId, { status: 'passed' }) expect(fetch).toHaveBeenCalledWith( `${sessionBaseUrl}/${sessionId}.json`, { method: 'PUT', body: JSON.stringify({ status: 'passed' }), headers }) }) describe('Cucumber only', function () { it('should call _update with status "failed" if strict mode is "on" and all tests are pending', async () => { service = new BrowserstackService({ testObservability: false } as any, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: true } } as any) const updateSpy = vi.spyOn(service, '_update') const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeFeature(null, { name: 'Feature1' }) await service.afterScenario({ pickle: { name: 'Can do something but pending 1' }, result: { status: 'PENDING' } as any }) await service.afterScenario({ pickle: { name: 'Can do something but pending 2' }, result: { status: 'PENDING' } as any }) await service.afterScenario({ pickle: { name: 'Can do something but pending 3' }, result: { status: 'PENDING' } as any }) await service.after(1) expect(updateSpy).toHaveBeenLastCalledWith(service['_browser']?.sessionId, { name: 'Feature1', reason: 'Some steps/hooks are pending for scenario "Can do something but pending 1"' + '\n' + 'Some steps/hooks are pending for scenario "Can do something but pending 2"' + '\n' + 'Some steps/hooks are pending for scenario "Can do something but pending 3"', status: 'failed', }) expect(updateSpy).toHaveBeenCalled() }) it('should call _update with status "passed" when strict mode is "off" and only passed and pending tests ran', async () => { service = new BrowserstackService({ testObservability: false } as any, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: false } } as any) const updateSpy = vi.spyOn(service, '_update') const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeFeature(null, { name: 'Feature1' }) await service.afterScenario({ pickle: { name: 'Can do something' }, result: { status: 'PASSED' } as any }) await service.afterScenario({ pickle: { name: 'Can do something' }, result: { status: 'PENDING' } as any }) await service.afterScenario({ pickle: { name: 'Can do something' }, result: { status: 'PASSED' } as any }) await service.after(0) expect(updateSpy).toHaveBeenCalled() expect(updateSpy).toHaveBeenLastCalledWith(service['_browser']?.sessionId, { name: 'Feature1', status: 'passed', }) }) it('should call _update with status is "failed" when strict mode is "on" and only passed and pending tests ran', async () => { service = new BrowserstackService({ testObservability: false } as any, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: true } } as any) const updateSpy = vi.spyOn(service, '_update') const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeFeature(null, { name: 'Feature1' }) await service.afterScenario({ pickle: { name: 'Can do something 1' }, result: { status: 'PASSED' } as any }) await service.afterScenario({ pickle: { name: 'Can do something but pending' }, result: { status: 'PENDING' } as any }) await service.afterScenario({ pickle: { name: 'Can do something 2' }, result: { status: 'PASSED' } as any }) await service.after(1) expect(updateSpy).toHaveBeenCalled() expect(updateSpy).toHaveBeenCalledWith(service['_browser']?.sessionId, { name: 'Feature1', reason: 'Some steps/hooks are pending for scenario "Can do something but pending"', status: 'failed', }) }) it('should call _update with status "passed" when all tests are skipped', async () => { const updateSpy = vi.spyOn(service, '_update') const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeFeature(null, { name: 'Feature1' }) await service.afterScenario({ pickle: { name: 'Can do something skipped 1' }, result: { status: 'SKIPPED' } as any }) await service.afterScenario({ pickle: { name: 'Can do something skipped 2' }, result: { status: 'SKIPPED' } as any }) await service.afterScenario({ pickle: { name: 'Can do something skipped 3' }, result: { status: 'SKIPPED' } as any }) await service.after(0) expect(updateSpy).toHaveBeenCalledWith(service['_browser']?.sessionId, { name: 'Feature1', status: 'passed', }) }) it('should call _update with status "failed" when strict mode is "on" and only failed and pending tests ran', async () => { service = new BrowserstackService({ testObservability: false } as any, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: true } } as any) const updateSpy = vi.spyOn(service, '_update') const afterSpy = vi.spyOn(service, 'after') const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.beforeSession(service['_config'] as any) await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeFeature(null, { name: 'Feature1' }) expect(updateSpy).toHaveBeenCalledWith(service['_browser']?.sessionId, { name: 'Feature1' }) await service.afterScenario({ pickle: { name: 'Can do something failed 1' }, result: { message: 'I am error, hear me roar', status: 'FAILED' } as any }) await service.afterScenario({ pickle: { name: 'Can do something but pending 2' }, result: { status: 'PENDING' } as any }) await service.afterScenario({ pickle: { name: 'Can do something but passed 3' }, result: { status: 'SKIPPED' } as any }) await service.after(1) expect(updateSpy).toHaveBeenCalledTimes(2) expect(updateSpy).toHaveBeenLastCalledWith( service['_browser']?.sessionId, { name: 'Feature1', reason: 'I am error, hear me roar' + '\n' + 'Some steps/hooks are pending for scenario "Can do something but pending 2"', status: 'failed', }) expect(afterSpy).toHaveBeenCalledTimes(1) }) it('should call _update with status "failed" when strict mode is "off" and only failed and pending tests ran', async () => { const updateSpy = vi.spyOn(service, '_update') const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.beforeSession(service['_config'] as any) await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeFeature(null, { name: 'Feature1' }) expect(updateSpy).toHaveBeenCalledWith(service['_browser']?.sessionId, { name: 'Feature1' }) await service.afterScenario({ pickle: { name: 'Can do something failed 1' }, result: { message: 'I am error, hear me roar', status: 'FAILED' } as any }) await service.afterScenario({ pickle: { name: 'Can do something but pending 2' }, result: { status: 'PENDING' } as any }) await service.afterScenario({ pickle: { name: 'Can do something but passed 3' }, result: { status: 'SKIPPED' } as any }) await service.after(1) expect(updateSpy).toHaveBeenCalledTimes(2) expect(updateSpy).toHaveBeenLastCalledWith( service['_browser']?.sessionId, { name: 'Feature1', reason: 'I am error, hear me roar', status: 'failed', } ) }) describe('preferScenarioName', () => { describe('enabled', () => { [ { status: 'FAILED', body: { name: 'Can do something single', reason: 'Unknown Error', status: 'failed', } } /*, 5, 4, 0*/ ].map(({ status, body }) => it(`should call _update /w status failed and name of Scenario when single "${status}" Scenario ran`, async () => { service = new BrowserstackService({ testObservability: false, preferScenarioName : true } as any, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: false } } as any) const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser service.before({}, [], browserWithExecuteScript) const updateSpy = vi.spyOn(service, '_update') await service.beforeFeature(null, { name: 'Feature1' }) await service.afterScenario({ pickle: { name: 'Can do something single' }, result: { status } as any }) await service.after(1) expect(updateSpy).toHaveBeenLastCalledWith(service['_browser']?.sessionId, body) }) ) it('should call _update /w status passed and name of Scenario when single "passed" Scenario ran', async () => { service = new BrowserstackService({ testObservability: false, preferScenarioName : true } as any, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: false } } as any) const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser service.before({}, [], browserWithExecuteScript) const updateSpy = vi.spyOn(service, '_update') await service.beforeFeature(null, { name: 'Feature1' }) await service.afterScenario({ pickle: { name: 'Can do something single' }, result: { status: 'passed' } as any }) await service.after(0) expect(updateSpy).toHaveBeenLastCalledWith(service['_browser']?.sessionId, { name: 'Can do something single', status: 'passed', }) }) }) describe('disabled', () => { ['FAILED', 'AMBIGUOUS', 'UNDEFINED', 'UNKNOWN'].map(status => it(`should call _update /w status failed and name of Feature when single "${status}" Scenario ran`, async () => { service = new BrowserstackService({ testObservability: false, preferScenarioName : false } as any, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: false } } as any) const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser service.before({}, [], browserWithExecuteScript) const updateSpy = vi.spyOn(service, '_update') await service.beforeFeature(null, { name: 'Feature1' }) await service.afterScenario({ pickle: { name: 'Can do something single' }, result: { status } as any }) await service.after(1) expect(updateSpy).toHaveBeenLastCalledWith(service['_browser']?.sessionId, { name: 'Feature1', reason: 'Unknown Error', status: 'failed', }) }) ) it('should call _update /w status passed and name of Feature when single "passed" Scenario ran', async () => { service = new BrowserstackService({ testObservability: false, preferScenarioName : false } as any, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: false } } as any) const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser service.before({}, [], browserWithExecuteScript) const updateSpy = vi.spyOn(service, '_update') await service.beforeFeature(null, { name: 'Feature1' }) await service.afterScenario({ pickle: { name: 'Can do something single' }, result: { status: 'PASSED' } as any }) await service.after(0) expect(updateSpy).toHaveBeenLastCalledWith(service['_browser']?.sessionId, { name: 'Feature1', status: 'passed', }) }) }) }) }) describe('Observability only', function () { it('should call _update with status "failed" if strict mode is "on" and all tests are pending', async () => { service = new BrowserstackService({ testObservability: true } as any, [] as any, { user: 'foo', key: 'bar', cucumberOpts: { strict: true } } as any) service['_failReasons'] = [] const updateSpy = vi.spyOn(service, '_updateJob') await service.after(1) expect(updateSpy).toHaveBeenCalled() }) }) }) describe('_updateCaps', () => { it('calls fn', () => { const fnSpy = vi.fn() service._updateCaps(fnSpy) expect(fnSpy).toBeCalledTimes(1) }) it('calls fn - caps present', () => { const fnSpy = vi.fn() service['_caps'] = { capabilities: { browserName: 'chrome' } } as any service._updateCaps(fnSpy) expect(fnSpy).toBeCalledTimes(1) }) }) describe('setAnnotation', () => { describe('Cucumber', () => { it('should correctly annotate Features, Scenarios, and Steps', async () => { const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser const service = new BrowserstackService({ sessionNamePrependTopLevelSuiteTitle: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeFeature(null, { name: 'Feature1' }) await service.beforeScenario({ pickle: { name: 'foobar' } }) const step = { id: '5', text: 'I am a step', astNodeIds: ['0'], keyword: 'Given ', } await service.beforeStep(step) expect(browser.execute).toBeCalledTimes(3) expect(browserWithExecuteScript.executeScript).toHaveBeenNthCalledWith(1, 'browserstack_executor: {"action":"annotate","arguments":{"data":"Feature: Feature1","level":"info"}}', []) expect(browserWithExecuteScript.executeScript).toHaveBeenNthCalledWith(2, 'browserstack_executor: {"action":"annotate","arguments":{"data":"Scenario: foobar","level":"info"}}', []) expect(browserWithExecuteScript.executeScript).toHaveBeenNthCalledWith(3, 'browserstack_executor: {"action":"annotate","arguments":{"data":"Step: Given I am a step","level":"info"}}', []) }) }) describe('Jasmine', () => { it('should correctly annotate Tests', async () => { const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeSuite({ title: jasmineSuiteTitle } as any) await service.beforeTest({ fullName: 'foo bar baz', description: 'baz' } as any) expect(browser.execute).toBeCalledTimes(1) expect(browser.execute).toBeCalledWith('browserstack_executor: {"action":"annotate","arguments":{"data":"Test: foo bar baz","level":"info"}}', []) }) }) describe('Mocha', () => { it('should correctly annotate Tests', async () => { const browserWithExecuteScript = { ...browser, executeScript: browser.execute } as WebdriverIO.Browser await service.before(service['_config'] as any, [], browserWithExecuteScript) await service.beforeSuite({ title: 'My Feature' } as any) await service.beforeTest({ title: 'Test Title', parent: 'Suite Title' } as any) expect(browser.execute).toBeCalledTimes(1) expect(browser.execute).toBeCalledWith('browserstack_executor: {"action":"annotate","arguments":{"data":"Test: Test Title","level":"info"}}', []) }) }) }) describe('ignoreHooksStatus feature', () => { let service: BrowserstackService beforeEach(() => { service = new BrowserstackService({ testObservability: false } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser }) describe('afterHook with ignoreHooksStatus=true', () => { beforeEach(() => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: true } } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser service['_insightsHandler'] = new InsightsHandler(browser) }) it('should track hook failures but not add them to main _failReasons when ignoreHooksStatus=true', async () => { const methodSpy = vi.spyOn(service['_insightsHandler'], 'afterHook') await service.afterHook({ title: 'foo', parent: 'bar' } as any, undefined as never, { passed: false, error: { message: 'Hook failed' } } as any) expect(service['_hookFailReasons']).toEqual(['Hook failed']) expect(service['_failReasons']).toEqual([]) // Should not be added when ignoreHooksStatus=true expect(methodSpy).toBeCalled() }) it('should add hook failures to _failReasons when ignoreHooksStatus=false', async () => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: false } } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_insightsHandler'] = new InsightsHandler(browser) await service.afterHook({ title: 'foo', parent: 'bar' } as any, undefined as never, { passed: false, error: { message: 'Hook failed' } } as any) expect(service['_hookFailReasons']).toEqual(['Hook failed']) expect(service['_failReasons']).toEqual(['Hook failed']) // Should be added when ignoreHooksStatus=false }) it('should add hook failures to _failReasons when ignoreHooksStatus is not set', async () => { service = new BrowserstackService({ testObservability: false } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_insightsHandler'] = new InsightsHandler(browser) await service.afterHook({ title: 'foo', parent: 'bar' } as any, undefined as never, { passed: false, error: { message: 'Hook failed' } } as any) expect(service['_hookFailReasons']).toEqual(['Hook failed']) expect(service['_failReasons']).toEqual(['Hook failed']) // Should be added when ignoreHooksStatus not set (default behavior) }) }) describe('afterTest with pure test failures', () => { beforeEach(() => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: true } } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser service['_insightsHandler'] = new InsightsHandler(browser) }) it('should track pure test failures in both _failReasons and _pureTestFailReasons', async () => { await service.afterTest({ title: 'foo', parent: 'bar' } as any, undefined as never, { passed: false, error: { message: 'Test failed' } } as any) expect(service['_failReasons']).toEqual(['Test failed']) expect(service['_pureTestFailReasons']).toEqual(['Test failed']) }) }) describe('session status with ignoreHooksStatus=true', () => { beforeEach(() => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: true }, setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser service['_insightsHandler'] = new InsightsHandler(browser) vi.spyOn(service, '_updateJob').mockResolvedValue({} as any) }) it('should mark session as passed when only hooks fail and ignoreHooksStatus=true', async () => { // Simulate hook failure await service.afterHook({ title: 'hook', parent: 'suite' } as any, undefined as never, { passed: false, error: { message: 'Hook failed' } } as any) // No test failures service['_specsRan'] = true await service.after(0) // Exit code 0 indicates success expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'passed' // Should be passed because only hooks failed }) }) it('should mark session as failed when tests fail even with ignoreHooksStatus=true', async () => { // Simulate hook failure await service.afterHook({ title: 'hook', parent: 'suite' } as any, undefined as never, { passed: false, error: { message: 'Hook failed' } } as any) // Simulate test failure await service.afterTest({ title: 'test', parent: 'suite' } as any, undefined as never, { passed: false, error: { message: 'Test failed' } } as any) await service.after(0) expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'failed', reason: 'Test failed' // Should show test failure reason, not hook failure }) }) it('should include hook and test failures in reason when ignoreHooksStatus=false', async () => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: false }, setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser service['_insightsHandler'] = new InsightsHandler(browser) vi.spyOn(service, '_updateJob').mockResolvedValue({} as any) // Simulate hook failure await service.afterHook({ title: 'hook', parent: 'suite' } as any, undefined as never, { passed: false, error: { message: 'Hook failed' } } as any) // Simulate test failure await service.afterTest({ title: 'test', parent: 'suite' } as any, undefined as never, { passed: false, error: { message: 'Test failed' } } as any) await service.after(0) expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'failed', reason: 'Hook failed\nTest failed' // Should show both failures }) }) }) describe('onReload with ignoreHooksStatus=true', () => { beforeEach(() => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: true }, setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser service['_insightsHandler'] = new InsightsHandler(browser) vi.spyOn(service, '_update').mockResolvedValue({} as any) }) it('should use pure test failures for status when ignoreHooksStatus=true', async () => { // Add hook failure service['_hookFailReasons'].push('Hook failed') service['_failReasons'].push('Test failed') // This would normally include hook failures too service['_pureTestFailReasons'].push('Test failed') await service.onReload('oldSessionId', 'newSessionId') expect(service['_update']).toHaveBeenCalledWith('oldSessionId', { status: 'failed', reason: 'Test failed' // Should only use pure test failures }) }) it('should pass when only hook failures exist and ignoreHooksStatus=true', async () => { // Add only hook failure service['_hookFailReasons'].push('Hook failed') // No pure test failures await service.onReload('oldSessionId', 'newSessionId') expect(service['_update']).toHaveBeenCalledWith('oldSessionId', { status: 'passed' // Should pass when only hooks fail }) }) it('should reset all failure arrays on reload', async () => { // Add failures service['_failReasons'].push('Some failure') service['_hookFailReasons'].push('Hook failed') service['_pureTestFailReasons'].push('Test failed') await service.onReload('oldSessionId', 'newSessionId') expect(service['_failReasons']).toEqual([]) expect(service['_hookFailReasons']).toEqual([]) expect(service['_pureTestFailReasons']).toEqual([]) }) }) describe('ignoreHooksStatus feature - Comprehensive Test Suite', () => { describe('afterHook method - Extended Coverage', () => { describe('when ignoreHooksStatus is true', () => { beforeEach(() => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: true }, setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser }) it('should handle multiple consecutive hook failures correctly', async () => { const hook1Error = { message: 'Before hook failed' } const hook2Error = { message: 'After hook failed' } const hook3Error = { message: 'Setup hook failed' } await service.afterHook({ title: 'beforeEach' } as any, undefined, { passed: false, error: hook1Error } as any) await service.afterHook({ title: 'afterEach' } as any, undefined, { passed: false, error: hook2Error } as any) await service.afterHook({ title: 'beforeAll' } as any, undefined, { passed: false, error: hook3Error } as any) expect(service['_hookFailReasons']).toEqual(['Before hook failed', 'After hook failed', 'Setup hook failed']) expect(service['_failReasons']).toEqual([]) // Should remain empty with ignoreHooksStatus=true }) it('should handle hooks that pass after failures correctly', async () => { const hookError = { message: 'First hook failed' } await service.afterHook({ title: 'hook1' } as any, undefined, { passed: false, error: hookError } as any) await service.afterHook({ title: 'hook2' } as any, undefined, { passed: true } as any) await service.afterHook({ title: 'hook3' } as any, undefined, { passed: true } as any) expect(service['_hookFailReasons']).toEqual(['First hook failed']) expect(service['_failReasons']).toEqual([]) }) it('should handle null/undefined errors gracefully', async () => { await service.afterHook({ title: 'hook' } as any, undefined, { passed: false, error: null } as any) await service.afterHook({ title: 'hook' } as any, undefined, { passed: false, error: undefined } as any) await service.afterHook({ title: 'hook' } as any, undefined, { passed: false } as any) expect(service['_hookFailReasons']).toEqual(['Hook failed', 'Hook failed', 'Hook failed']) expect(service['_failReasons']).toEqual([]) }) }) describe('when ignoreHooksStatus is undefined/not specified', () => { it('should default to false and add hook failures to failReasons', async () => { service = new BrowserstackService({ testObservability: false, // testObservabilityOptions not specified setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser const hookError = { message: 'Hook failed' } await service.afterHook({ title: 'hook' } as any, undefined, { passed: false, error: hookError } as any) expect(service['_hookFailReasons']).toEqual(['Hook failed']) expect(service['_failReasons']).toEqual(['Hook failed']) // Should be added (default behavior) }) }) }) describe('after method - Complex Session Status Scenarios', () => { describe('when ignoreHooksStatus is true', () => { beforeEach(() => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: true }, setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser vi.spyOn(service, '_updateJob').mockResolvedValue({} as any) }) it('should handle complex mixed failure scenarios', async () => { service['_specsRan'] = true service['_hookFailReasons'] = ['Before hook failed', 'After hook failed', 'Setup hook failed'] service['_failReasons'] = ['Test assertion failed', 'Another test failed'] service['_pureTestFailReasons'] = ['Test assertion failed', 'Another test failed'] await service.after(1) // Failing exit code expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'failed', reason: 'Test assertion failed\nAnother test failed' }) }) it('should override to passed when exit code is 1 but only hooks failed', async () => { service['_specsRan'] = true service['_hookFailReasons'] = ['Multiple', 'Hook', 'Failures'] service['_failReasons'] = [] // Empty because hooks ignored service['_pureTestFailReasons'] = [] await service.after(1) // Failing exit code expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'passed' }) }) it('should handle empty failure arrays correctly', async () => { service['_specsRan'] = true service['_hookFailReasons'] = [] service['_failReasons'] = [] service['_pureTestFailReasons'] = [] await service.after(0) // Success exit code expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'passed' }) }) it('should mark as failed when specs did not run regardless of hook status', async () => { service['_specsRan'] = false service['_hookFailReasons'] = ['Hook failed'] service['_failReasons'] = [] service['_pureTestFailReasons'] = [] await service.after(0) // Even with success exit code expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'failed' }) }) it('should handle scenario where setSessionName is enabled', async () => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: true }, setSessionStatus: true, setSessionName: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser vi.spyOn(service, '_updateJob').mockResolvedValue({} as any) service['_specsRan'] = true service['_fullTitle'] = 'My Test Suite' service['_hookFailReasons'] = ['Hook failed'] service['_failReasons'] = [] service['_pureTestFailReasons'] = [] await service.after(1) expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'passed', name: 'My Test Suite' }) }) it('should respect session name and status options independently', async () => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: true }, setSessionStatus: false, // Disabled setSessionName: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser vi.spyOn(service, '_updateJob').mockResolvedValue({} as any) service['_specsRan'] = true service['_hookFailReasons'] = ['Hook failed'] service['_failReasons'] = [] service['_pureTestFailReasons'] = [] await service.after(1) // Should not call _updateJob when setSessionStatus is false expect(service['_updateJob']).not.toHaveBeenCalled() }) }) describe('comparison with ignoreHooksStatus disabled', () => { beforeEach(() => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: false }, setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser vi.spyOn(service, '_updateJob').mockResolvedValue({} as any) }) it('should mark as failed when hooks fail (ignoreHooksStatus=false)', async () => { service['_specsRan'] = true service['_hookFailReasons'] = ['Hook failed'] service['_failReasons'] = ['Hook failed'] // Includes hook failures service['_pureTestFailReasons'] = [] await service.after(0) // Success exit code expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'failed', reason: 'Hook failed' }) }) it('should combine hook and test failure reasons', async () => { service['_specsRan'] = true service['_hookFailReasons'] = ['Hook failed'] service['_failReasons'] = ['Hook failed', 'Test failed'] // Both included service['_pureTestFailReasons'] = ['Test failed'] await service.after(1) expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'failed', reason: 'Hook failed\nTest failed' }) }) }) }) describe('Integration test scenarios', () => { beforeEach(() => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: true }, setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser vi.spyOn(service, '_updateJob').mockResolvedValue({} as any) }) it('should handle complete test lifecycle with mixed outcomes', async () => { // Setup phase - hook fails await service.afterHook({ title: 'beforeEach' } as any, undefined, { passed: false, error: { message: 'Setup failed' } } as any) // First test - passes await service.afterTest({ title: 'test1' } as any, undefined as never, { passed: true } as any) // Second test - fails await service.afterTest({ title: 'test2' } as any, undefined as never, { passed: false, error: { message: 'Assertion failed' } } as any) // Cleanup phase - hook fails await service.afterHook({ title: 'afterEach' } as any, undefined, { passed: false, error: { message: 'Cleanup failed' } } as any) // Third test - passes await service.afterTest({ title: 'test3' } as any, undefined as never, { passed: true } as any) // Verify intermediate state expect(service['_hookFailReasons']).toEqual(['Setup failed', 'Cleanup failed']) expect(service['_failReasons']).toEqual(['Assertion failed']) expect(service['_pureTestFailReasons']).toEqual(['Assertion failed']) // Final session status await service.after(1) expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'failed', reason: 'Assertion failed' }) }) it('should handle only hook failures across entire test lifecycle', async () => { // Multiple hooks fail await service.afterHook({ title: 'beforeAll' } as any, undefined, { passed: false, error: { message: 'Global setup failed' } } as any) await service.afterHook({ title: 'beforeEach' } as any, undefined, { passed: false, error: { message: 'Test setup failed' } } as any) // All tests pass await service.afterTest({ title: 'test1' } as any, undefined as never, { passed: true } as any) await service.afterTest({ title: 'test2' } as any, undefined as never, { passed: true } as any) await service.afterTest({ title: 'test3' } as any, undefined as never, { passed: true } as any) // More hooks fail await service.afterHook({ title: 'afterEach' } as any, undefined, { passed: false, error: { message: 'Test cleanup failed' } } as any) await service.afterHook({ title: 'afterAll' } as any, undefined, { passed: false, error: { message: 'Global cleanup failed' } } as any) // Verify state expect(service['_hookFailReasons']).toEqual([ 'Global setup failed', 'Test setup failed', 'Test cleanup failed', 'Global cleanup failed' ]) expect(service['_failReasons']).toEqual([]) expect(service['_pureTestFailReasons']).toEqual([]) // Should pass despite framework exit code 1 await service.after(1) expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'passed' }) }) }) describe('Boundary and edge cases', () => { beforeEach(() => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: true }, setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser vi.spyOn(service, '_updateJob').mockResolvedValue({} as any) }) it('should handle extremely long error messages', async () => { const longErrorMessage = 'A'.repeat(10000) // Very long error message await service.afterHook({ title: 'hook' } as any, undefined, { passed: false, error: { message: longErrorMessage } } as any) expect(service['_hookFailReasons']).toEqual([longErrorMessage]) expect(service['_failReasons']).toEqual([]) }) it('should handle special characters in error messages', async () => { const specialMessage = 'Error with special chars: \n\t\r"\'\\$`@#%^&*()[]{}|;:<>?/~' await service.afterTest({ title: 'test' } as any, undefined as never, { passed: false, error: { message: specialMessage } } as any) expect(service['_failReasons']).toEqual([specialMessage]) expect(service['_pureTestFailReasons']).toEqual([specialMessage]) }) it('should handle rapid alternating pass/fail scenarios', async () => { for (let i = 0; i < 100; i++) { const shouldFail = i % 2 === 0 await (shouldFail ? service.afterTest({ title: `test${i}` } as any, undefined as never, { passed: false, error: { message: `Test ${i} failed` } } as any) : service.afterTest({ title: `test${i}` } as any, undefined as never, { passed: true } as any)) } // Should have 50 failures (even indices 0, 2, 4, ..., 98) expect(service['_failReasons']).toHaveLength(50) expect(service['_pureTestFailReasons']).toHaveLength(50) }) }) describe('Cucumber afterScenario with ignoreHooksStatus', () => { beforeEach(() => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: true }, setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser service['_insightsHandler'] = new InsightsHandler(browser) }) it('should not add failure when scenario fails due to hooks only', async () => { const world = { pickle: { name: 'Test scenario' }, result: { status: 'FAILED', message: 'Hook failed' } } as any // Mock hasTestStepFailures to return false (no test step failures) vi.spyOn(service['_insightsHandler'], 'hasTestStepFailures').mockReturnValue(false) await service.afterScenario(world) expect(service['_failReasons']).toEqual([]) expect(service['_pureTestFailReasons']).toEqual([]) expect(service['_scenariosThatRan']).toEqual(['Test scenario']) }) it('should add failure when scenario fails due to test steps', async () => { const world = { pickle: { name: 'Test scenario' }, result: { status: 'FAILED', message: 'Test step failed' } } as any // Mock hasTestStepFailures to return true (test step failures exist) vi.spyOn(service['_insightsHandler'], 'hasTestStepFailures').mockReturnValue(true) await service.afterScenario(world) expect(service['_failReasons']).toEqual(['Test step failed']) expect(service['_pureTestFailReasons']).toEqual(['Test step failed']) expect(service['_scenariosThatRan']).toEqual(['Test scenario']) }) it('should add failure when ignoreHooksStatus is false regardless of step failures', async () => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: false }, setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser service['_insightsHandler'] = new InsightsHandler(browser) const world = { pickle: { name: 'Test scenario' }, result: { status: 'FAILED', message: 'Hook failed' } } as any // Mock hasTestStepFailures to return false (no test step failures) vi.spyOn(service['_insightsHandler'], 'hasTestStepFailures').mockReturnValue(false) await service.afterScenario(world) expect(service['_failReasons']).toEqual(['Hook failed']) expect(service['_pureTestFailReasons']).toEqual(['Hook failed']) }) it('should handle pending scenarios with ignoreHooksStatus', async () => { const world = { pickle: { name: 'Pending scenario' }, result: { status: 'PENDING' } } as any // Mock hasTestStepFailures to return false vi.spyOn(service['_insightsHandler'], 'hasTestStepFailures').mockReturnValue(false) await service.afterScenario(world) expect(service['_failReasons']).toEqual([]) expect(service['_pureTestFailReasons']).toEqual([]) }) }) describe('Process exit override functionality', () => { beforeEach(() => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: true }, setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser vi.spyOn(service, '_updateJob').mockResolvedValue({} as any) }) it('should override process exit when only hooks fail and ignoreHooksStatus=true', async () => { service['_specsRan'] = true service['_hookFailReasons'] = ['Hook failed'] service['_failReasons'] = [] // Empty because hooks ignored service['_pureTestFailReasons'] = [] // after method should return early, preventing normal exit code handling const result = await service.after(1) // Exit code 1 (failure) expect(result).toBeUndefined() // Method returns early expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'passed' }) }) it('should not override process exit when tests actually failed', async () => { service['_specsRan'] = true service['_hookFailReasons'] = ['Hook failed'] service['_failReasons'] = ['Test failed'] service['_pureTestFailReasons'] = ['Test failed'] const result = await service.after(1) // Exit code 1 (failure) expect(result).toBeUndefined() // Normal flow, no early return expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'failed', reason: 'Test failed' }) }) it('should not override process exit when ignoreHooksStatus=false', async () => { service = new BrowserstackService({ testObservability: false, testObservabilityOptions: { ignoreHooksStatus: false }, setSessionStatus: true } as any, [] as any, { user: 'foo', key: 'bar' } as any) service['_browser'] = browser vi.spyOn(service, '_updateJob').mockResolvedValue({} as any) service['_specsRan'] = true service['_hookFailReasons'] = ['Hook failed'] service['_failReasons'] = ['Hook failed'] const result = await service.after(1) // Exit code 1 (failure) expect(result).toBeUndefined() // Normal flow, no early return expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'failed', reason: 'Hook failed' }) }) it('should not override process exit when specs did not run', async () => { service['_specsRan'] = false service['_hookFailReasons'] = ['Hook failed'] service['_failReasons'] = [] const result = await service.after(1) // Exit code 1 (failure) expect(result).toBeUndefined() // Normal flow, no early return expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'failed' }) }) it('should not override process exit when exit code is 0', async () => { service['_specsRan'] = true service['_hookFailReasons'] = ['Hook failed'] service['_failReasons'] = [] const result = await service.after(0) // Exit code 0 (success) expect(result).toBeUndefined() // Normal flow, no early return expect(service['_updateJob']).toHaveBeenCalledWith({ status: 'passed' }) }) }) }) })