diff --git a/packages/wdio-browserstack-service/src/accessibility-handler.ts b/packages/wdio-browserstack-service/src/accessibility-handler.ts index aad3ce7d16b..01c7f981ce5 100644 --- a/packages/wdio-browserstack-service/src/accessibility-handler.ts +++ b/packages/wdio-browserstack-service/src/accessibility-handler.ts @@ -1,6 +1,8 @@ import util from 'node:util' -import type { Capabilities, Frameworks } from '@wdio/types' +import type { Capabilities, Frameworks, Options } from '@wdio/types' + +import type { BrowserstackConfig, BrowserstackOptions } from './types.js' import type { ITestCaseHookParameter } from './cucumber-types.js' @@ -19,6 +21,8 @@ import { o11yClassErrorHandler, shouldScanTestForAccessibility, validateCapsWithA11y, + shouldAddServiceVersion, + validateCapsWithNonBstackA11y, isTrue, validateCapsWithAppA11y, getAppA11yResults @@ -34,6 +38,9 @@ class _AccessibilityHandler { private _caps: Capabilities.RemoteCapability private _suiteFile?: string private _accessibility?: boolean + private _turboscale?: boolean + private _options: BrowserstackConfig & BrowserstackOptions + private _config: Options.Testrunner private _accessibilityOptions?: { [key: string]: any; } private _testMetadata: { [key: string]: any; } = {} private static _a11yScanSessionMap: { [key: string]: any; } = {} @@ -43,9 +50,12 @@ class _AccessibilityHandler { constructor ( private _browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, private _capabilities: Capabilities.RemoteCapability, + _options : BrowserstackConfig & BrowserstackOptions, private isAppAutomate: boolean, + _config : Options.Testrunner, private _framework?: string, private _accessibilityAutomation?: boolean | string, + _turboscale?: boolean | string, private _accessibilityOpts?: { [key: string]: any; } ) { const caps = (this._browser as WebdriverIO.Browser).capabilities as WebdriverIO.Capabilities @@ -62,6 +72,9 @@ class _AccessibilityHandler { this._caps = _capabilities this._accessibility = isTrue(_accessibilityAutomation) this._accessibilityOptions = _accessibilityOpts + this._options = _options + this._config= _config + this._turboscale = isTrue(_turboscale) } setSuiteFile(filename: string) { @@ -101,7 +114,12 @@ class _AccessibilityHandler { this._sessionId = sessionId this._accessibility = isTrue(this._getCapabilityValue(this._caps, 'accessibility', 'browserstack.accessibility')) - if (isBrowserstackSession(this._browser)) { + //checks for running ALLY on non-bstack infra + if (isAccessibilityAutomationSession(this._accessibility) && (this._turboscale || !shouldAddServiceVersion(this._config, this._options.testObservability))){ + if (validateCapsWithNonBstackA11y(this._platformA11yMeta.browser_name as string, this._platformA11yMeta?.browser_version as string)){ + this._accessibility = true + } + } else { if (isAccessibilityAutomationSession(this._accessibility) && !this.isAppAutomate) { const deviceName = this._getCapabilityValue(this._caps, 'deviceName', 'device') const chromeOptions = this._getCapabilityValue(this._caps, 'goog:chromeOptions', '') @@ -331,7 +349,7 @@ class _AccessibilityHandler { if (!browser) { return false } - return isBrowserstackSession(browser) && isAccessibilityAutomationSession(isAccessibility) + return isAccessibilityAutomationSession(isAccessibility) } private async checkIfPageOpened(browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, testIdentifier: string, shouldScanTest?: boolean) { diff --git a/packages/wdio-browserstack-service/src/launcher.ts b/packages/wdio-browserstack-service/src/launcher.ts index 66ef8c6ec49..1f9939b9ac7 100644 --- a/packages/wdio-browserstack-service/src/launcher.ts +++ b/packages/wdio-browserstack-service/src/launcher.ts @@ -39,6 +39,8 @@ import { ObjectsAreEqual, isValidCapsForHealing, getBooleanValueFromString, + validateCapsWithNonBstackA11y, + mergeChromeOptions } from './util.js' import { getProductMap } from './testHub/utils.js' import CrashReporter from './crash-reporter.js' @@ -53,6 +55,7 @@ import AiHandler from './ai-handler.js' import TestOpsConfig from './testOps/testOpsConfig.js' import PerformanceTester from './instrumentation/performance/performance-tester.js' import * as PERFORMANCE_SDK_EVENTS from './instrumentation/performance/constants.js' +import accessibilityScripts from './scripts/accessibility-scripts.js' type BrowserstackLocal = BrowserstackLocalLauncher.Local & { pid?: number @@ -291,6 +294,12 @@ export default class BrowserstackLauncherService implements Services.ServiceInst buildIdentifier: this._buildIdentifier }, this.browserStackConfig, this._accessibilityAutomation) + //added checks for Accessibility running on non-bstack infra + if (isAccessibilityAutomationSession(this._accessibilityAutomation) && (process.env.BROWSERSTACK_TURBOSCALE || !shouldAddServiceVersion(this._config, this._options.testObservability))){ + const overrideOptions: Partial = accessibilityScripts.ChromeExtension + this._updateObjectTypeCaps(capabilities, 'goog:chromeOptions', overrideOptions) + } + if (buildStartResponse?.accessibility) { if (this._accessibilityAutomation === null) { this.browserStackConfig.accessibility = buildStartResponse.accessibility.success as boolean @@ -571,6 +580,20 @@ export default class BrowserstackLauncherService implements Services.ServiceInst return c as (Capabilities.DesiredCapabilities) }) .forEach((capability: Capabilities.DesiredCapabilities) => { + + if (validateCapsWithNonBstackA11y(capability.browserName, capability.browserVersion )){ + if (capType === 'goog:chromeOptions' && value) { + + const chromeOptions = capability['goog:chromeOptions'] as unknown as Capabilities.ChromeOptions + if (chromeOptions){ + const finalChromeOptions = mergeChromeOptions(chromeOptions, value) + capability['goog:chromeOptions'] = finalChromeOptions + } else { + capability['goog:chromeOptions'] = value + } + return + } + } if (!capability['bstack:options']) { const extensionCaps = Object.keys(capability).filter((cap) => cap.includes(':')) if (extensionCaps.length) { @@ -604,6 +627,21 @@ export default class BrowserstackLauncherService implements Services.ServiceInst }) } else if (typeof capabilities === 'object') { Object.entries(capabilities as Capabilities.MultiRemoteCapabilities).forEach(([, caps]) => { + if (validateCapsWithNonBstackA11y( + (caps.capabilities as WebdriverIO.Capabilities).browserName, + (caps.capabilities as WebdriverIO.Capabilities).browserVersion + )) { + if (capType === 'goog:chromeOptions' && value) { + const chromeOptions = (caps.capabilities as WebdriverIO.Capabilities)['goog:chromeOptions'] as unknown as Capabilities.ChromeOptions + if (chromeOptions) { + const finalChromeOptions = mergeChromeOptions(chromeOptions, value); + (caps.capabilities as WebdriverIO.Capabilities)['goog:chromeOptions'] = finalChromeOptions + } else { + (caps.capabilities as WebdriverIO.Capabilities)['goog:chromeOptions'] = value + } + return + } + } if (!(caps.capabilities as WebdriverIO.Capabilities)['bstack:options']) { const extensionCaps = Object.keys(caps.capabilities).filter((cap) => cap.includes(':')) if (extensionCaps.length) { diff --git a/packages/wdio-browserstack-service/src/scripts/accessibility-scripts.ts b/packages/wdio-browserstack-service/src/scripts/accessibility-scripts.ts index f8c6e4e1600..95cdf4561f8 100644 --- a/packages/wdio-browserstack-service/src/scripts/accessibility-scripts.ts +++ b/packages/wdio-browserstack-service/src/scripts/accessibility-scripts.ts @@ -10,6 +10,7 @@ class AccessibilityScripts { public getResultsSummary: string | null = null public saveTestResults: string | null = null public commandsToWrap: Array | null = null + public ChromeExtension: { [key: string]: unknown } = {} public browserstackFolderPath = '' public commandsPath = '' @@ -64,7 +65,7 @@ class AccessibilityScripts { } } - public update(data: { commands: any[], scripts: Record }) { + public update(data: { commands: any[], scripts: Record, nonBStackInfraA11yChromeOptions: {} }) { if (data.scripts) { this.performScan = data.scripts.scan this.getResults = data.scripts.getResults @@ -74,6 +75,9 @@ class AccessibilityScripts { if (data.commands && data.commands.length) { this.commandsToWrap = data.commands } + if (data.nonBStackInfraA11yChromeOptions){ + this.ChromeExtension = data.nonBStackInfraA11yChromeOptions + } } public store() { @@ -88,7 +92,8 @@ class AccessibilityScripts { getResults: this.getResults, getResultsSummary: this.getResultsSummary, saveResults: this.saveTestResults, - } + }, + nonBStackInfraA11yChromeOptions: this.ChromeExtension, })) } } diff --git a/packages/wdio-browserstack-service/src/service.ts b/packages/wdio-browserstack-service/src/service.ts index 7dfe1a16b13..3d061e71484 100644 --- a/packages/wdio-browserstack-service/src/service.ts +++ b/packages/wdio-browserstack-service/src/service.ts @@ -146,21 +146,23 @@ export default class BrowserstackService implements Services.ServiceInstance { if (this._browser) { try { const sessionId = this._browser.sessionId - if (isBrowserstackSession(this._browser)) { - try { - this._accessibilityHandler = new AccessibilityHandler( - this._browser, - this._caps, - this._isAppAutomate(), - this._config.framework, - this._accessibility, - this._options.accessibilityOptions - ) - await this._accessibilityHandler.before(sessionId) - Listener.setAccessibilityOptions(this._options.accessibilityOptions) - } catch (err) { - BStackLogger.error(`[Accessibility Test Run] Error in service class before function: ${err}`) - } + + try { + this._accessibilityHandler = new AccessibilityHandler( + this._browser, + this._caps, + this._options, + this._isAppAutomate(), + this._config, + this._config.framework, + this._accessibility, + this._turboScale, + this._options.accessibilityOptions + ) + await this._accessibilityHandler.before(sessionId) + Listener.setAccessibilityOptions(this._options.accessibilityOptions) + } catch (err) { + BStackLogger.error(`[Accessibility Test Run] Error in service class before function: ${err}`) } if (shouldProcessEventForTesthub('')) { diff --git a/packages/wdio-browserstack-service/src/util.ts b/packages/wdio-browserstack-service/src/util.ts index 85702e91168..d23f6c9fa63 100644 --- a/packages/wdio-browserstack-service/src/util.ts +++ b/packages/wdio-browserstack-service/src/util.ts @@ -320,9 +320,11 @@ export const processAccessibilityResponse = (response: LaunchResponse) => { if (response.accessibility.options) { const { accessibilityToken, pollingTimeout, scannerVersion } = jsonifyAccessibilityArray(response.accessibility.options.capabilities, 'name', 'value') + const result = jsonifyAccessibilityArray(response.accessibility.options.capabilities, 'name', 'value') const scriptsJson = { 'scripts': jsonifyAccessibilityArray(response.accessibility.options.scripts, 'name', 'command'), - 'commands': response.accessibility.options.commandsToWrap.commands + 'commands': response.accessibility.options.commandsToWrap.commands, + 'nonBStackInfraA11yChromeOptions': result['goog:chromeOptions'] } if (scannerVersion) { process.env.BSTACK_A11Y_SCANNER_VERSION = scannerVersion @@ -389,6 +391,11 @@ export const launchTestSession = PerformanceTester.measureWrapper(PERFORMANCE_SD config: {} } + if (accessibilityAutomation && (isTurboScale(options) || data.browserstackAutomation === false)){ + data.accessibility.settings ??= {} + data.accessibility.settings.includeEncodedExtension = true + } + try { if (Object.keys(CrashReporter.userConfigForReporting).length === 0) { CrashReporter.userConfigForReporting = process.env.USER_CONFIG_FOR_REPORTING !== undefined ? JSON.parse(process.env.USER_CONFIG_FOR_REPORTING) : {} @@ -406,6 +413,7 @@ export const launchTestSession = PerformanceTester.measureWrapper(PERFORMANCE_SD password: getObservabilityKey(options, config), json: data }).json() + delete data?.accessibility?.settings?.includeEncodedExtension BStackLogger.debug(`[Start_Build] Success response: ${JSON.stringify(response)}`) process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true' if (response.jwt) { @@ -474,6 +482,20 @@ export const validateCapsWithA11y = (deviceName?: any, platformMeta?: { [key: st return false } +export const validateCapsWithNonBstackA11y = (browserName?: string | undefined, browserVersion?:string | undefined ) => { + + if (browserName?.toLowerCase() !== 'chrome') { + BStackLogger.warn('Accessibility Automation will run only on Chrome browsers.') + return false + } + if ( !isUndefined(browserVersion) && !(browserVersion === 'latest' || parseFloat(browserVersion + '') > 100)) { + BStackLogger.warn('Accessibility Automation will run only on Chrome browser version greater than 100.') + return false + } + return true + +} + export const shouldScanTestForAccessibility = (suiteTitle: string | undefined, testTitle: string, accessibilityOptions?: { [key: string]: any; }, world?: { [key: string]: any; }, isCucumber?: boolean ) => { try { const includeTags = Array.isArray(accessibilityOptions?.includeTagsInTestingScope) ? accessibilityOptions?.includeTagsInTestingScope : [] @@ -538,10 +560,6 @@ export const _getParamsForAppAccessibility = ( commandName?: string ): { thTestR export const performA11yScan = async (isAppAutomate: boolean, browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, isBrowserStackSession?: boolean, isAccessibility?: boolean | string, commandName?: string) : Promise<{ [key: string]: any; } | undefined> => { return await PerformanceTester.measureWrapper(PERFORMANCE_SDK_EVENTS.A11Y_EVENTS.PERFORM_SCAN, async () => { - if (!isBrowserStackSession) { - BStackLogger.warn('Not a BrowserStack Automate session, cannot perform Accessibility scan.') - return // since we are running only on Automate as of now - } if (!isAccessibilityAutomationSession(isAccessibility)) { BStackLogger.warn('Not an Accessibility Automation session, cannot perform Accessibility scan.') @@ -565,10 +583,6 @@ export const performA11yScan = async (isAppAutomate: boolean, browser: Webdriver } export const getA11yResults = PerformanceTester.measureWrapper(PERFORMANCE_SDK_EVENTS.A11Y_EVENTS.GET_RESULTS, async (isAppAutomate: boolean, browser: WebdriverIO.Browser, isBrowserStackSession?: boolean, isAccessibility?: boolean | string) : Promise> => { - if (!isBrowserStackSession) { - BStackLogger.warn('Not a BrowserStack Automate session, cannot retrieve Accessibility results.') - return [] // since we are running only on Automate as of now - } if (!isAccessibilityAutomationSession(isAccessibility)) { BStackLogger.warn('Not an Accessibility Automation session, cannot retrieve Accessibility results.') @@ -644,9 +658,6 @@ const getAppA11yResultResponse = async (apiUrl: string, isAppAutomate: boolean, } export const getA11yResultsSummary = PerformanceTester.measureWrapper(PERFORMANCE_SDK_EVENTS.A11Y_EVENTS.GET_RESULTS_SUMMARY, async (isAppAutomate: boolean, browser: WebdriverIO.Browser, isBrowserStackSession?: boolean, isAccessibility?: boolean | string) : Promise<{ [key: string]: any; }> => { - if (!isBrowserStackSession) { - return {} // since we are running only on Automate as of now - } if (!isAccessibilityAutomationSession(isAccessibility)) { BStackLogger.warn('Not an Accessibility Automation session, cannot retrieve Accessibility results summary.') @@ -1573,4 +1584,42 @@ export function getBooleanValueFromString(value: string | undefined): boolean { } return ['true'].includes(value.trim().toLowerCase()) } +export function mergeDeep(target: Record, ...sources: any[]): Record { + if (!sources.length) {return target} + const source = sources.shift() + + if (isObject(target) && isObject(source)) { + for (const key in source) { + const sourceValue = source[key] + const targetValue = target[key] + if (isObject(sourceValue)) { + if (!targetValue || !isObject(targetValue)) { + target[key] = {} + } + mergeDeep(target[key], sourceValue) + } else { + target[key] = sourceValue + } + } + } + + return mergeDeep(target, ...sources) +} + +export function mergeChromeOptions(base: Capabilities.ChromeOptions, override: Partial): Capabilities.ChromeOptions { + const merged: Capabilities.ChromeOptions = { ...base } + + if (override.args) { + merged.args = [...(base.args || []), ...override.args] + } + + if (override.extensions) { + merged.extensions = [...(base.extensions || []), ...override.extensions] + } + + if (override.prefs) { + merged.prefs = mergeDeep({ ...(base.prefs || {}) }, override.prefs) + } + return merged +} diff --git a/packages/wdio-browserstack-service/tests/accessibility-handler.test.ts b/packages/wdio-browserstack-service/tests/accessibility-handler.test.ts index 911e1d70047..db21ea94c39 100644 --- a/packages/wdio-browserstack-service/tests/accessibility-handler.test.ts +++ b/packages/wdio-browserstack-service/tests/accessibility-handler.test.ts @@ -8,12 +8,15 @@ import logger from '@wdio/logger' import AccessibilityHandler from '../src/accessibility-handler.js' import * as utils from '../src/util.js' -import type { Capabilities } from '@wdio/types' +import type { Capabilities, Options } from '@wdio/types' import * as bstackLogger from '../src/bstackLogger.js' +import type { BrowserstackConfig, BrowserstackOptions } from '../src/types.js' const log = logger('test') let accessibilityHandler: AccessibilityHandler let browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser +let options: BrowserstackConfig & BrowserstackOptions +let config : Options.Testrunner let caps: Capabilities.RemoteCapability let accessibilityOpts: { [key: string]: any; } @@ -72,7 +75,12 @@ beforeEach(() => { osVersion: 'Catalina', accessibility: true } } as Capabilities.RemoteCapability - accessibilityHandler = new AccessibilityHandler(browser, caps, false, 'framework', true) + + options = { + accessibility: true + } as BrowserstackConfig & BrowserstackOptions + config = {} as Options.Testrunner + accessibilityHandler = new AccessibilityHandler(browser, caps, options, false, config, 'framework', true) }) it('should initialize correctly', () => { @@ -83,7 +91,7 @@ it('should initialize correctly', () => { needsReview: true } } - accessibilityHandler = new AccessibilityHandler(browser, caps, false, 'framework', true, accessibilityOpts) + accessibilityHandler = new AccessibilityHandler(browser, caps, options, false, config, 'framework', true) expect(accessibilityHandler['_platformA11yMeta']).toEqual({ browser_name: 'chrome', browser_version: 'latest', os_name: 'OS X', os_version: 'Catalina' }) expect(accessibilityHandler['_accessibility']).toEqual(true) expect(accessibilityHandler['_caps']).toEqual(caps) @@ -94,39 +102,37 @@ describe('before', () => { // let _getCapabilityValueSpy const isBrowserstackSessionSpy = vi.spyOn(utils, 'isBrowserstackSession') const getA11yResultsSummarySpy = vi.spyOn(utils, 'getA11yResultsSummary') + const shouldAddServiceVersionSpy = vi.spyOn(utils, 'shouldAddServiceVersion') const getA11yResultsSpy = vi.spyOn(utils, 'getA11yResults') const isAccessibilityAutomationSessionSpy = vi.spyOn(utils, 'isAccessibilityAutomationSession') beforeEach(() => { - accessibilityHandler = new AccessibilityHandler(browser, caps, false, 'framework', true, accessibilityOpts) + accessibilityHandler = new AccessibilityHandler(browser, caps, options, false, config, 'framework', true, false, accessibilityOpts) getA11yResultsSpy.mockClear() isBrowserstackSessionSpy.mockClear() getA11yResultsSummarySpy.mockClear() isAccessibilityAutomationSessionSpy.mockClear() }) - it('calls isBrowserstackSession', async () => { - isBrowserstackSessionSpy.mockReturnValue(true) - await accessibilityHandler.before('session123') - expect(isBrowserstackSessionSpy).toBeCalledTimes(1) - }) - - it('isBrowserstackSession returns true', async () => { + it('calls isAccessibilityAutomationSession', async () => { isBrowserstackSessionSpy.mockReturnValue(true) await accessibilityHandler.before('session123') - expect(isBrowserstackSessionSpy).toBeCalledTimes(1) + expect(isAccessibilityAutomationSessionSpy).toBeCalledTimes(2) }) - it('calls isAccessibilityAutomationSession', async () => { - isBrowserstackSessionSpy.mockReturnValue(true) + it('calls validateCapsWithNonBstackA11y', async () => { + const validateCapsWithNonBstackA11ySpy = vi.spyOn(utils, 'validateCapsWithNonBstackA11y') + shouldAddServiceVersionSpy.mockReturnValue(false) + isAccessibilityAutomationSessionSpy.mockReturnValue(true) await accessibilityHandler.before('session123') - expect(isAccessibilityAutomationSessionSpy).toBeCalledTimes(1) + expect(validateCapsWithNonBstackA11ySpy).toBeCalledTimes(1) }) it('calls validateCapsWithA11y', async () => { const _getCapabilityValueSpy = vi.spyOn(accessibilityHandler, '_getCapabilityValue').mockReturnValue(true) const validateCapsWithA11ySpy = vi.spyOn(utils, 'validateCapsWithA11y') isBrowserstackSessionSpy.mockReturnValue(true) + shouldAddServiceVersionSpy.mockReturnValue(true) isAccessibilityAutomationSessionSpy.mockReturnValue(true) await accessibilityHandler.before('session123') expect(_getCapabilityValueSpy).toBeCalledTimes(3) @@ -155,7 +161,7 @@ describe('beforeScenario', () => { let executeSpy: any beforeEach(() => { - accessibilityHandler = new AccessibilityHandler(browser, caps, false, 'framework', true, accessibilityOpts) + accessibilityHandler = new AccessibilityHandler(browser, caps, options, false, config, 'framework', true, false, accessibilityOpts) executeAsyncSpy = vi.spyOn((browser as WebdriverIO.Browser), 'executeAsync') executeSpy = vi.spyOn((browser as WebdriverIO.Browser), 'execute') vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true) @@ -287,7 +293,7 @@ describe('afterScenario', () => { let accessibilityHandler: AccessibilityHandler beforeEach(() => { - accessibilityHandler = new AccessibilityHandler(browser, caps, false, 'framework', true, accessibilityOpts) + accessibilityHandler = new AccessibilityHandler(browser, caps, options, false, config, 'framework', true, false, accessibilityOpts) executeAsyncSpy = vi.spyOn((browser as WebdriverIO.Browser), 'executeAsync') vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true) vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true) @@ -391,7 +397,7 @@ describe('beforeTest', () => { describe('mocha', () => { beforeEach(() => { - accessibilityHandler = new AccessibilityHandler(browser, caps, false, 'mocha', true, accessibilityOpts) + accessibilityHandler = new AccessibilityHandler(browser, caps, options, false, config, 'mocha', true, false, accessibilityOpts) vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true) vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true) vi.spyOn(utils, 'getUniqueIdentifier').mockReturnValue('test title') @@ -451,7 +457,7 @@ describe('beforeTest', () => { describe('jasmine', () => { let isBrowserstackSession: any beforeEach(() => { - accessibilityHandler = new AccessibilityHandler(browser, caps, false, 'jasmine', true, accessibilityOpts) + accessibilityHandler = new AccessibilityHandler(browser, caps, options, false, config, 'jasmine', true, false, accessibilityOpts) isBrowserstackSession = vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true) }) @@ -470,7 +476,7 @@ describe('afterTest', () => { let accessibilityHandler: AccessibilityHandler beforeEach(() => { - accessibilityHandler = new AccessibilityHandler(browser, caps, false, 'mocha', true, accessibilityOpts) + accessibilityHandler = new AccessibilityHandler(browser, caps, options, false, config, 'mocha', true, false, accessibilityOpts) executeAsyncSpy = vi.spyOn((browser as WebdriverIO.Browser), 'executeAsync') vi.spyOn(utils, 'isBrowserstackSession').mockReturnValue(true) vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true) @@ -523,7 +529,7 @@ describe('getIdentifier', () => { let getUniqueIdentifierForCucumberSpy: any beforeEach(() => { - accessibilityHandler = new AccessibilityHandler(browser, caps, false, 'framework', true, accessibilityOpts) + accessibilityHandler = new AccessibilityHandler(browser, caps, options, false, config, 'framework', true, false, accessibilityOpts) getUniqueIdentifierSpy = vi.spyOn(utils, 'getUniqueIdentifier') getUniqueIdentifierForCucumberSpy = vi.spyOn(utils, 'getUniqueIdentifierForCucumber') diff --git a/packages/wdio-browserstack-service/tests/accessibility-scripts.test.ts b/packages/wdio-browserstack-service/tests/accessibility-scripts.test.ts index 8f5951851ca..609d4b07242 100644 --- a/packages/wdio-browserstack-service/tests/accessibility-scripts.test.ts +++ b/packages/wdio-browserstack-service/tests/accessibility-scripts.test.ts @@ -6,7 +6,7 @@ import AccessibilityScripts from '../src/scripts/accessibility-scripts.js' vi.mock('node:fs', () => ({ default: { - readFileSync: vi.fn().mockReturnValue('{"scripts": {"scan": "scan", "getResults": "getResults", "getResultsSummary": "getResultsSummary", "saveResults": "saveResults"}, "commands": [{"command": "command1"}, {"command": "command2"}]}'), + readFileSync: vi.fn().mockReturnValue('{"scripts": {"scan": "scan", "getResults": "getResults", "getResultsSummary": "getResultsSummary", "saveResults": "saveResults"}, "commands": [{"command": "command1"}, {"command": "command2"}], "nonBStackInfraA11yChromeOptions": {"extension": ["extension1"]}}'), writeFileSync: vi.fn(), existsSync: vi.fn().mockReturnValue(true), mkdirSync: vi.fn(), @@ -34,6 +34,7 @@ describe('AccessibilityScripts', () => { expect(accessibilityScripts.getResultsSummary).to.equal('getResultsSummary') expect(accessibilityScripts.saveTestResults).to.equal('saveResults') expect(accessibilityScripts.commandsToWrap).to.deep.equal([{ command: 'command1' }, { command: 'command2' }]) + expect(accessibilityScripts.ChromeExtension).to.deep.equal({ extension: ['extension1'] }) }) it('should update data', () => { @@ -45,15 +46,17 @@ describe('AccessibilityScripts', () => { getResultsSummary: 'getResultsSummary', saveResults: 'saveResults', }, + nonBStackInfraA11yChromeOptions: { extension: ['extension1'] } } as unknown - accessibilityScripts.update(data as { commands: [any]; scripts: { scan: null; getResults: null; getResultsSummary: null; saveResults: null } }) + accessibilityScripts.update(data as { commands: [any]; scripts: { scan: null; getResults: null; getResultsSummary: null; saveResults: null }; nonBStackInfraA11yChromeOptions:{} }) expect(accessibilityScripts.performScan).to.equal('scan') expect(accessibilityScripts.getResults).to.equal('getResults') expect(accessibilityScripts.getResultsSummary).to.equal('getResultsSummary') expect(accessibilityScripts.saveTestResults).to.equal('saveResults') expect(accessibilityScripts.commandsToWrap).to.deep.equal([{ command: 'command1' }, { command: 'command2' }]) + expect(accessibilityScripts.ChromeExtension).to.deep.equal({ extension: ['extension1'] }) }) it('should store data to file', () => { @@ -63,6 +66,7 @@ describe('AccessibilityScripts', () => { accessibilityScripts.getResultsSummary = 'getResultsSummary' accessibilityScripts.saveTestResults = 'saveResults' accessibilityScripts.commandsToWrap = [{ command: 'command1' }, { command: 'command2' }] + accessibilityScripts.ChromeExtension = { extension: ['extension1'] } const writeFileSyncStub = vi.spyOn(fs, 'writeFileSync') accessibilityScripts.store() @@ -76,7 +80,8 @@ describe('AccessibilityScripts', () => { getResults: accessibilityScripts.getResults, getResultsSummary: accessibilityScripts.getResultsSummary, saveResults: accessibilityScripts.saveTestResults, - } + }, + nonBStackInfraA11yChromeOptions: accessibilityScripts.ChromeExtension, }) ) }) diff --git a/packages/wdio-browserstack-service/tests/app-accessibility-handler.test.ts b/packages/wdio-browserstack-service/tests/app-accessibility-handler.test.ts index 5aaaac0c106..a1ff54de14e 100644 --- a/packages/wdio-browserstack-service/tests/app-accessibility-handler.test.ts +++ b/packages/wdio-browserstack-service/tests/app-accessibility-handler.test.ts @@ -5,13 +5,16 @@ import logger from '@wdio/logger' import AccessibilityHandler from '../src/accessibility-handler.js' import * as utils from '../src/util.js' -import type { Capabilities } from '@wdio/types' +import type { Capabilities, Options } from '@wdio/types' import * as bstackLogger from '../src/bstackLogger.js' +import type { BrowserstackConfig, BrowserstackOptions } from '../src/types.js' const log = logger('test') let accessibilityHandler: AccessibilityHandler let browser: WebdriverIO.Browser let caps: Capabilities.RemoteCapability +let options: BrowserstackConfig & BrowserstackOptions +let config : Options.Testrunner let accessibilityOpts: { [key: string]: any } vi.mock('got') @@ -57,8 +60,13 @@ describe('App Automate Accessibility Handler', () => { needsReview: true } } + options = { + accessibility: true + } as BrowserstackConfig & BrowserstackOptions - accessibilityHandler = new AccessibilityHandler(browser, caps, true, 'mocha', true, accessibilityOpts) + config = {} as Options.Testrunner + + accessibilityHandler = new AccessibilityHandler(browser, caps, options, true, config, 'mocha', true, false, accessibilityOpts) }) describe('initialization', () => { @@ -93,7 +101,7 @@ describe('App Automate Accessibility Handler', () => { await accessibilityHandler.before('app123') - expect(isBrowserstackSessionSpy).toBeCalledTimes(1) + expect(isBrowserstackSessionSpy).toBeCalledTimes(0) expect(isAppAccessibilityAutomationSessionSpy).toBeCalledTimes(1) expect(validateCapsWithAppA11ySpy).toBeCalledTimes(1) }) diff --git a/packages/wdio-browserstack-service/tests/launcher.test.ts b/packages/wdio-browserstack-service/tests/launcher.test.ts index cf37915675b..869bd818d92 100644 --- a/packages/wdio-browserstack-service/tests/launcher.test.ts +++ b/packages/wdio-browserstack-service/tests/launcher.test.ts @@ -948,6 +948,32 @@ describe('_updateObjectTypeCaps', () => { expect(caps.chromeBrowser.capabilities).toEqual({ 'browserstack.wdioService': pkg.version, 'browserstack.accessibilityOptions': { includeIssueType: { bestPractice: true, needsReview: true } } }) }) + it('should set chromeOptions if capType is goog:chromeOptions and no existing options are present', () => { + const value = { args: ['--disable-gpu'] } + vi.spyOn(utils, 'validateCapsWithNonBstackA11y').mockImplementation(() => true) + const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) + service._updateObjectTypeCaps(caps, 'goog:chromeOptions', value) + expect(caps[0]['goog:chromeOptions']).toEqual(value) + }) + + it('should merge chromeOptions if capType is goog:chromeOptions and value is provided', () => { + const caps: any = [{ 'goog:chromeOptions': { args: ['--headless'] } }] + const value = { args: ['--disable-gpu'] } + vi.spyOn(utils, 'validateCapsWithNonBstackA11y').mockImplementation(() => true) + const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) + service._updateObjectTypeCaps(caps, 'goog:chromeOptions', value) + expect(caps[0]['goog:chromeOptions']).toEqual({ args: ['--headless', '--disable-gpu'] }) + }) + + it('should update goog:chromeOptions in caps object if value is provided', () => { + const caps = { chromeBrowser: { capabilities: { 'goog:chromeOptions': { args: ['--headless'] }, 'bstack:options': {} } } } + const value = { args: ['--disable-gpu'] } + vi.spyOn(utils, 'validateCapsWithNonBstackA11y').mockImplementation(() => true) + const service = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config) + service._updateObjectTypeCaps(caps, 'goog:chromeOptions', value) + expect(caps.chromeBrowser.capabilities['goog:chromeOptions']).toEqual({ args: ['--headless', '--disable-gpu'] }) + }) + it('should delete accessibilityOptions in caps array if value not passed in _updateObjectTypeCaps', () => { const caps = [{ 'bstack:options': { accessibilityOptions: { wcagVersion: 'wcag2a' } } }] const service = new BrowserstackLauncher(options as any, caps as any, config) diff --git a/packages/wdio-browserstack-service/tests/util.test.ts b/packages/wdio-browserstack-service/tests/util.test.ts index 9bba2fa60b8..981b8fa84fe 100644 --- a/packages/wdio-browserstack-service/tests/util.test.ts +++ b/packages/wdio-browserstack-service/tests/util.test.ts @@ -37,6 +37,7 @@ import { getFailureObject, validateCapsWithAppA11y, validateCapsWithA11y, + validateCapsWithNonBstackA11y, shouldScanTestForAccessibility, isAccessibilityAutomationSession, isAppAccessibilityAutomationSession, @@ -53,6 +54,8 @@ import { 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' @@ -725,6 +728,7 @@ describe('launchTestSession', () => { expect(got.post).toBeCalledTimes(1) expect(result).toEqual(mockResponse) }) + }) describe('getLogTag', () => { @@ -1039,6 +1043,41 @@ describe('validateCapsWithA11y', () => { }) }) +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: { @@ -1118,11 +1157,6 @@ describe('getA11yResults', () => { on: vi.fn(), } as any as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser - it('return false if BrowserStack Session', async () => { - const result: any = await utils.getA11yResults((browser as WebdriverIO.Browser), false, false) - expect(result).toEqual([]) - }) - 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) @@ -1167,11 +1201,6 @@ describe('getA11yResultsSummary', () => { on: vi.fn(), } as any as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser - it('return false if BrowserStack Session', async () => { - const result: any = await utils.getA11yResultsSummary((browser as WebdriverIO.Browser), false, false) - expect(result).toEqual({}) - }) - 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) @@ -1643,18 +1672,6 @@ describe('performA11yScan', () => { logInfoMock = vi.spyOn(log, 'warn') }) - it('should return early if not a BrowserStack session', async () => { - browser = { - execute: async () => ({ success: true }), - executeAsync: async () => ({ success: true }), - } as unknown as WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser - - const result = await performA11yScan(false, browser, false, true) - expect(result).toBeUndefined() - expect(logInfoMock.mock.calls[0][0]) - .toContain('Not a BrowserStack Automate session, cannot perform Accessibility scan.') - }) - it('should return early if not an Accessibility Automation session', async () => { browser = { execute: async () => ({ success: true }), @@ -1920,3 +1937,53 @@ describe('getAppA11yResultsSummary', () => { 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' + }) + }) +}) \ No newline at end of file