Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ See [CHANGELOG - v7](https://github.com/webdriverio/webdriverio/blob/v7/CHANGELO

---

## v8.48.0 (2025-11-29)

#### :bug: Bug Fix
* `wdio-browserstack-service`
* [#14844](https://github.com/webdriverio/webdriverio/pull/14844) Adding extra key for app accessibility feature to use centralHeader for authentication ([@pri-gadhiya](https://github.com/pri-gadhiya))

#### Committers: 1
- Priyanka Gadhiya ([@pri-gadhiya](https://github.com/pri-gadhiya))


## v8.47.0 (2025-09-09)

#### :rocket: New Feature
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"packages": [
"packages/*"
],
"version": "8.47.0",
"version": "8.48.0",
"command": {
"bootstrap": {
"npmClientArgs": [
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/wdio-browserstack-service/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@wdio/browserstack-service",
"version": "8.47.0",
"version": "8.48.0",
"description": "WebdriverIO service for better Browserstack integration",
"author": "Adam Bjerstedt <abjerstedt@gmail.com>",
"homepage": "https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-browserstack-service",
Expand Down
21 changes: 11 additions & 10 deletions packages/wdio-browserstack-service/src/accessibility-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ import {
isTrue,
validateCapsWithAppA11y,
getAppA11yResults,
isFalse
isFalse,
setBrowserstackAnnotation
} from './util.js'
import accessibilityScripts from './scripts/accessibility-scripts.js'
import PerformanceTester from './instrumentation/performance/performance-tester.js'
Expand Down Expand Up @@ -212,6 +213,10 @@ class _AccessibilityHandler {
}

browserWithA11y.startA11yScanning = async () => {
if (this._testIdentifier === null){
BStackLogger.warn('Accessibility scanning cannot be started from outside the test')
return
}
AccessibilityHandler._a11yScanSessionMap[sessionId] = true
this._testMetadata[this._testIdentifier as string] = {
scanTestForAccessibility : true,
Expand All @@ -221,6 +226,10 @@ class _AccessibilityHandler {
}

browserWithA11y.stopA11yScanning = async () => {
if (this._testIdentifier === null){
BStackLogger.warn('Accessibility scanning cannot be stopped from outside the test')
return
}
AccessibilityHandler._a11yScanSessionMap[sessionId] = false
await this._setAnnotation('Accessibility scanning has stopped')
}
Expand Down Expand Up @@ -462,15 +471,7 @@ class _AccessibilityHandler {
}

private async _setAnnotation(message: string) {
if (this._accessibility && isBrowserstackSession(this._browser)) {
await (this._browser as WebdriverIO.Browser).execute(`browserstack_executor: ${JSON.stringify({
action: 'annotate',
arguments: {
data: message,
level: 'info'
}
})}`)
}
await setBrowserstackAnnotation(this._browser as WebdriverIO.Browser, message, Boolean(this._accessibility))
Comment on lines 464 to +474

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we directly call setBrowserstackAnnotation function instead of calling _setAnnotation

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to keep the approach similar in both the files where it is used -> handler and module

}
}

Expand Down
3 changes: 2 additions & 1 deletion packages/wdio-browserstack-service/src/cli/cliUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ export class CLIUtils {
sdk_language: this.getSdkLanguage(),
}
if (!isNullOrEmpty(existingCliPath)) {
queryParams.cli_version = await this.runShellCommand(`${existingCliPath} version`)
const nullDevice = platform() === 'win32' ? 'NUL' : '/dev/null'
queryParams.cli_version = await this.runShellCommand(`${existingCliPath} version 2>${nullDevice}`)
}
const response = await this.requestToUpdateCLI(queryParams, config)
if (nestedKeyValue(response, ['updated_cli_version'])) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <reference path="../../@types/bstack-service-types.d.ts" />
import BaseModule from './baseModule.js'
import { BrowserstackCLI } from '../index.js'
import { BStackLogger } from '../cliLogger.js'
import TestFramework from '../frameworks/testFramework.js'
import AutomationFramework from '../frameworks/automationFramework.js'
Expand All @@ -9,7 +10,7 @@ import { TestFrameworkState } from '../states/testFrameworkState.js'
import { AutomationFrameworkState } from '../states/automationFrameworkState.js'
import { HookState } from '../states/hookState.js'
import accessibilityScripts from '../../scripts/accessibility-scripts.js'
import { _getParamsForAppAccessibility, formatString, getAppA11yResults, getAppA11yResultsSummary, shouldScanTestForAccessibility, validateCapsWithA11y, validateCapsWithAppA11y, validateCapsWithNonBstackA11y } from '../../util.js'
import { _getParamsForAppAccessibility, formatString, getAppA11yResults, getAppA11yResultsSummary, shouldScanTestForAccessibility, validateCapsWithA11y, validateCapsWithAppA11y, validateCapsWithNonBstackA11y, setBrowserstackAnnotation } from '../../util.js'
import { AutomationFrameworkConstants } from '../frameworks/constants/automationFrameworkConstants.js'
import util from 'node:util'
import type { Accessibility } from '@browserstack/wdio-browserstack-service'
Expand All @@ -25,12 +26,16 @@ export default class AccessibilityModule extends BaseModule {
name: string
scriptInstance: typeof accessibilityScripts
accessibility: boolean = false
autoScanning: boolean = true
isAppAccessibility: boolean
isNonBstackA11y: boolean
accessibilityConfig: Accessibility
static MODULE_NAME = 'AccessibilityModule'
accessibilityMap: Map<number, boolean>
LOG_DISABLED_SHOWN: Map<number, boolean>
testMetadata: Record<string, any> = {}
centralAuthA11yConfig: Record<string, unknown> = {}
centralAuthConfigFetched: boolean = false

constructor(accessibilityConfig: Accessibility, isNonBstackA11y: boolean) {
super()
Expand All @@ -39,7 +44,9 @@ export default class AccessibilityModule extends BaseModule {
AutomationFramework.registerObserver(AutomationFrameworkState.CREATE, HookState.POST, this.onBeforeExecute.bind(this))
TestFramework.registerObserver(TestFrameworkState.TEST, HookState.PRE, this.onBeforeTest.bind(this))
TestFramework.registerObserver(TestFrameworkState.TEST, HookState.POST, this.onAfterTest.bind(this))
this.accessibility = true
this.accessibility = Boolean(accessibilityConfig)
const accessibilityOptions = (BrowserstackCLI.getInstance().options as any)?.accessibilityOptions
this.autoScanning = accessibilityOptions?.autoScanning ?? true
this.scriptInstance = accessibilityScripts
this.accessibilityMap = new Map()
this.LOG_DISABLED_SHOWN = new Map()
Expand Down Expand Up @@ -103,9 +110,26 @@ export default class AccessibilityModule extends BaseModule {

//patching performScan
(browser as any).performScan = async () => {
if (!this.accessibility && !this.isAppAccessibility){
return
}
return await this.performScanCli(browser)
}

(browser as any).startA11yScanning = async () => {
if (!this.accessibility && !this.isAppAccessibility){
return
}
this.logger.warn('Accessibility scanning cannot be started from outside the test')
}

(browser as any).stopA11yScanning = async () => {
if (!this.accessibility && !this.isAppAccessibility){
return
}
this.logger.warn('Accessibility scanning cannot be stopped from outside the test')
}

if (!this.accessibility) {
this.logger.info('Accessibility automation is disabled for this session.')
return
Expand Down Expand Up @@ -169,6 +193,7 @@ export default class AccessibilityModule extends BaseModule {

async onBeforeTest(args: any) {
try {

this.logger.debug('Accessibility before test hook. Starting accessibility scan for this test case.')
const suiteTitle = args.suiteTitle || ''
const test = args.test || {}
Expand All @@ -178,18 +203,59 @@ export default class AccessibilityModule extends BaseModule {

const sessionId = AutomationFramework.getState(autoInstance, AutomationFrameworkConstants.KEY_FRAMEWORK_SESSION_ID)
const accessibilityOptions = this.config.accessibilityOptions
const shouldScanTest = shouldScanTestForAccessibility(suiteTitle, test.title, accessibilityOptions as { [key: string]: any } | undefined) && this.accessibility
const shouldScanTest = this.autoScanning && shouldScanTestForAccessibility(suiteTitle, test.title, accessibilityOptions as { [key: string]: any } | undefined) && this.accessibility

this.accessibilityMap.set(sessionId, shouldScanTest)
// Create test metadata similar to accessibility-handler
const testIdentifier = testInstance.getContext().getId()
const testMetadata = {
const testIdentifier = String(testInstance.getContext().getId())
this.testMetadata[testIdentifier] = {
scanTestForAccessibility: shouldScanTest,
accessibilityScanStarted: shouldScanTest
}

const browser = AutomationFramework.getDriver(autoInstance) as WebdriverIO.Browser

(browser as any).startA11yScanning = async () => {
if (!this.accessibility && !this.isAppAccessibility){
return
}
this.accessibilityMap.set(sessionId, true)
this.testMetadata[testIdentifier] = {
scanTestForAccessibility : true,
accessibilityScanStarted : true
}
TestFramework.setState(testInstance, `accessibility_metadata_${testIdentifier}`, this.testMetadata[testIdentifier])
await this._setAnnotation('Accessibility scanning has started')
}

(browser as any).stopA11yScanning = async () => {
if (!this.accessibility && !this.isAppAccessibility){
return
}
this.accessibilityMap.set(sessionId, false)
await this._setAnnotation('Accessibility scanning has stopped')
}

(browser as any).performScan = async () => {
if (!this.accessibility && !this.isAppAccessibility){
return
}
const results = await this.performScanCli(browser)
if (results){
const testIdentifier = String(testInstance.getContext().getId())
this.testMetadata[testIdentifier] = {
scanTestForAccessibility : true,
accessibilityScanStarted : true
}
TestFramework.setState(testInstance, `accessibility_metadata_${testIdentifier}`, this.testMetadata[testIdentifier])
await this._setAnnotation('Accessibility scanning was triggered manually')

}
return results
}

// Store test metadata in test instance
TestFramework.setState(testInstance, `accessibility_metadata_${testIdentifier}`, testMetadata)
this.accessibilityMap.set(sessionId, shouldScanTest)
TestFramework.setState(testInstance, `accessibility_metadata_${testIdentifier}`, this.testMetadata[testIdentifier])

// Log if accessibility scan is enabled for this test
if (shouldScanTest) {
Expand Down Expand Up @@ -219,7 +285,7 @@ export default class AccessibilityModule extends BaseModule {
}

// Get test metadata that was stored in onBeforeTest
const testIdentifier = testInstance.getContext().getId()
const testIdentifier = String(testInstance.getContext().getId())
const testMetadata = testInstance.getData(`accessibility_metadata_${testIdentifier}`)

if (!testMetadata) {
Expand All @@ -245,7 +311,7 @@ export default class AccessibilityModule extends BaseModule {
'thBuildUuid': process.env.BROWSERSTACK_TESTHUB_UUID,
'thJwtToken': process.env.BROWSERSTACK_TESTHUB_JWT
}
const driverExecuteParams = await this.getDriverExecuteParams()
const driverExecuteParams = await this.getDriverExecuteParams('saveResults')
dataForExtension = { ...dataForExtension, ...driverExecuteParams }

// final scan and saving the results
Expand Down Expand Up @@ -307,13 +373,26 @@ export default class AccessibilityModule extends BaseModule {
PERFORMANCE_SDK_EVENTS.A11Y_EVENTS.PERFORM_SCAN,
async () => {
try {
// Fetch central auth accessibility configuration
const centralAuthConfig = await this.fetchCentralAuthA11yConfig('scan')
if (!this.accessibility) {
this.logger.debug('Not an Accessibility Automation session.')
return
}
if (this.isAppAccessibility) {
// Get app accessibility params and merge with central auth config
const appAccessibilityParams = _getParamsForAppAccessibility(commandName)

// Merge with central auth config
const mergedParams: Record<string, any> = { ...appAccessibilityParams, ...centralAuthConfig }

// Use centralAuthToken if available
if (centralAuthConfig.centralAuthToken) {
// Set the auth header with the token value
mergedParams.centralAuthHeader = centralAuthConfig.centralAuthToken
}
const results: unknown = await (browser as WebdriverIO.Browser).execute(
formatString(this.scriptInstance.performScan, JSON.stringify(_getParamsForAppAccessibility(commandName))) as string,
formatString(this.scriptInstance.performScan, JSON.stringify(mergedParams)) as string,
{}
)
BStackLogger.debug(util.format(results as string))
Expand All @@ -335,14 +414,16 @@ export default class AccessibilityModule extends BaseModule {

private async sendTestStopEvent(browser: WebdriverIO.Browser, dataForExtension: any) {
try {
const autoInstance: AutomationFrameworkInstance = AutomationFramework.getTrackedInstance()
const sessionId = AutomationFramework.getState(autoInstance, AutomationFrameworkConstants.KEY_FRAMEWORK_SESSION_ID)
if (!this.accessibility) {
this.logger.debug('Not an Accessibility Automation session.')
return
}

this.logger.debug('Performing scan before saving results')
await this.performScanCli(browser)

if (sessionId && this.accessibilityMap.get(sessionId)) {
this.logger.debug('Performing scan before saving results')
await this.performScanCli(browser)
}
if (this.isAppAccessibility) {
return
}
Expand Down Expand Up @@ -399,10 +480,10 @@ export default class AccessibilityModule extends BaseModule {
)()
}

async getDriverExecuteParams() {
async getDriverExecuteParams(scriptName: string) {
const payload: Omit<FetchDriverExecuteParamsEventRequest, 'binSessionId'> = {
product: 'accessibility',
scriptName: 'saveResults'
scriptName: scriptName
}
const response: FetchDriverExecuteParamsEventResponse = await GrpcClient.getInstance().fetchDriverExecuteParamsEvent(payload)
if (response.success) {
Expand All @@ -412,4 +493,32 @@ export default class AccessibilityModule extends BaseModule {
return {}
}

public async _setAnnotation(message: string) {
const autoInstance: AutomationFrameworkInstance = AutomationFramework.getTrackedInstance()
const browser = AutomationFramework.getDriver(autoInstance) as WebdriverIO.Browser

await setBrowserstackAnnotation(browser, message, this.accessibility)
}

/**
* Fetch central auth accessibility configuration for the given script name.
* Returns cached config if already fetched, otherwise loads and caches it.
*
* @param scriptName - Name of the script to fetch config for
* @returns Configuration object, empty object if error occurs
*/
async fetchCentralAuthA11yConfig(scriptName: string): Promise<Record<string, unknown>> {
try {
if (this.centralAuthConfigFetched) {
return this.centralAuthA11yConfig
}
this.centralAuthA11yConfig = await this.getDriverExecuteParams(scriptName)
this.centralAuthConfigFetched = true
return this.centralAuthA11yConfig
} catch (error) {
this.logger.error(`fetchCentralAuthA11yConfig: Failed to fetch driver execute params for ${scriptName}: ${util.format(error)}`)
return {}
}
}

}
5 changes: 4 additions & 1 deletion packages/wdio-browserstack-service/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,11 +715,14 @@ export default class BrowserstackService implements Services.ServiceInstance {
suiteTitle,
test?.title
)
} else if (test && !test.fullName) {
} else if (test && !test.fullName && this._config.framework !== 'jasmine') {
// Mocha
const pre = this._options.sessionNamePrependTopLevelSuiteTitle ? `${suiteTitle} - ` : ''
const post = !this._options.sessionNameOmitTestTitle ? ` - ${test.title}` : ''
name = `${pre}${test.parent}${post}`
} else if (this._config.framework === 'jasmine') {
// Jasmine
name = (this._caps as any)?.['bstack:options']?.sessionName
}

if (!BrowserstackCLI.getInstance().isRunning()) {
Expand Down
Loading
Loading