Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 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 @@ -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; } = {}
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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', '')
Expand Down Expand Up @@ -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) {
Expand Down
38 changes: 38 additions & 0 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,
isValidCapsForHealing,
getBooleanValueFromString,
validateCapsWithNonBstackA11y,
mergeChromeOptions
} from './util.js'
import { getProductMap } from './testHub/utils.js'
import CrashReporter from './crash-reporter.js'
Expand All @@ -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
Expand Down Expand Up @@ -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<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 @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class AccessibilityScripts {
public getResultsSummary: string | null = null
public saveTestResults: string | null = null
public commandsToWrap: Array<any> | null = null
public ChromeExtension: { [key: string]: unknown } = {}

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

public update(data: { commands: any[], scripts: Record<string, any> }) {
public update(data: { commands: any[], scripts: Record<string, any>, nonBStackInfraA11yChromeOptions: {} }) {
if (data.scripts) {
this.performScan = data.scripts.scan
this.getResults = data.scripts.getResults
Expand All @@ -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() {
Expand All @@ -88,7 +92,8 @@ class AccessibilityScripts {
getResults: this.getResults,
getResultsSummary: this.getResultsSummary,
saveResults: this.saveTestResults,
}
},
nonBStackInfraA11yChromeOptions: this.ChromeExtension,
}))
}
}
Expand Down
32 changes: 17 additions & 15 deletions packages/wdio-browserstack-service/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('')) {
Expand Down
73 changes: 61 additions & 12 deletions packages/wdio-browserstack-service/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) : {}
Expand All @@ -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) {
Expand Down Expand Up @@ -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 : []
Expand Down Expand Up @@ -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.')
Expand All @@ -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<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 @@ -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.')
Expand Down Expand Up @@ -1573,4 +1584,42 @@ 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