import path from 'node:path' import type { LaunchResponse } from '../src/types.js' import { describe, expect, it, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest' import gitRepoInfo from 'git-repo-info' import CrashReporter from '../src/crash-reporter.js' import logger from '@wdio/logger' import * as utils from '../src/util.js' import logPatcher from '../src/logPatcher.js' import AccessibilityScripts from '../src/scripts/accessibility-scripts.js' import { getBrowserDescription, getBrowserCapabilities, isBrowserstackCapability, getParentSuiteName, getCiInfo, getCloudProvider, isBrowserstackSession, getUniqueIdentifier, getUniqueIdentifierForCucumber, removeAnsiColors, getScenarioExamples, stopBuildUpstream, launchTestSession, getGitMetaData, getLogTag, getHookType, isScreenshotCommand, getObservabilityUser, getObservabilityKey, getObservabilityBuild, getObservabilityProject, getObservabilityBuildTags, o11yErrorHandler, frameworkSupportsHook, getFailureObject, validateCapsWithAppA11y, validateCapsWithA11y, validateCapsWithNonBstackA11y, shouldScanTestForAccessibility, isAccessibilityAutomationSession, isAppAccessibilityAutomationSession, isTrue, uploadLogs, getObservabilityProduct, isUndefined, processTestObservabilityResponse, processAccessibilityResponse, processLaunchBuildResponse, jsonifyAccessibilityArray, formatString, _getParamsForAppAccessibility, performA11yScan, getAppA11yResults, getAppA11yResultsSummary, mergeDeep, mergeChromeOptions } from '../src/util.js' import * as bstackLogger from '../src/bstackLogger.js' import { BROWSERSTACK_OBSERVABILITY, TESTOPS_BUILD_COMPLETED_ENV, BROWSERSTACK_TESTHUB_JWT, BROWSERSTACK_ACCESSIBILITY } from '../src/constants.js' import * as testHubUtils from '../src/testHub/utils.js' import * as fs from 'node:fs/promises' import * as os from 'node:os' import type { Options } from '@wdio/types' const log = logger('test') vi.mock('fetch') vi.mock('git-repo-info') vi.useFakeTimers().setSystemTime(new Date('2020-01-01')) vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) vi.mock('fs', () => ({ default: { createReadStream: vi.fn().mockImplementation(() => {return { pipe: vi.fn().mockReturnThis() }}), createWriteStream: vi.fn().mockReturnValue({ pipe: vi.fn() }), stat: vi.fn().mockReturnValue(Promise.resolve({ size: 123 })), existsSync: vi.fn(), mkdirSync: vi.fn(), writeFileSync: vi.fn(), } })) vi.mock('./fileStream') vi.mock('fs', async (importOriginal) => { const actual = await importOriginal() return { ...actual, promises: { readFile: vi.fn().mockImplementation((path) => fs.readFile(path)) } } }) const bstackLoggerSpy = vi.spyOn(bstackLogger.BStackLogger, 'logToFile') bstackLoggerSpy.mockImplementation(() => {}) 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) } describe('getBrowserCapabilities', () => { it('should get default browser capabilities', () => { const browser = { capabilities: { browser: 'browser' } } as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser expect(getBrowserCapabilities(browser)) .toEqual(browser.capabilities as any) }) it('should get multiremote browser capabilities', () => { const browser = { isMultiremote: true, getInstance: vi.fn().mockImplementation((browserName: string) => browser[browserName]), browserA: { capabilities: { browser: 'browser' } } } as unknown as WebdriverIO.MultiRemoteBrowser expect(getBrowserCapabilities(browser, {}, 'browserA')) .toEqual(browser.browserA.capabilities as any) }) it('should handle null multiremote browser capabilities', () => { const browser = { isMultiremote: true, getInstance: vi.fn().mockImplementation((browserName: string) => browser[browserName]), browserA: {} } as unknown as WebdriverIO.MultiRemoteBrowser expect(getBrowserCapabilities(browser, {}, 'browserB')).toEqual({}) }) it('should merge service capabilities and browser capabilities', () => { const browser = { capabilities: { browser: 'browser', os: 'OS X', } } as unknown as WebdriverIO.Browser expect(getBrowserCapabilities(browser, { os: 'Windows' })) .toEqual({ os:'Windows', browser: 'browser' } as any) }) it('should merge multiremote service capabilities and browser capabilities', () => { const browser = { isMultiremote: true, getInstance: vi.fn().mockImplementation((browserName: string) => browser[browserName]), browserA: { capabilities: { browser: 'browser', os: 'OS X', } } } as unknown as WebdriverIO.MultiRemoteBrowser expect(getBrowserCapabilities(browser, { browserA: { capabilities: { os: 'Windows' } } }, 'browserA')) .toEqual({ os:'Windows', browser: 'browser' } as any) }) it('should handle null multiremote browser capabilities', () => { const browser = { isMultiremote: true, getInstance: vi.fn().mockImplementation((browserName: string) => browser[browserName]), browserA: {} } as unknown as WebdriverIO.MultiRemoteBrowser expect(getBrowserCapabilities(browser, {}, 'browserB')) .toEqual({}) }) it('should handle null multiremote browser capabilities', () => { const browser = { isMultiremote: true, getInstance: vi.fn().mockImplementation((browserName: string) => browser[browserName]), browserA: {} } as unknown as WebdriverIO.MultiRemoteBrowser expect(getBrowserCapabilities(browser, { browserB: {} } as any, 'browserB')) .toEqual({}) }) }) describe('getBrowserDescription', () => { const defaultCap = { 'device': 'device', 'os': 'os', 'osVersion': 'osVersion', 'browserName': 'browserName', 'browser': 'browser', 'browserVersion': 'browserVersion', } const defaultDesc = 'device os osVersion browserName browser browserVersion' const legacyCap = { 'os_version': 'os_version', 'browser_version': 'browser_version' } it('should get correct description for default capabilities', () => { expect(getBrowserDescription(defaultCap)).toEqual(defaultDesc) }) it('should get correct description for legacy capabilities', () => { expect(getBrowserDescription(legacyCap)).toEqual('os_version browser_version') }) it('should get correct description for W3C capabilities', () => { expect(getBrowserDescription({ 'bstack:options': defaultCap })).toEqual(defaultDesc) }) it('should merge W3C and lecacy capabilities', () => { expect(getBrowserDescription({ 'bstack:options': defaultCap })).toEqual(defaultDesc) }) it('should not crash when capabilities is null or undefined', () => { // @ts-expect-error test invalid params expect(getBrowserDescription(undefined)).toEqual('') // @ts-expect-error test invalid params expect(getBrowserDescription(null)).toEqual('') }) }) describe('isBrowserstackCapability', () => { it('should return false if browserstack W3C capabilities is absent or not valid', () => { expect(isBrowserstackCapability({})).toBe(false) expect(isBrowserstackCapability()).toBe(false) // @ts-expect-error test invalid params expect(isBrowserstackCapability({ 'bstack:options': null })).toBe(false) }) it('should return false if only key in browserstack W3C capabilities is wdioService', () => { expect(isBrowserstackCapability({ 'bstack:options': { wdioService: 'version' } })).toBe(false) }) it('should detect browserstack W3C capabilities', () => { expect(isBrowserstackCapability({ 'bstack:options': { os: 'some os' } })).toBe(true) }) }) describe('getParentSuiteName', () => { it('should return the parent suite name', () => { expect(getParentSuiteName('foo bar', 'foo')).toBe('foo') expect(getParentSuiteName('foo', 'foo bar')).toBe('foo') expect(getParentSuiteName('foo bar', 'foo baz')).toBe('foo') expect(getParentSuiteName('foo bar', 'foo bar')).toBe('foo bar') }) it('should return empty string if no common parent', () => { expect(getParentSuiteName('foo bar', 'baz bar')).toBe('') }) it('should handle empty values', () => { expect(getParentSuiteName('', 'foo')).toBe('') expect(getParentSuiteName('foo', '')).toBe('') }) }) describe('getCiInfo', () => { describe('should handle if running on CI', () => { beforeEach(() => { process.env.CI = 'true' }) afterEach(() => { delete process.env.CI }) it('should return object if any CI being used - codeship', () => { process.env.CI_NAME = 'codeship' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.CI_NAME }) it('should return object if any CI being used - JENKINS', () => { process.env.JENKINS_URL = 'url' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.JENKINS_URL }) it('should return object if any CI being used - TRAVIS', () => { process.env.TRAVIS = 'true' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.TRAVIS }) it('should return object if any CI being used - CIRCLECI', () => { process.env.CIRCLECI = 'true' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.CIRCLECI }) it('should return object if any CI being used - BITBUCKET', () => { process.env.BITBUCKET_BRANCH = 'true' process.env.BITBUCKET_COMMIT = 'true' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.BITBUCKET_COMMIT delete process.env.BITBUCKET_BRANCH }) it('should return object if any CI being used - DRONE', () => { process.env.DRONE = 'true' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.DRONE }) it('should return object if any CI being used - SEMAPHORE', () => { process.env.SEMAPHORE = 'true' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.SEMAPHORE }) it('should return object if any CI being used - GITLAB_CI', () => { process.env.GITLAB_CI = 'true' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.GITLAB_CI }) it('should return object if any CI being used - BUILDKITE', () => { process.env.BUILDKITE = 'true' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.BUILDKITE }) it('should return object if any CI being used - TF_BUILD', () => { process.env.TF_BUILD = 'True' process.env.TF_BUILD_BUILDNUMBER = '123' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.TF_BUILD delete process.env.TF_BUILD_BUILDNUMBER }) it('should return object if any CI being used - Appveyor', () => { process.env.APPVEYOR = 'True' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.APPVEYOR }) it('should return object if any CI being used - CodeBuild', () => { process.env.CODEBUILD_BUILD_ID = '1211' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.CODEBUILD_BUILD_ID }) it('should return object if any CI being used - Bamboo', () => { process.env.bamboo_buildNumber = '123' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.APviwPVEYOR delete process.env.bamboo_buildNumber }) it('should return object if any CI being used - Wercker', () => { process.env.WERCKER = 'true' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.WERCKER }) it('should return object if any CI being used - GCP', () => { process.env.GCP_PROJECT = 'True' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.GCP_PROJECT }) it('should return object if any CI being used - Shippable', () => { process.env.SHIPPABLE = 'true' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.SHIPPABLE }) it('should return object if any CI being used - Netlify', () => { process.env.NETLIFY = 'true' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.NETLIFY }) it('should return object if any CI being used - Github Actions', () => { process.env.GITHUB_ACTIONS = 'true' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.GITHUB_ACTIONS }) it('should return object if any CI being used - Vercel', () => { process.env.VERCEL = '1' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.VERCEL }) it('should return object if any CI being used - Teamcity', () => { process.env.TEAMCITY_VERSION = '3.4' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.TEAMCITY_VERSION }) it('should return object if any CI being used - Concourse', () => { process.env.CONCOURSE = 'true' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.CONCOURSE }) it('should return object if any CI being used - GoCD', () => { process.env.GO_JOB_NAME = 'job' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.GO_JOB_NAME }) it('should return object if any CI being used - CodeFresh', () => { process.env.CF_BUILD_ID = 'True' expect(getCiInfo()).toBeInstanceOf(Object) delete process.env.CF_BUILD_ID }) }) it('should return null if no CI being used', () => { expect(getCiInfo()).toBeNull() }) }) describe('getCloudProvider', () => { it('return unknown_grid if test not on browserstack', () => { expect(getCloudProvider({})).toEqual('unknown_grid') }) it('return Browserstack if test being run on browserstack', () => { expect(getCloudProvider({ options: { hostname: 'hub.browserstack.com' } })).toEqual('browserstack') }) }) describe('isBrowserstackSession', () => { it('return false if run locally', () => { expect(isBrowserstackSession({})).toEqual(false) }) it('return false if run on any other cloud service', () => { expect(isBrowserstackSession({ options: { hostname: 'anything-saucelabs.com' } })).toEqual(false) }) it('return true if run on Browserstack', () => { expect(isBrowserstackSession({ options: { hostname: 'hub.browserstack.com' } })).toEqual(true) }) }) describe('getUniqueIdentifier', () => { it('return unique identifier for mocha tests', () => { const test = { parent: 'root-level', title: 'title', fullName: '', ctx: '', type: '', fullTitle: '', pending: false, file: '' } expect(getUniqueIdentifier(test)).toEqual('root-level - title') }) it('return unique identifier for jasmine tests', () => { const test = { description: 'title', fullName: 'root-level title', pending: false } expect(getUniqueIdentifier(test as any, 'jasmine')).toEqual('root-level title') }) }) describe('getUniqueIdentifierForCucumber', () => { it('return unique identifier for cucumber tests', () => { const test = { pickle: { uri: 'uri', astNodeIds: ['1', '2'] } } expect(getUniqueIdentifierForCucumber(test)).toEqual('uri_1,2') }) }) describe('removeAnsiColors', () => { it('remove color encoding', () => { expect(removeAnsiColors('\x1b[F\x1b[31;1mHello, there!\x1b[m\x1b[E')).toEqual('Hello, there!') }) }) describe('getScenarioExamples', () => { it('return undefined if no nesting is there', () => { const test = { pickle: { name: 'name', astNodeIds: ['1'] } } expect(getScenarioExamples(test)).toEqual(undefined) }) it('return undefined if astNodeIds not present', () => { const test = { pickle: { name: 'name' } } expect(getScenarioExamples(test)).toEqual(undefined) }) it('return examples array', () => { const test = { gherkinDocument: { feature: { children: [ { background: { id: '1', name: '', } }, { scenario: { id: '8', steps: [ { id: '2', keyword: 'Given ', text: 'the title is ', }, { id: '3', keyword: 'Then ', text: 'I expect that element "h1" contains the same text as element ".subtitle"', } ], examples: [ { id: '7', tags: [], location: { line: 12, column: 9 }, keyword: 'Examples', name: '', description: '', tableHeader: { id: '4', location: { line: 13, column: 13 }, cells: [ { location: { line: 13, column: 15 }, value: 'title' } ] }, tableBody: [ { id: '5', location: { line: 14, column: 13 }, cells: [ { location: { line: 14, column: 15 }, value: 'value1' } ] }, { id: '6', location: { line: 15, column: 13 }, cells: [ { location: { line: 15, column: 15 }, value: 'value2' } ] } ] } ] } } ] }, }, pickle: { name: 'name', astNodeIds: ['8', '5'] } } expect(getScenarioExamples(test)).toEqual(['value1']) }) it('return array with passed examples if rule present', () => { const test = { gherkinDocument: { feature: { children: [ { background: { id: '1', name: '', } }, { rule: { children: [ { scenario: { id: '8', steps: [ { id: '2', keyword: 'Given ', text: 'the title is <title>', }, { id: '3', keyword: 'Then ', text: 'I expect that element "h1" contains the same text as element ".subtitle"', } ], examples: [ { id: '7', tags: [], location: { line: 12, column: 9 }, keyword: 'Examples', name: '', description: '', tableHeader: { id: '4', location: { line: 13, column: 13 }, cells: [ { location: { line: 13, column: 15 }, value: 'title' } ] }, tableBody: [ { id: '5', location: { line: 14, column: 13 }, cells: [ { location: { line: 14, column: 15 }, value: 'value1' } ] }, { id: '6', location: { line: 15, column: 13 }, cells: [ { location: { line: 15, column: 15 }, value: 'value2' } ] } ] } ] } } ] } } ] }, }, pickle: { name: 'name', astNodeIds: ['8', '5'] } } expect(getScenarioExamples(test)).toEqual(['value1']) }) it('return undefined if no examples present', () => { const test = { gherkinDocument: { feature: { children: [] }, }, pickle: { name: 'name', astNodeIds: ['8', '5'] } } expect(getScenarioExamples(test)).toEqual(undefined) }) }) describe('stopBuildUpstream', () => { it('return error if completed but jwt token not present', async () => { process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true' delete process.env[BROWSERSTACK_TESTHUB_JWT] const result: any = await stopBuildUpstream() delete process.env[TESTOPS_BUILD_COMPLETED_ENV] expect(result.status).toEqual('error') expect(result.message).toEqual('Token/buildID is undefined, build creation might have failed') }) it('return success if completed', async () => { process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true' process.env[BROWSERSTACK_TESTHUB_JWT] = 'jwt' vi.mocked(fetch).mockReturnValueOnce(Promise.resolve(Response.json({}))) const result: any = await stopBuildUpstream() expect(vi.mocked(fetch).mock.calls[0][1]?.method).toEqual('PUT') expect(result.status).toEqual('success') }) it('return error if failed', async () => { process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true' process.env[BROWSERSTACK_TESTHUB_JWT] = 'jwt' vi.mocked(fetch).mockReturnValueOnce(Promise.reject(Response.json({}))) const result: any = await stopBuildUpstream() expect(vi.mocked(fetch).mock.calls[0][1]?.method).toEqual('PUT') expect(result.status).toEqual('error') }) afterEach(() => { vi.mocked(fetch).mockClear() }) }) describe('launchTestSession', () => { vi.mocked(gitRepoInfo).mockReturnValue({} as any) vi.spyOn(testHubUtils, 'getProductMap').mockReturnValue({} as any) it('returns launch response when build is started successfully', async () => { const mockResponse = { build_hashed_id: 'build_id', jwt: 'jwt' } const fetchMock = vi.fn().mockResolvedValue({ json: vi.fn().mockResolvedValue(mockResponse) }) vi.mocked(fetch).mockImplementation(fetchMock) vi.spyOn(testHubUtils, 'getProductMapForBuildStartCall').mockReturnValue({ key1: false, key2: true }) const result: any = await launchTestSession({ framework: 'framework' } as any, {}, {}, {}) expect(fetchMock).toHaveBeenCalledTimes(1) const [url, options] = fetchMock.mock.calls[0] expect(options.method).toBe('POST') expect(result).toEqual(mockResponse) }) }) describe('getLogTag', () => { it('return correct tag', () => { expect(getLogTag('TestRunStarted')).toEqual('Test_Upload') expect(getLogTag('TestRunFinished')).toEqual('Test_Upload') expect(getLogTag('HookRunStarted')).toEqual('Hook_Upload') expect(getLogTag('HookRunFinished')).toEqual('Hook_Upload') expect(getLogTag('LogCreated')).toEqual('Log_Upload') }) }) describe('getGitMetaData', () => { it('return undefined', async () => { vi.mocked(gitRepoInfo).mockReturnValue({} as any) const result: any = await getGitMetaData() expect(result).toEqual(undefined) }) it('return non empty object', async () => { vi.mocked(gitRepoInfo).mockReturnValue({ commonGitDir: '/tmp', worktreeGitDir: '/tmp' } as any) try { const result: any = await getGitMetaData() expect(result).toEqual({}) } catch (e) { // } }) }) describe('getHookType', () => { it('get hook type as string', () => { expect(getHookType('"before each" hook for test 1')).toEqual('BEFORE_EACH') expect(getHookType('"after each" hook for test 1')).toEqual('AFTER_EACH') expect(getHookType('"before all" hook for test 1')).toEqual('BEFORE_ALL') expect(getHookType('"after all" hook for test 1')).toEqual('AFTER_ALL') expect(getHookType('no hook test')).toEqual('unknown') }) }) describe('isScreenshotCommand', () => { it('get true if screenshot command', () => { expect(isScreenshotCommand({ endpoint: 'session/:sessionId/screenshot' })).toEqual(true) }) it('get false if not a screenshot command', () => { expect(isScreenshotCommand({ endpoint: 'session/:sessionId/element/text' })).toEqual(false) }) }) describe('getObservabilityUser', () => { it('get env var', () => { process.env.BROWSERSTACK_USERNAME = 'try' expect(getObservabilityUser({}, {})).toEqual('try') delete process.env.BROWSERSTACK_USERNAME }) it('get user passed in testObservabilityOptions', () => { delete process.env.BROWSERSTACK_USERNAME expect(getObservabilityUser({ testObservabilityOptions: { user: 'user' } } as any, {})).toEqual('user') }) it('get user passed at root level', () => { delete process.env.BROWSERSTACK_USERNAME expect(getObservabilityUser({ testObservabilityOptions: { } } as any, { user: 'user-root' })).toEqual('user-root') }) it('get undefined', () => { delete process.env.BROWSERSTACK_USERNAME expect(getObservabilityUser({}, {})).toEqual(undefined) }) }) describe('getObservabilityKey', () => { it('get env var', () => { process.env.BROWSERSTACK_ACCESS_KEY = 'try' expect(getObservabilityKey({}, {})).toEqual('try') delete process.env.BROWSERSTACK_ACCESS_KEY }) it('get key passed in testObservabilityOptions', () => { delete process.env.BROWSERSTACK_ACCESS_KEY expect(getObservabilityKey({ testObservabilityOptions: { key: 'user-key' } } as any, {})).toEqual('user-key') }) it('get key passed at root level', () => { delete process.env.BROWSERSTACK_ACCESS_KEY expect(getObservabilityKey({ testObservabilityOptions: { } } as any, { key: 'key-root' })).toEqual('key-root') }) it('get undefined', () => { delete process.env.BROWSERSTACK_ACCESS_KEY expect(getObservabilityKey({}, {})).toEqual(undefined) }) }) describe('getObservabilityBuild', () => { it('get env var', () => { process.env.TEST_OBSERVABILITY_BUILD_NAME = 'try' expect(getObservabilityBuild({})).toEqual('try') delete process.env.TEST_OBSERVABILITY_BUILD_NAME }) it('get name passed in testObservabilityOptions', () => { delete process.env.TEST_OBSERVABILITY_BUILD_NAME expect(getObservabilityBuild({ testObservabilityOptions: { buildName: 'build' } } as any)).toEqual('build') }) it('get name passed at root level', () => { delete process.env.TEST_OBSERVABILITY_BUILD_NAME expect(getObservabilityBuild({ key: 'key-root', testObservabilityOptions: { } } as any, 'build-name')).toEqual('build-name') }) it('get default', () => { delete process.env.TEST_OBSERVABILITY_BUILD_NAME expect(getObservabilityBuild({})).toEqual(path.basename(path.resolve(process.cwd()))) }) }) describe('getObservabilityProject', () => { it('get env var', () => { process.env.TEST_OBSERVABILITY_PROJECT_NAME = 'try' expect(getObservabilityProject({})).toEqual('try') delete process.env.TEST_OBSERVABILITY_PROJECT_NAME }) it('get name passed in testObservabilityOptions', () => { delete process.env.TEST_OBSERVABILITY_PROJECT_NAME expect(getObservabilityProject({ testObservabilityOptions: { projectName: 'project' } } as any)).toEqual('project') }) it('get name passed at root level', () => { delete process.env.TEST_OBSERVABILITY_PROJECT_NAME expect(getObservabilityProject({ key: 'key-root', testObservabilityOptions: { } } as any, 'project-name')).toEqual('project-name') }) it('get undefined', () => { delete process.env.TEST_OBSERVABILITY_PROJECT_NAME expect(getObservabilityProject({})).toEqual(undefined) }) }) describe('getObservabilityBuildTags', () => { it('get array from env var', () => { process.env.TEST_OBSERVABILITY_BUILD_TAG = 'try,qa' expect(getObservabilityBuildTags({})).toEqual(['try', 'qa']) delete process.env.TEST_OBSERVABILITY_BUILD_TAG }) it('get tags passed in testObservabilityOptions', () => { delete process.env.TEST_OBSERVABILITY_BUILD_TAG expect(getObservabilityBuildTags({ testObservabilityOptions: { buildTag: ['qa', 'test'] } } as any)).toEqual(['qa', 'test']) }) it('get name passed at root level', () => { delete process.env.TEST_OBSERVABILITY_BUILD_TAG expect(getObservabilityBuildTags({ key: 'key-root', testObservabilityOptions: { } } as any, 'qa')).toEqual(['qa']) }) it('get empty array', () => { delete process.env.TEST_OBSERVABILITY_BUILD_TAG expect(getObservabilityBuildTags({})).toEqual([]) }) }) describe('o11yErrorHandler', () => { let spy: any beforeEach(() => { spy = vi.spyOn(CrashReporter, 'uploadCrashReport') spy.mockImplementation(() => {}) }) afterEach(() => { spy.mockClear() spy.mockReset() }) describe('synchronous function', () => { const func = (a: number, b: number) => { if (a === 0 && b === 0) { throw 'zero error' } return a + b } it ('should pass the arguments and return value correctly', () => { const newFunc = o11yErrorHandler(func) expect(() => { expect(newFunc(1, 2)).toEqual(3) }).not.toThrow() expect(spy).toBeCalledTimes(0) }) it('should catch error thrown from function', () => { const newFunc = o11yErrorHandler(func) expect(() => { newFunc(0, 0) }).not.toThrow() expect(spy).toBeCalledTimes(1) }) }) describe('asynchronous function', () => { const func = async (a: number, b: number) => { const val = await new Promise(resolve => { if (a === 0 && b === 0) { throw 'zero error' } resolve(a * b) }) return val } it('should return values correctly from async function', async () => { const newFunc = o11yErrorHandler(func) const val = await newFunc(1, 2) expect(val).toEqual(2) expect(spy).toBeCalledTimes(0) }) it('should catch error from async function', async () => { const newFunc = o11yErrorHandler(func) await newFunc(0, 0) expect(spy).toBeCalledTimes(1) }) }) }) describe('validateCapsWithAppA11y', () => { let logInfoMock: any beforeEach(() => { logInfoMock = vi.spyOn(log, 'warn') }) it('returns false if platform version is lesser than 11', async () => { const platformMeta = { platform_name: 'android', platform_version: '10.0' } expect(validateCapsWithAppA11y(platformMeta)).toEqual(false) expect(logInfoMock.mock.calls[0][0]) .toContain('App Accessibility Automation tests are supported on OS version 11 and above for Android devices.') }) it('returns true if validation done', async () => { const platformMeta = { 'platform_name': 'android', 'platform_version': '13.0' } expect(validateCapsWithAppA11y(undefined, platformMeta)).toEqual(true) }) }) describe('validateCapsWithA11y', () => { let logInfoMock: any beforeEach(() => { logInfoMock = vi.spyOn(log, 'warn') }) it('returns false if deviceName is defined', async () => { expect(validateCapsWithA11y('Samsung S22')).toEqual(false) expect(logInfoMock.mock.calls[0][0]) .toContain('Accessibility Automation will run only on Desktop browsers.') }) it('returns false if browser is not chrome', async () => { const platformMeta = { 'browser_name': 'safari' } expect(validateCapsWithA11y(undefined, platformMeta)).toEqual(false) expect(logInfoMock.mock.calls[0][0]) .toContain('Accessibility Automation will run only on Chrome browsers.') }) it('returns false if browser version is lesser than 94', async () => { const platformMeta = { 'browser_name': 'chrome', 'browser_version': '90' } expect(validateCapsWithA11y(undefined, platformMeta)).toEqual(false) expect(logInfoMock.mock.calls[0][0]) .toContain('Accessibility Automation will run only on Chrome browser version greater than 94.') }) it('returns false if browser version is lesser than 94', async () => { const platformMeta = { 'browser_name': 'chrome', 'browser_version': 'latest' } const chromeOptions = { args: ['--headless'] } expect(validateCapsWithA11y(undefined, platformMeta, chromeOptions)).toEqual(false) expect(logInfoMock.mock.calls[0][0]) .toContain('Accessibility Automation will not run on legacy headless mode. Switch to new headless mode or avoid using headless mode.') }) it('returns true if validation done', async () => { const platformMeta = { 'browser_name': 'chrome', 'browser_version': 'latest' } const chromeOptions = {} expect(validateCapsWithA11y(undefined, platformMeta, chromeOptions)).toEqual(true) }) }) describe('validateCapsWithNonBstackA11y', () => { let logInfoMock: any beforeEach(() => { logInfoMock = vi.spyOn(log, 'warn') }) it('returns false if browser is not chrome', async () => { const browserName = 'safari' const browserVersion = 'latest' expect(validateCapsWithNonBstackA11y(browserName, browserVersion)).toEqual(false) expect(logInfoMock.mock.calls[0][0]) .toContain('Accessibility Automation will run only on Chrome browsers.') }) it('returns false if browser version is lesser than 100', async () => { const browserName = 'chrome' const browserVersion = '98' expect(validateCapsWithNonBstackA11y(browserName, browserVersion)).toEqual(false) expect(logInfoMock.mock.calls[0][0]) .toContain('Accessibility Automation will run only on Chrome browser version greater than 100.') }) it('returns true if validation done', async () => { const browserName = 'chrome' const browserVersion = 'latest' expect(validateCapsWithNonBstackA11y(browserName, browserVersion)).toEqual(true) }) }) describe('shouldScanTestForAccessibility', () => { const cucumberWorldObj = { pickle: { tags: [ { name: 'someTag' } ] } } it('returns true if full test name contains includeTags', async () => { expect(shouldScanTestForAccessibility('suite title', 'test title', { includeTagsInTestingScope: 'title' })).toEqual(true) }) it('returns false if full test name contains excludeTags', async () => { expect(shouldScanTestForAccessibility('suite title', 'test title', { excludeTagsInTestingScope: 'title' })).toEqual(true) }) it('returns true if cucumber tags contain includeTags', async () => { expect(shouldScanTestForAccessibility('suite title', 'test title', { includeTagsInTestingScope: 'someTag' }, cucumberWorldObj, true )).toEqual(true) }) it('returns false if cucumber tags contain excludeTags', async () => { expect(shouldScanTestForAccessibility('suite title', 'test title', { excludeTagsInTestingScope: 'someTag' }, cucumberWorldObj, true)).toEqual(true) }) }) describe('isAccessibilityAutomationSession', () => { it('returns true if accessibility is true and ally token is present', async () => { process.env.BSTACK_A11Y_JWT = 'someToken' expect(isAccessibilityAutomationSession(true)).toEqual(true) }) it('returns true if accessibility is true and ally token is present', async () => { process.env.BSTACK_A11Y_JWT = '' expect(isAccessibilityAutomationSession(true)).toEqual(false) }) }) describe('isAppAccessibilityAutomationSession', () => { it('returns true if accessibility and app automate are true and app ally token is present', async () => { process.env.BSTACK_A11Y_JWT = 'someToken' expect(isAppAccessibilityAutomationSession(true, true)).toEqual(true) }) it('returns true if accessibility and app automate are true and app ally token is present', async () => { process.env.BSTACK_A11Y_JWT = '' expect(isAppAccessibilityAutomationSession(true, true)).toEqual(false) }) }) describe('getA11yResults', () => { const browser = { sessionId: 'session123', config: {}, capabilities: { device: '', os: 'OS X', os_version: 'Catalina', browserName: 'chrome' }, instances: ['browserA', 'browserB'], isMultiremote: false, browserA: { sessionId: 'session456', capabilities: { 'bstack:options': { device: '', os: 'Windows', osVersion: 10, browserName: 'chrome' } } }, getInstance: vi.fn().mockImplementation((browserName: string) => browser[browserName]), browserB: {}, execute: vi.fn(), executeAsync: vi.fn(), on: vi.fn(), } as unknown as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser it('return success object if ally token defined and no error in response data', async () => { vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(false) const result: any = await utils.getA11yResults((browser as WebdriverIO.Browser), true, false) expect(result).toEqual([]) }) it('should call executeAccessibilityScript if bstack and accessibility session are enabled', async () => { process.env.BSTACK_A11Y_JWT = 'abc' vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true) const executeAccessibilityScriptSpy = vi .spyOn(utils, 'executeAccessibilityScript') .mockResolvedValue(undefined) vi.spyOn(AccessibilityScripts, 'getResults', 'get').mockReturnValue('mocked_results_script') const results = await utils.getA11yResults(false, browser as WebdriverIO.Browser, true, true) expect(results).toEqual(undefined) executeAccessibilityScriptSpy.mockRestore() delete process.env.BSTACK_A11Y_JWT }) }) describe('getA11yResultsSummary', () => { const browser = { sessionId: 'session123', config: {}, capabilities: { device: '', os: 'OS X', os_version: 'Catalina', browserName: 'chrome' }, instances: ['browserA', 'browserB'], isMultiremote: false, browserA: { sessionId: 'session456', capabilities: { 'bstack:options': { device: '', os: 'Windows', osVersion: 10, browserName: 'chrome' } } }, getInstance: vi.fn().mockImplementation((browserName: string) => browser[browserName]), browserB: {}, execute: vi.fn(), executeAsync: vi.fn(), on: vi.fn(), } as unknown as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser it('return success object if ally token defined and no error in response data', async () => { vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(false) const result: any = await utils.getA11yResultsSummary((browser as WebdriverIO.Browser), true, false) expect(result).toEqual({}) }) it('returns results object for an accessibility session', async () => { process.env.BSTACK_A11Y_JWT = 'abc' AccessibilityScripts.getResultsSummary = 'mockScript' vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true) const mockExecuteAccessibilityScript = vi .spyOn(utils, 'executeAccessibilityScript') .mockResolvedValue({ }) const result = await utils.getA11yResultsSummary(false, {} as WebdriverIO.Browser, true, true) delete process.env.BSTACK_A11Y_JWT expect(result).toEqual({ }) }) }) describe('isTrue', () => { it('returns true if value is `true`', async () => { expect(isTrue('true')).toEqual(true) }) it('returns false if value is `false`', async () => { expect(isTrue('false')).toEqual(false) }) it('returns false if value is undefined', async () => { expect(isTrue(undefined)).toEqual(false) }) }) describe('getBrowserStackUser', function () { it('should return userName from config if not present as env variable', function () { expect(utils.getBrowserStackUser({ user: 'config_user_name', key: 'config_user_key', capabilities: {} })).toEqual('config_user_name') }) it('should return userName if present as env variable', function () { process.env.BROWSERSTACK_USERNAME = 'user_name' expect(utils.getBrowserStackUser({ user: 'config_user_name', key: 'config_user_key', capabilities: {} })).toEqual('user_name') }) }) describe('getBrowserStackKey', function () { it('should return accessKey from config if not present as env variable', function () { expect(utils.getBrowserStackKey({ user: 'config_user_name', key: 'config_user_key', capabilities: {} })).toEqual('config_user_key') }) it('should return accessKey if present as env variable', function () { process.env.BROWSERSTACK_ACCESS_KEY = 'user_key' expect(utils.getBrowserStackKey({ user: 'config_user_name', key: 'config_user_key', capabilities: {} })).toEqual('user_key') }) }) describe('frameworkSupportsHook', function () { describe('mocha', function () { it('should return true for beforeHook', function () { expect(frameworkSupportsHook('before', 'mocha')).toBe(true) }) }) describe('cucumber', function () { it('should return true for cucumber', function () { expect(frameworkSupportsHook('before', 'cucumber')).toBe(true) }) }) it('should return false for any other framework', function () { expect(frameworkSupportsHook('before', 'jasmine')).toBe(false) }) }) describe('uploadLogs', function () { let tempLogFile: string beforeAll(async () => { tempLogFile = path.join(os.tmpdir(), 'test-logs.txt') await fs.writeFile(tempLogFile, 'mock log content') bstackLogger.BStackLogger.logFilePath = tempLogFile vi.mocked(fetch).mockClear() vi.mocked(fetch).mockReturnValueOnce(Promise.resolve(Response.json({ status: 'success', message: 'Logs uploaded Successfully' }))) }) it('should return if user is undefined', async function () { await uploadLogs(undefined, 'some_key', 'some_uuid') expect(fetch).not.toHaveBeenCalled() }) it('should return if key is undefined', async function () { await uploadLogs('some_user', undefined, 'some_uuid') expect(fetch).not.toHaveBeenCalled() }) it('should upload the logs', async function () { await uploadLogs('some_user', 'some_key', 'some_uuid') expect(fetch).toHaveBeenCalled() }) afterAll(async () => { await fs.unlink(tempLogFile) vi.mocked(fetch).mockClear() vi.restoreAllMocks() }) }) describe('getFailureObject', function () { it('should return parsed failure object for string error', function () { const error = 'some error' expect(getFailureObject(error)).toEqual({ failure: [{ backtrace: [''] }], failure_reason: 'some error', failure_type: 'UnhandledError' }) }) it('should parse for assertion error', function () { const error = new Error('AssertionError: 2 is not equal to 4') expect(getFailureObject(error)).toMatchObject({ failure_reason: 'AssertionError: 2 is not equal to 4', failure_type: 'AssertionError' }) }) it ('should get stacktrace for error object', function () { const error = new Error('some error') expect(getFailureObject(error)).toMatchObject({ failure: [{ backtrace: [error.stack.toString()] }] }) }) }) describe('getObservabilityProduct', () => { it ('should return app automate', function () { expect(getObservabilityProduct(undefined, true)).toEqual('app-automate') }) }) describe('isUndefined', () => { it ('should return true for empty string', function () { expect(isUndefined('')).toEqual(true) }) }) describe('processTestObservabilityResponse', () => { let response: LaunchResponse, handleErrorForObservabilitySpy beforeAll(() => { response = { jwt: 'abc', build_hashed_id: 'abc', observability: { success: true, options: {}, errors: undefined }, accessibility: { success: true, options: { status: 'true', commandsToWrap: { scriptsToRun: [], commands: [] }, scripts: [{ name: 'abc', command: 'abc' }], capabilities: [{ name: 'abc', value: 'abc' }] }, errors: undefined } } }) it ('processTestObservabilityResponse should not log an error', function () { processTestObservabilityResponse(response) expect(process.env[BROWSERSTACK_OBSERVABILITY]).toEqual('true') }) it ('processTestObservabilityResponse should log error if observability success is false', function () { handleErrorForObservabilitySpy = vi.spyOn(testHubUtils, 'handleErrorForObservability').mockReturnValue({} as any) const res = response res.observability!.success = false processTestObservabilityResponse(res) expect(handleErrorForObservabilitySpy).toBeCalled() }) it ('processTestObservabilityResponse should log error if observability field not found', function () { handleErrorForObservabilitySpy = vi.spyOn(testHubUtils, 'handleErrorForObservability').mockReturnValue({} as any) const res = response res.observability = undefined processTestObservabilityResponse(res) expect(handleErrorForObservabilitySpy).toBeCalled() }) afterEach(() => { handleErrorForObservabilitySpy?.mockClear() }) }) describe('processAccessibilityResponse', () => { let response: LaunchResponse, handleErrorForAccessibilitySpy let options: BrowserstackConfig & Options.Testrunner beforeAll(() => { response = { jwt: 'abc', build_hashed_id: 'abc', observability: { success: true, options: {}, errors: undefined }, accessibility: { success: true, options: { status: 'true', commandsToWrap: { scriptsToRun: [], commands: [] }, scripts: [{ name: 'abc', command: 'abc' }], capabilities: [ { name: 'accessibilityToken', value: 'abc' }, { name: 'scannerVersion', value: 'abc' } ] }, errors: undefined } } options = {} }) it ('processAccessibilityResponse should not log an error', function () { const optionsWithAccessibilityTrue = options optionsWithAccessibilityTrue.accessibility = true processAccessibilityResponse(response, optionsWithAccessibilityTrue) expect(process.env[BROWSERSTACK_ACCESSIBILITY]).toEqual('true') }) it ('processAccessibilityResponse should log error if accessibility success is false', function () { handleErrorForAccessibilitySpy = vi.spyOn(testHubUtils, 'handleErrorForAccessibility').mockReturnValue({} as any) const res = response res.accessibility!.success = false const optionsWithAccessibilityTrue = options optionsWithAccessibilityTrue.accessibility = true processAccessibilityResponse(res, optionsWithAccessibilityTrue) expect(handleErrorForAccessibilitySpy).toBeCalled() }) it ('processAccessibilityResponse should log error if accessibility field not found', function () { handleErrorForAccessibilitySpy = vi.spyOn(testHubUtils, 'handleErrorForAccessibility').mockReturnValue({} as any) const res = response res.accessibility = undefined const optionsWithAccessibilityTrue = options optionsWithAccessibilityTrue.accessibility = true processAccessibilityResponse(res, optionsWithAccessibilityTrue) expect(handleErrorForAccessibilitySpy).toBeCalled() }) it ('processAccessibilityResponse should not log error if accessibility field not found & accessibility not found in options', function () { handleErrorForAccessibilitySpy = vi.spyOn(testHubUtils, 'handleErrorForAccessibility').mockReturnValue({} as any) const res = response res.accessibility = undefined const optionsWithAccessibilityNull = options optionsWithAccessibilityNull.accessibility = null processAccessibilityResponse(res, optionsWithAccessibilityNull) expect(handleErrorForAccessibilitySpy).toBeCalledTimes(0) }) afterEach(() => { handleErrorForAccessibilitySpy?.mockClear() }) }) describe('processLaunchBuildResponse', () => { let response: LaunchResponse, observabilitySpy, accessibilitySpy beforeAll(() => { response = { jwt: 'abc', build_hashed_id: 'abc', observability: { success: true, options: {}, errors: undefined }, accessibility: { success: true, options: { status: 'true', commandsToWrap: { scriptsToRun: [], commands: [] }, scripts: [{ name: 'abc', command: 'abc' }], capabilities: [{ name: 'accessibilityToken', value: 'abc' }] }, errors: undefined } } }) beforeEach(() => { observabilitySpy = vi.spyOn(utils, 'processTestObservabilityResponse').mockImplementation(() => {}) accessibilitySpy = vi.spyOn(utils, 'processAccessibilityResponse').mockImplementation(() => {}) }) it ('processTestObservabilityResponse should be called', function () { processLaunchBuildResponse(response, { testObservability: true, accessibility: true, capabilities: {} }) expect(process.env[BROWSERSTACK_OBSERVABILITY]).toEqual('true') }) it ('processAccessibilityResponse should be called', function () { processLaunchBuildResponse(response, { testObservability: true, accessibility: true, capabilities: {} }) expect(process.env[BROWSERSTACK_ACCESSIBILITY]).toEqual('true') }) afterEach(() => { observabilitySpy?.mockClear() accessibilitySpy?.mockClear() }) }) describe('jsonifyAccessibilityArray', () => { const array = [{ name: 'accessibilityToken', value: 'abc' }] it('jsonifyAccessibilityArray', () => { expect(jsonifyAccessibilityArray(array, 'name', 'value')).toEqual({ 'accessibilityToken': 'abc' }) }) }) describe('logPatcher', () => { let emitSpy: jest.SpyInstance beforeEach(() => { emitSpy = vi.spyOn(process, 'emit') as unknown as vi.SpyInstance }) afterEach(() => { emitSpy.mockRestore() }) it('logPatcher methods should emit data', () => { const BSTestOpsPatcher = new logPatcher({}) BSTestOpsPatcher.info('abc') BSTestOpsPatcher.error('abc') BSTestOpsPatcher.warn('abc') BSTestOpsPatcher.trace('abc') BSTestOpsPatcher.debug('abc') BSTestOpsPatcher.log('abc') expect(emitSpy).toHaveBeenCalled() }) }) describe('formatString', () => { it('should replace %s placeholders with provided values in order', () => { const template = 'Hello %s, your score is %s' const values = ['John', '100'] expect(formatString(template, ...values)).toBe('Hello John, your score is 100') }) it('should handle null values in array', () => { const template = 'Name: %s, Score: %s' const values = ['John', null] expect(formatString(template, ...values)).toBe('Name: John, Score: ') }) it('should handle null template', () => { const template = null const values = ['John', '100'] expect(formatString(template, ...values)).toBe('') }) it('should handle undefined values', () => { const template = 'Value: %s' const values = [undefined] expect(formatString(template, ...values)).toBe('Value: ') }) it('should handle template without placeholders', () => { const template = 'Hello World' const values = ['John', null] expect(formatString(template, ...values)).toBe('Hello World') }) it('should handle empty template string', () => { const template = '' const values = ['John', null] expect(formatString(template, ...values)).toBe('') }) }) describe('_getParamsForAppAccessibility', () => { const originalEnv = process.env beforeEach(() => { process.env = { TEST_ANALYTICS_ID: 'test-123', BROWSERSTACK_TESTHUB_UUID: 'build-456', BROWSERSTACK_TESTHUB_JWT: 'jwt-789', BSTACK_A11Y_JWT: 'auth-abc' } }) afterEach(() => { process.env = originalEnv }) it('should return params object with command name when provided', () => { const result = _getParamsForAppAccessibility('clickElement') expect(result).toEqual({ thTestRunUuid: 'test-123', thBuildUuid: 'build-456', thJwtToken: 'jwt-789', authHeader: 'auth-abc', scanTimestamp: Date.now(), method: 'clickElement' }) }) it('should return params object with undefined method when no command name provided', () => { const result = _getParamsForAppAccessibility() expect(result).toEqual({ thTestRunUuid: 'test-123', thBuildUuid: 'build-456', thJwtToken: 'jwt-789', authHeader: 'auth-abc', scanTimestamp: Date.now(), method: undefined }) }) it('should handle missing environment variables', () => { process.env = {} const result = _getParamsForAppAccessibility('test') expect(result).toEqual({ thTestRunUuid: undefined, thBuildUuid: undefined, thJwtToken: undefined, authHeader: undefined, scanTimestamp: Date.now(), method: 'test' }) }) it('should handle partial environment variables', () => { process.env = { TEST_ANALYTICS_ID: 'test-123', BROWSERSTACK_TESTHUB_UUID: 'build-456' } const result = _getParamsForAppAccessibility('test') expect(result).toEqual({ thTestRunUuid: 'test-123', thBuildUuid: 'build-456', thJwtToken: undefined, authHeader: undefined, scanTimestamp: Date.now(), method: 'test' }) }) }) describe('performA11yScan', () => { let browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser let logInfoMock: any beforeEach(() => { logInfoMock = vi.spyOn(log, 'warn') }) it('should return early if not an Accessibility Automation session', async () => { browser = { execute: async () => ({ success: true }), executeAsync: async () => ({ success: true }), } as unknown as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser const result = await performA11yScan(false, browser, true, false) expect(result).toBeUndefined() expect(logInfoMock.mock.calls[0][0]) .toContain('Not an Accessibility Automation session, cannot perform Accessibility scan.') }) it('should perform app accessibility scan when isAppAutomate is true', async () => { const mockResults = { success: true } const mockScanScript = 'scan script with param: %s' vi.spyOn(AccessibilityScripts, 'performScan', 'get').mockReturnValue(mockScanScript) const browser = { execute: vi.fn().mockResolvedValue(mockResults), executeAsync: vi.fn().mockResolvedValue(mockResults), capabilities: {} } as unknown as WebdriverIO.Browser process.env.TEST_ANALYTICS_ID = 'test-123' process.env.BROWSERSTACK_TESTHUB_UUID = 'build-456' process.env.BROWSERSTACK_TESTHUB_JWT = 'jwt-789' process.env.BSTACK_A11Y_JWT = 'auth-abc' vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true) vi.spyOn(utils, 'isAppAccessibilityAutomationSession').mockReturnValue(true) const result = await performA11yScan(true, browser, true, true, 'clickElement') expect(result).toEqual(mockResults) expect(browser.execute).toHaveBeenCalledWith( expect.stringContaining('scan script with param:'), {} ) delete process.env.TEST_ANALYTICS_ID delete process.env.BROWSERSTACK_TESTHUB_UUID delete process.env.BROWSERSTACK_TESTHUB_JWT delete process.env.BSTACK_A11Y_JWT }) it('should perform web accessibility scan when isAppAutomate is false', async () => { const mockResults = { success: true } const browser = { execute: vi.fn().mockResolvedValue(mockResults), executeAsync: vi.fn().mockResolvedValue(mockResults), capabilities: {} } as unknown as WebdriverIO.Browser process.env.TEST_ANALYTICS_ID = 'test-123' process.env.BROWSERSTACK_TESTHUB_UUID = 'build-456' process.env.BROWSERSTACK_TESTHUB_JWT = 'jwt-789' process.env.BSTACK_A11Y_JWT = 'auth-abc' vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true) vi.spyOn(utils, 'isAppAccessibilityAutomationSession').mockReturnValue(false) vi.spyOn(AccessibilityScripts, 'performScan', 'get').mockReturnValue('scan_script_for_web') const result = await performA11yScan(false, browser, true, true, 'clickElement') expect(result).toEqual(mockResults) expect(browser.execute).toHaveBeenCalledWith( expect.stringContaining('scan_script_for_web'), ) delete process.env.TEST_ANALYTICS_ID delete process.env.BROWSERSTACK_TESTHUB_UUID delete process.env.BROWSERSTACK_TESTHUB_JWT delete process.env.BSTACK_A11Y_JWT }) }) describe('getAppA11yResults', () => { let browser: WebdriverIO.Browser let logInfoMock: any beforeEach(() => { logInfoMock = vi.spyOn(log, 'warn') const result = { data: { issues: [{ 'issueName': 'Readable Text Spacing' }], }, } vi.mocked(fetch).mockClear() vi.mocked(fetch).mockResolvedValue({ json: async () => (result), headers: new Headers(), ok: true, status: 200, } as Response) logInfoMock = vi.spyOn(log, 'warn') }) it('should return empty array if not a BrowserStack session', async () => { browser = { execute: async () => ({ success: true }), executeAsync: async () => ({ success: true }), capabilities: {} } as unknown as WebdriverIO.Browser const result = await getAppA11yResults(true, browser, false, true) expect(result).toEqual([]) }) it('should return empty array if not an App Accessibility Automation session', async () => { browser = { execute: async () => ({ success: true }), executeAsync: async () => ({ success: true }), capabilities: {} } as unknown as WebdriverIO.Browser const result = await getAppA11yResults(true, browser, true, false) expect(result).toEqual([]) expect(logInfoMock.mock.calls[0][0]) .toContain('Not an Accessibility Automation session, cannot retrieve Accessibility results summary.') }) it('should return results for valid app accessibility session', async () => { const mockResults = [{ 'issueName': 'Readable Text Spacing' }] const browser = { execute: vi.fn().mockResolvedValue(mockResults), executeAsync: vi.fn().mockResolvedValue(mockResults), capabilities: {}, } as unknown as WebdriverIO.Browser process.env.BSTACK_A11Y_POLLING_TIMEOUT = '30' process.env.TEST_ANALYTICS_ID = 'test-123' process.env.BSTACK_A11Y_JWT = 'abc' vi.spyOn(utils, 'isAppAccessibilityAutomationSession').mockReturnValue(true) vi.spyOn(utils, 'performA11yScan').mockResolvedValue(undefined) const result = await getAppA11yResults(true, browser, true, true, 'session123') expect(result).toEqual(mockResults) delete process.env.BSTACK_A11Y_POLLING_TIMEOUT delete process.env.TEST_ANALYTICS_ID delete process.env.BSTACK_A11Y_JWT }) it('should return empty array if error occurs during fetch', async () => { browser = { execute: async () => { throw new Error('Test error') }, executeAsync: async () => ({ success: true }), capabilities: {} } as unknown as WebdriverIO.Browser const result = await getAppA11yResults(true, browser, true, true, 'session123') expect(result).toEqual([]) }) }) describe('getAppA11yResultsSummary', () => { let browser: WebdriverIO.Browser let logInfoMock: any let pollApiMock: any beforeEach(() => { logInfoMock = vi.spyOn(log, 'warn') const result = { data: { summary: { 'totalIssueCount': 64, 'totalBySeverity': { 'minor': 0, 'serious': 0, 'critical': 6, 'moderate': 58, }, }, }, } vi.mocked(fetch).mockClear() vi.mocked(fetch).mockResolvedValue({ json: async () => (result), headers: new Headers(), ok: true, status: 200, } as Response) logInfoMock = vi.spyOn(log, 'warn') pollApiMock = vi.spyOn(utils, 'pollApi').mockResolvedValue({ data: { data: { summary: {} } } } as any) }) it('should return empty object if not a BrowserStack session', async () => { browser = { execute: async () => ({ success: true }), executeAsync: async () => ({ success: true }), capabilities: {} } as unknown as WebdriverIO.Browser const result = await getAppA11yResultsSummary(true, browser, false, true) expect(result).toEqual({}) }) it('should return empty object if not an App Accessibility Automation session', async () => { browser = { execute: async () => ({ success: true }), executeAsync: async () => ({ success: true }), capabilities: {} } as unknown as WebdriverIO.Browser const result = await getAppA11yResultsSummary(true, browser, true, false) expect(result).toEqual({}) expect(logInfoMock.mock.calls[0][0]) .toContain('Not an Accessibility Automation session, cannot retrieve Accessibility results summary.') }) it('should return results summary for valid app accessibility session', async () => { const mockResults = { 'totalIssueCount' : 64, 'totalBySeverity': { 'minor':0, 'serious':0, 'critical': 6, 'moderate': 58 } } const browser = { execute: vi.fn().mockResolvedValue(mockResults), executeAsync: vi.fn().mockResolvedValue(mockResults), capabilities: {}, } as unknown as WebdriverIO.Browser process.env.BSTACK_A11Y_POLLING_TIMEOUT = '30' process.env.TEST_ANALYTICS_ID = 'test-123' process.env.BSTACK_A11Y_JWT = 'abc' vi.spyOn(utils, 'isAppAccessibilityAutomationSession').mockReturnValue(true) vi.spyOn(utils, 'performA11yScan').mockResolvedValue(undefined) const result = await getAppA11yResultsSummary(true, browser, true, true, 'session123') expect(result).toEqual(mockResults) delete process.env.BSTACK_A11Y_POLLING_TIMEOUT delete process.env.TEST_ANALYTICS_ID delete process.env.BSTACK_A11Y_JWT }) it('should return empty object if error occurs during fetch', async () => { browser = { execute: async () => { throw new Error('Test error') }, executeAsync: async () => ({ success: true }), capabilities: {} } as unknown as WebdriverIO.Browser pollApiMock.mockRejectedValueOnce(new Error('API Error')) const result = await getAppA11yResultsSummary(true, browser, true, true, 'session123') expect(result).toEqual({}) }) afterEach(() => { delete process.env.TEST_ANALYTICS_ID delete process.env.BSTACK_A11Y_JWT vi.clearAllMocks() }) describe('mergeDeep', () => { it('should deeply merge two objects', () => { const target = { a: 1, b: { c: 2 } } const source = { b: { d: 3 }, e: 4 } const result = mergeDeep(target, source) expect(result).toEqual({ a: 1, b: { c: 2, d: 3 }, e: 4 }) }) it('should handle empty sources', () => { const target = { a: 1 } const result = mergeDeep(target) expect(result).toEqual({ a: 1 }) }) }) describe('mergeChromeOptions', () => { it('should merge ChromeOptions args and extensions correctly', () => { const base = { args: ['--disable-gpu'], extensions: ['ext1'], prefs: { homepage: 'https://example.com' } } const override = { args: ['--headless'], extensions: ['ext2'], prefs: { newtab: 'https://newtab.com' } } const result = mergeChromeOptions(base, override) expect(result.args).toEqual(['--disable-gpu', '--headless']) expect(result.extensions).toEqual(['ext1', 'ext2']) expect(result.prefs).toEqual({ homepage: 'https://example.com', newtab: 'https://newtab.com' }) }) }) })