Skip to content
Open
32 changes: 29 additions & 3 deletions packages/wdio-browserstack-service/src/accessibility-handler.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -19,6 +21,8 @@ import {
o11yClassErrorHandler,
shouldScanTestForAccessibility,
validateCapsWithA11y,
shouldAddServiceVersion,
validateCapsWithNonBstackA11y,
isTrue,
validateCapsWithAppA11y,
getAppA11yResults,
Expand All @@ -35,6 +39,9 @@ class _AccessibilityHandler {
private _caps: Capabilities.ResolvedTestrunnerCapabilities
private _suiteFile?: string
private _accessibility?: boolean
private _turboscale?: boolean
private _options: BrowserstackConfig & BrowserstackOptions
private _config: Options.Testrunner
private _accessibilityOptions?: { [key: string]: unknown; }
private _testMetadata: { [key: string]: unknown; } = {}
private static _a11yScanSessionMap: { [key: string]: unknown; } = {}
Expand All @@ -44,9 +51,12 @@ class _AccessibilityHandler {
constructor (
private _browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser,
_capabilities: Capabilities.ResolvedTestrunnerCapabilities,
_options : BrowserstackConfig & BrowserstackOptions,
private isAppAutomate: boolean,
_config : Options.Testrunner,
private _framework?: string,
_accessibilityAutomation?: boolean | string,
_turboscale?: boolean | string,
_accessibilityOpts?: { [key: string]: unknown; }
) {
const caps = (this._browser as WebdriverIO.Browser).capabilities as WebdriverIO.Capabilities
Expand All @@ -64,6 +74,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) {
Expand Down Expand Up @@ -103,7 +116,20 @@ 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)
) &&
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', '') as Capabilities.ChromeOptions
Expand Down Expand Up @@ -356,7 +382,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) {
Expand Down
42 changes: 40 additions & 2 deletions packages/wdio-browserstack-service/src/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
ObjectsAreEqual, getBasicAuthHeader,
isValidCapsForHealing,
getBooleanValueFromString,
validateCapsWithNonBstackA11y,
mergeChromeOptions
} from './util.js'
import CrashReporter from './crash-reporter.js'
import { BStackLogger } from './bstackLogger.js'
Expand All @@ -50,7 +52,7 @@ import { sendFinish, sendStart } from './instrumentation/funnelInstrumentation.j
import AiHandler from './ai-handler.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'
import { _fetch as fetch } from './fetchWrapper.js'

type BrowserstackLocal = BrowserstackLocalLauncher.Local & {
Expand Down Expand Up @@ -295,6 +297,13 @@ 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<Capabilities.ChromeOptions> = accessibilityScripts.ChromeExtension
this._updateObjectTypeCaps(capabilities, 'goog:chromeOptions', overrideOptions)
}

if (buildStartResponse?.accessibility) {
if (this._accessibilityAutomation === null) {
this.browserStackConfig.accessibility = buildStartResponse.accessibility.success as boolean
Expand Down Expand Up @@ -573,7 +582,7 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
}
}

_updateObjectTypeCaps(capabilities?: Capabilities.TestrunnerCapabilities, capType?: string, value?: { [key: string]: unknown }) {
_updateObjectTypeCaps(capabilities?: Capabilities.TestrunnerCapabilities | WebdriverIO.Capabilities, capType?: string, value?: { [key: string]: unknown }) {
try {
if (Array.isArray(capabilities)) {
capabilities
Expand All @@ -588,6 +597,19 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
return c as WebdriverIO.Capabilities
})
.forEach((capability: WebdriverIO.Capabilities) => {
if (
validateCapsWithNonBstackA11y(capability.browserName, capability.browserVersion) &&
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) {
Expand Down Expand Up @@ -622,6 +644,22 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
})
} else if (typeof capabilities === 'object') {
Object.entries(capabilities as Capabilities.RequestedMultiremoteCapabilities).forEach(([, caps]) => {
if (
validateCapsWithNonBstackA11y(
(caps.capabilities as WebdriverIO.Capabilities).browserName,
(caps.capabilities as WebdriverIO.Capabilities).browserVersion
) &&
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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class AccessibilityScripts {
public getResultsSummary: string | null = null
public saveTestResults: string | null = null
public commandsToWrap: Array<Command> | null = null
public ChromeExtension: { [key: string]: unknown } = {}

public browserstackFolderPath = ''
public commandsPath = ''
Expand Down Expand Up @@ -77,7 +78,7 @@ class AccessibilityScripts {
}
}

public update(data: { commands: [], scripts: Scripts }) {
public update(data: { commands: [], scripts: Scripts, nonBStackInfraA11yChromeOptions: {} }) {
if (data.scripts) {
this.performScan = data.scripts.scan
this.getResults = data.scripts.getResults
Expand All @@ -87,6 +88,10 @@ class AccessibilityScripts {
if (data.commands && data.commands.length) {
this.commandsToWrap = data.commands
}
if (data.nonBStackInfraA11yChromeOptions){
this.ChromeExtension = data.nonBStackInfraA11yChromeOptions
}

}

public store() {
Expand All @@ -101,7 +106,8 @@ class AccessibilityScripts {
getResults: this.getResults,
getResultsSummary: this.getResultsSummary,
saveResults: this.saveTestResults,
}
},
nonBStackInfraA11yChromeOptions: this.ChromeExtension,
}))
}
}
Expand Down
31 changes: 16 additions & 15 deletions packages/wdio-browserstack-service/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,22 +149,23 @@ export default class BrowserstackService implements Services.ServiceInstance {
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)
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}`)
}
Listener.setAccessibilityOptions(this._options.accessibilityOptions)
} catch (err) {
BStackLogger.error(`[Accessibility Test Run] Error in service class before function: ${err}`)
}

if (shouldProcessEventForTesthub('')) {
Expand Down
76 changes: 63 additions & 13 deletions packages/wdio-browserstack-service/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import * as PERFORMANCE_SDK_EVENTS from './instrumentation/performance/constants
import { logBuildError, handleErrorForObservability, handleErrorForAccessibility, getProductMapForBuildStartCall } from './testHub/utils.js'
import type BrowserStackConfig from './config.js'
import type { Errors } from './testHub/utils.js'

import type { UserConfig, UploadType, BrowserstackConfig, BrowserstackOptions, LaunchResponse } from './types.js'
import type { ITestCaseHookParameter } from './cucumber-types.js'
import {
Expand Down Expand Up @@ -328,9 +327,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 as string
Expand Down Expand Up @@ -398,6 +399,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) : {}
Expand All @@ -420,6 +426,7 @@ export const launchTestSession = PerformanceTester.measureWrapper(PERFORMANCE_SD
body: JSON.stringify(data)
})
const jsonResponse: LaunchResponse = await response.json()
delete data?.accessibility?.settings?.includeEncodedExtension
BStackLogger.debug(`[Start_Build] Success response: ${JSON.stringify(jsonResponse)}`)
process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true'
if (jsonResponse.jwt) {
Expand Down Expand Up @@ -486,6 +493,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]: string; }, world?: { [key: string]: unknown; }, isCucumber?: boolean ) => {
try {
const includeTags = Array.isArray(accessibilityOptions?.includeTagsInTestingScope) ? accessibilityOptions?.includeTagsInTestingScope : []
Expand Down Expand Up @@ -551,10 +572,6 @@ export const _getParamsForAppAccessibility = ( commandName?: string ): { thTestR

/* eslint-disable @typescript-eslint/no-explicit-any */
export const performA11yScan = async (isAppAutomate: boolean, browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, isBrowserStackSession?: boolean, isAccessibility?: boolean | string, commandName?: string) : Promise<{ [key: string]: any; } | undefined> => {
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.')
Expand All @@ -580,10 +597,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<Array<{ [key: string]: any; }>> => {
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.')
Expand Down Expand Up @@ -663,9 +676,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.')
Expand Down Expand Up @@ -1640,3 +1650,43 @@ export function getBooleanValueFromString(value: string | undefined): boolean {
return ['true'].includes(value.trim().toLowerCase())
}

export function mergeDeep(target: Record<string, any>, ...sources: any[]): Record<string, any> {
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>): 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
}

Loading
Loading