From f2ac7bffae77e126f0c310d7163a5b23494ebd37 Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Mon, 21 Sep 2026 18:52:11 +0530 Subject: [PATCH 01/13] fix(accessibility): guard each overwriteCommand in the a11y wrap loop AccessibilityModule.onBeforeExecute wrapped every entry of the server-sent commandsToWrap list in one unguarded loop. The list can name a command the active driver never registered: appium sessions omit web-only commands, and the list also carries Selenium-shaped entries (startA11yScanning, stopA11yScanning, performScan with class HttpCommandExecutor, library org.openqa.selenium) meant for other SDKs. WebdriverIO's overwriteCommand throws on an unknown name, so the first such entry aborted the whole loop, left every command after it unwrapped, and surfaced as "Error in onBeforeExecute: overwriteCommand: no command to be overwritten: startA11yScanning". Guard each overwriteCommand call individually so an unknown name is skipped (debug-logged) and the rest of the list still wraps. Ports the guard already shipped on the v9 line in 8539e5f to v8. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/guard-command-wrap-v8.md | 16 +++++++++ .../src/cli/modules/accessibilityModule.ts | 20 +++++++---- .../cli/modules/accessibilityModule.test.ts | 34 +++++++++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 .changeset/guard-command-wrap-v8.md diff --git a/.changeset/guard-command-wrap-v8.md b/.changeset/guard-command-wrap-v8.md new file mode 100644 index 00000000..20d3b461 --- /dev/null +++ b/.changeset/guard-command-wrap-v8.md @@ -0,0 +1,16 @@ +--- +"@wdio/browserstack-service": patch +--- + +fix(a11y): skip unregistered commands instead of aborting accessibility command wrapping + +`AccessibilityModule.onBeforeExecute` wrapped every entry of the server-sent +`commandsToWrap` list in a single unguarded loop. The list can name a command the active +driver never registered — appium sessions omit web-only commands, and the list also carries +Selenium-shaped entries (`startA11yScanning`, `stopA11yScanning`, `performScan` with class +`HttpCommandExecutor`) intended for other SDKs. WebdriverIO's `overwriteCommand` throws on an +unknown name, so the first such entry aborted the whole loop and left every command after it +unwrapped, surfacing as `Error in onBeforeExecute: overwriteCommand: no command to be +overwritten: startA11yScanning`. Each `overwriteCommand` call is now individually guarded, so +an unknown name is skipped (logged at debug) and the rest of the list still auto-scans. Ports +the guard already shipped on the v9 line. diff --git a/packages/browserstack-service/src/cli/modules/accessibilityModule.ts b/packages/browserstack-service/src/cli/modules/accessibilityModule.ts index e568f14d..de4aebaf 100644 --- a/packages/browserstack-service/src/cli/modules/accessibilityModule.ts +++ b/packages/browserstack-service/src/cli/modules/accessibilityModule.ts @@ -139,16 +139,24 @@ export default class AccessibilityModule extends BaseModule { return } - // Wrap commands if accessibility scripts are available + // Guard EACH overwriteCommand individually: the list is server-sent and may name a + // command this driver never registered (appium omits web-only commands; the list also + // carries Selenium-shaped entries meant for other SDKs). overwriteCommand throws on an + // unknown name, so an unguarded loop aborts at the first one and drops every wrap after + // it. Skipping the unknown name keeps the rest of the list wrapped and auto-scanning. if (this.scriptInstance.commandsToWrap && this.scriptInstance.commandsToWrap.length > 0) { this.scriptInstance.commandsToWrap .filter((command) => command.name && command.class) .forEach((command) => { - browser.overwriteCommand( - command.name, - this.commandWrapper.bind(this, command), - command.class === 'Element' - ) + try { + browser.overwriteCommand( + command.name, + this.commandWrapper.bind(this, command), + command.class === 'Element' + ) + } catch (wrapError) { + this.logger.debug(`Skipping command wrap for ${command.name}: ${wrapError}`) + } }) } diff --git a/packages/browserstack-service/tests/cli/modules/accessibilityModule.test.ts b/packages/browserstack-service/tests/cli/modules/accessibilityModule.test.ts index e0e36768..8279e148 100644 --- a/packages/browserstack-service/tests/cli/modules/accessibilityModule.test.ts +++ b/packages/browserstack-service/tests/cli/modules/accessibilityModule.test.ts @@ -85,6 +85,7 @@ import { HookState } from '../../../src/cli/states/hookState.js' import { TestFrameworkState } from '../../../src/cli/states/testFrameworkState.js' import { BrowserstackCLI } from '../../../src/cli/index.js' import { shouldScanTestForAccessibility, validateCapsWithA11y, validateCapsWithAppA11y } from '../../../src/util.js' +import accessibilityScripts from '../../../src/scripts/accessibility-scripts.js' describe('AccessibilityModule', () => { let accessibilityModule: AccessibilityModule @@ -265,6 +266,39 @@ describe('AccessibilityModule', () => { expect(loggerWarnSpy).toHaveBeenCalledWith('Accessibility scanning cannot be stopped from outside the test') }) + + // SDK-7452: the server-sent commandsToWrap list ends with Selenium-shaped entries + // (startA11yScanning/stopA11yScanning/performScan, class HttpCommandExecutor) that a + // WebdriverIO driver never registers. overwriteCommand throws on those names, and without a + // per-command guard the throw escaped the forEach and aborted onBeforeExecute. + it('skips a command the driver did not register without aborting the wrap loop', async () => { + const loggerErrorSpy = vi.spyOn(accessibilityModule.logger, 'error') + accessibilityModule.accessibility = true + accessibilityModule.isAppAccessibility = true + vi.mocked(validateCapsWithA11y).mockReturnValue(true) + vi.mocked(validateCapsWithAppA11y).mockReturnValue(true) + + accessibilityScripts.commandsToWrap = [ + { name: 'click', class: 'Element' }, + { name: 'startA11yScanning', class: 'HttpCommandExecutor' }, + { name: 'addValue', class: 'Element' } + ] as any + mockBrowser.overwriteCommand = vi.fn((name: string) => { + if (name === 'startA11yScanning') { + throw new Error('overwriteCommand: no command to be overwritten: ' + name) + } + }) + + await accessibilityModule.onBeforeExecute() + + expect(mockBrowser.overwriteCommand).toHaveBeenCalledTimes(3) + expect(mockBrowser.overwriteCommand).toHaveBeenLastCalledWith('addValue', expect.any(Function), true) + expect(loggerErrorSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Error in onBeforeExecute') + ) + + accessibilityScripts.commandsToWrap = [] + }) }) describe('onBeforeTest', () => { From 355d1d5d669acf21c0ff6ee0ad075128dcabc0d4 Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Tue, 22 Sep 2026 13:06:26 +0530 Subject: [PATCH 02/13] refactor(accessibility): align the wrap-loop guard comment with the v9 line The v8 guard body is byte-identical to the one on v9; the comment was not. Drop the v8-specific narrative for v9's rationale, minus the isAppAccessibility history that never applied to v8. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/cli/modules/accessibilityModule.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/browserstack-service/src/cli/modules/accessibilityModule.ts b/packages/browserstack-service/src/cli/modules/accessibilityModule.ts index de4aebaf..1a12b179 100644 --- a/packages/browserstack-service/src/cli/modules/accessibilityModule.ts +++ b/packages/browserstack-service/src/cli/modules/accessibilityModule.ts @@ -139,11 +139,9 @@ export default class AccessibilityModule extends BaseModule { return } - // Guard EACH overwriteCommand individually: the list is server-sent and may name a - // command this driver never registered (appium omits web-only commands; the list also - // carries Selenium-shaped entries meant for other SDKs). overwriteCommand throws on an - // unknown name, so an unguarded loop aborts at the first one and drops every wrap after - // it. Skipping the unknown name keeps the rest of the list wrapped and auto-scanning. + // Guard EACH overwriteCommand individually: a command the driver doesn't register just + // skips (logged) rather than aborting the whole wrap loop, so the commands appium DOES + // register (click, setValue, ...) still auto-scan on app. if (this.scriptInstance.commandsToWrap && this.scriptInstance.commandsToWrap.length > 0) { this.scriptInstance.commandsToWrap .filter((command) => command.name && command.class) From 5e8c8ffb044f2a1171005a400b5bd3e62db1a343 Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Tue, 22 Sep 2026 13:07:09 +0530 Subject: [PATCH 03/13] chore: drop the hand-written changeset changeset-from-pr.yml generates .changeset/pr-.md from the PR's Release section on this branch, so a hand-written file only duplicates the CHANGELOG entry. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/guard-command-wrap-v8.md | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 .changeset/guard-command-wrap-v8.md diff --git a/.changeset/guard-command-wrap-v8.md b/.changeset/guard-command-wrap-v8.md deleted file mode 100644 index 20d3b461..00000000 --- a/.changeset/guard-command-wrap-v8.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@wdio/browserstack-service": patch ---- - -fix(a11y): skip unregistered commands instead of aborting accessibility command wrapping - -`AccessibilityModule.onBeforeExecute` wrapped every entry of the server-sent -`commandsToWrap` list in a single unguarded loop. The list can name a command the active -driver never registered — appium sessions omit web-only commands, and the list also carries -Selenium-shaped entries (`startA11yScanning`, `stopA11yScanning`, `performScan` with class -`HttpCommandExecutor`) intended for other SDKs. WebdriverIO's `overwriteCommand` throws on an -unknown name, so the first such entry aborted the whole loop and left every command after it -unwrapped, surfacing as `Error in onBeforeExecute: overwriteCommand: no command to be -overwritten: startA11yScanning`. Each `overwriteCommand` call is now individually guarded, so -an unknown name is skipped (logged at debug) and the rest of the list still auto-scans. Ports -the guard already shipped on the v9 line. From 2443f4d4f23d268e3ee9454a0401fdce25ffe855 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 07:39:59 +0000 Subject: [PATCH 04/13] chore(changeset): auto-generate from PR template (patch) --- .changeset/pr-212.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pr-212.md diff --git a/.changeset/pr-212.md b/.changeset/pr-212.md new file mode 100644 index 00000000..5789e0e6 --- /dev/null +++ b/.changeset/pr-212.md @@ -0,0 +1,5 @@ +--- +"@wdio/browserstack-service": patch +--- + +- Fixed accessibility command wrapping aborting on App Automate sessions, which logged `Error in onBeforeExecute` and left the remaining commands unwrapped for auto-scanning. From f69e0949d16958b0be1b606a4ad1a5bc650bfa40 Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Tue, 22 Sep 2026 20:24:36 +0530 Subject: [PATCH 05/13] fix(a11y): log the error the results-summary getters swallow getAppA11yResultsSummary and getA11yResultsSummary caught their failure with a bare `catch`, logged a fixed "No accessibility summary was found." and returned {}. The sibling getters (getAppA11yResults, getA11yResults) bind the error and debug-log it; these two discarded it. The message is the same whether the results API errored, returned nothing, or the 30s poll in getAppA11yResultResponse timed out, so an empty summary on a real run cannot be told apart from an API failure. Bind the error and debug-log it, matching the results getters. Co-Authored-By: Claude Opus 5 (1M context) --- packages/browserstack-service/src/util.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 8a14f3be..bbd155b2 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -682,8 +682,9 @@ export const getAppA11yResultsSummary = PerformanceTester.measureWrapper(PERFORM const result = apiRespone?.data?.data?.summary BStackLogger.debug(`Polling Result: ${JSON.stringify(result)}`) return result - } catch { + } catch (error: any) { BStackLogger.error('No accessibility summary was found.') + BStackLogger.debug(`getAppA11yResultsSummary Failed. Error: ${error}`) return {} } }) @@ -711,8 +712,9 @@ export const getA11yResultsSummary = PerformanceTester.measureWrapper(PERFORMANC await performA11yScan(isAppAutomate, browser, isBrowserStackSession, isAccessibility) const summaryResults: { [key: string]: any; } = await (browser as WebdriverIO.Browser).executeAsync(AccessibilityScripts.getResultsSummary as string) return summaryResults - } catch { + } catch (error: any) { BStackLogger.error('No accessibility summary was found.') + BStackLogger.debug(`getA11yResultsSummary Failed. Error: ${error}`) return {} } }) From 8de3c64cf6c732358f8754faa332f17ff4dfbdb6 Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Tue, 22 Sep 2026 20:45:59 +0530 Subject: [PATCH 06/13] fix(a11y): log the summary-getter cause at error level, not debug The previous commit put the cause behind BStackLogger.debug, which does not print at the default log level -- on O11Y build t4x7uep76ef2im4wwjv0rcm6jk6aohbkrdqhni6h the run carried the change and still emitted a bare "No accessibility summary was found." with no DEBUG line anywhere. The message is already an error, so the cause belongs on that line. An empty summary now reports whether the results API errored, returned no payload, or the 30s poll in getAppA11yResultResponse timed out, without needing logLevel: debug. Co-Authored-By: Claude Opus 5 (1M context) --- packages/browserstack-service/src/util.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index bbd155b2..1be6563d 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -683,8 +683,7 @@ export const getAppA11yResultsSummary = PerformanceTester.measureWrapper(PERFORM BStackLogger.debug(`Polling Result: ${JSON.stringify(result)}`) return result } catch (error: any) { - BStackLogger.error('No accessibility summary was found.') - BStackLogger.debug(`getAppA11yResultsSummary Failed. Error: ${error}`) + BStackLogger.error(`No accessibility summary was found. Error: ${error}`) return {} } }) @@ -713,8 +712,7 @@ export const getA11yResultsSummary = PerformanceTester.measureWrapper(PERFORMANC const summaryResults: { [key: string]: any; } = await (browser as WebdriverIO.Browser).executeAsync(AccessibilityScripts.getResultsSummary as string) return summaryResults } catch (error: any) { - BStackLogger.error('No accessibility summary was found.') - BStackLogger.debug(`getA11yResultsSummary Failed. Error: ${error}`) + BStackLogger.error(`No accessibility summary was found. Error: ${error}`) return {} } }) From d08448f7125a9c1ac0cf9fa27202066bc5750d99 Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Tue, 22 Sep 2026 21:39:36 +0530 Subject: [PATCH 07/13] fix(a11y): render the caught value instead of [object Object] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pollApi throws a plain object, not an Error: throw { data: {}, headers: {}, message: ... } so `${error}` produced a literal "[object Object]" — which is what O11Y build eo2oxrhccxnhwxf5f3gcg4i8zqhx2ddsgagusp93 logged, still saying nothing about the cause. The useful field is `message`, carrying the server's message from the response body. Render `error?.message` with a util.inspect fallback, in all three getters. The results getter also stops claiming "No accessibility summary was found" when it failed to fetch results, and drops its debug duplicate. Worth noting for whoever reads the next failure: of pollApi's paths only a non-404 error response throws. A poll that exhausts the upper time limit, a 404 with a missing next_poll_time header, and a request with no response all RETURN { data: {} } instead. So this error line means a hard HTTP error from the results API, not a timeout. Co-Authored-By: Claude Opus 5 (1M context) --- packages/browserstack-service/src/util.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 1be6563d..d87049c7 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -660,8 +660,7 @@ export const getAppA11yResults = PerformanceTester.measureWrapper(PERFORMANCE_SD BStackLogger.debug(`Polling Result: ${JSON.stringify(result)}`) return result } catch (error: any) { - BStackLogger.error('No accessibility summary was found.') - BStackLogger.debug(`getAppA11yResults Failed. Error: ${error}`) + BStackLogger.error(`No accessibility results were found. Error: ${error?.message ?? util.inspect(error, { depth: 2 })}`) return [] } }) @@ -683,7 +682,7 @@ export const getAppA11yResultsSummary = PerformanceTester.measureWrapper(PERFORM BStackLogger.debug(`Polling Result: ${JSON.stringify(result)}`) return result } catch (error: any) { - BStackLogger.error(`No accessibility summary was found. Error: ${error}`) + BStackLogger.error(`No accessibility summary was found. Error: ${error?.message ?? util.inspect(error, { depth: 2 })}`) return {} } }) @@ -712,7 +711,7 @@ export const getA11yResultsSummary = PerformanceTester.measureWrapper(PERFORMANC const summaryResults: { [key: string]: any; } = await (browser as WebdriverIO.Browser).executeAsync(AccessibilityScripts.getResultsSummary as string) return summaryResults } catch (error: any) { - BStackLogger.error(`No accessibility summary was found. Error: ${error}`) + BStackLogger.error(`No accessibility summary was found. Error: ${error?.message ?? util.inspect(error, { depth: 2 })}`) return {} } }) From 3aaf8313c2dd806febfb78e622e66c93051c75b8 Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Tue, 22 Sep 2026 21:52:58 +0530 Subject: [PATCH 08/13] fix(a11y): keep the status code and body when pollApi rejects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A local App Automate run of the android start-a11y cell produced: ERROR @wdio/browserstack-service: No accessibility summary was found. Error: { data: {}, headers: {}, message: undefined } message is undefined because the results API answered with a JSON body that has no `message` field — the ternary only covers an EMPTY body, so a JSON error body of any other shape yields undefined and the reader learns nothing. The status code and the body itself were never carried at all. Carry statusCode and body on the rejection, and fall back to `HTTP : ` when the body has no message, so an empty summary always names the HTTP failure behind it. JSON.parse is also guarded: a non-JSON error body previously threw inside the catch, replacing the real failure with a SyntaxError. Co-Authored-By: Claude Opus 5 (1M context) --- packages/browserstack-service/src/util.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index d87049c7..0e14dcf7 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -1784,10 +1784,20 @@ export async function pollApi( await new Promise((resolve) => setTimeout(resolve, elapsedTime)) return pollApi(url, params, headers, upperLimit, startTime) } else if (error.response) { + const statusCode = error.response.statusCode + const body = typeof error.response.body === 'string' ? error.response.body : '' + let message: string | undefined + try { + message = body ? JSON.parse(body).message : undefined + } catch { + // non-JSON body; the raw-body message below carries it instead + } throw { data: {}, headers: {}, - message: error.response.body ? JSON.parse(error.response.body).message : 'Unknown error', + statusCode, + body, + message: message ?? `HTTP ${statusCode}${body ? `: ${body.slice(0, 300)}` : ''}`, } } else { BStackLogger.error(`Unexpected error occurred: ${error}`) From f37ec83f5a28c87324278bc6699dad3523b92f5b Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Tue, 22 Sep 2026 22:58:19 +0530 Subject: [PATCH 09/13] fix(a11y): name the identifiers the App A11y results query used A local run showed the results API answering HTTP 422 "test_run_uuid is invalid", but the log did not say which uuid was sent, so the failure could not be attributed to the SDK or to the service. Both App A11y getters now report testRunUuid and sessionId alongside the error. Neither is a secret; the a11y JWT is still never logged. Co-Authored-By: Claude Opus 5 (1M context) --- packages/browserstack-service/src/util.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 0e14dcf7..02b09e2c 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -660,7 +660,7 @@ export const getAppA11yResults = PerformanceTester.measureWrapper(PERFORMANCE_SD BStackLogger.debug(`Polling Result: ${JSON.stringify(result)}`) return result } catch (error: any) { - BStackLogger.error(`No accessibility results were found. Error: ${error?.message ?? util.inspect(error, { depth: 2 })}`) + BStackLogger.error(`No accessibility results were found for testRunUuid=${process.env.TEST_ANALYTICS_ID} sessionId=${sessionId}. Error: ${error?.message ?? util.inspect(error, { depth: 2 })}`) return [] } }) @@ -682,7 +682,7 @@ export const getAppA11yResultsSummary = PerformanceTester.measureWrapper(PERFORM BStackLogger.debug(`Polling Result: ${JSON.stringify(result)}`) return result } catch (error: any) { - BStackLogger.error(`No accessibility summary was found. Error: ${error?.message ?? util.inspect(error, { depth: 2 })}`) + BStackLogger.error(`No accessibility summary was found for testRunUuid=${process.env.TEST_ANALYTICS_ID} sessionId=${sessionId}. Error: ${error?.message ?? util.inspect(error, { depth: 2 })}`) return {} } }) From ec9b48d66a008f33257a13e613559e7fc1c05336 Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Wed, 23 Sep 2026 12:36:24 +0530 Subject: [PATCH 10/13] fix(a11y): guard the wrap loop on the non-CLI flow too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AccessibilityHandler.before() iterates the SAME server-sent accessibilityScripts.commandsToWrap list as the CLI module and called browser.overwriteCommand with no per-command boundary, so the Selenium-shaped entries (startA11yScanning/stopA11yScanning/performScan, class HttpCommandExecutor) produce the SDK-7452 symptom on that flow as well. It is reachable for App Automate — the handler takes isAppAutomate and branches on isAppAccessibilityAutomationSession — and before() has no try/catch of its own, so a throw rejects the whole hook rather than being logged as it is on the CLI side. v9 already guards this loop; this ports that guard, including its debug message. v9's prevImpl/orig binding is a separate change needing a different commandWrapper signature and is deliberately not ported. Co-Authored-By: Claude Opus 5 (1M context) --- packages/browserstack-service/src/accessibility-handler.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/browserstack-service/src/accessibility-handler.ts b/packages/browserstack-service/src/accessibility-handler.ts index 9da71d1d..fd7041f1 100644 --- a/packages/browserstack-service/src/accessibility-handler.ts +++ b/packages/browserstack-service/src/accessibility-handler.ts @@ -247,7 +247,11 @@ class _AccessibilityHandler { .filter((command) => command.name && command.class) .forEach((command) => { const browser = this._browser as WebdriverIO.Browser - browser.overwriteCommand(command.name, this.commandWrapper.bind(this, command), command.class === 'Element') + try { + browser.overwriteCommand(command.name, this.commandWrapper.bind(this, command), command.class === 'Element') + } catch (error) { + BStackLogger.debug(`Exception in overwrite command ${command.name} - ${error}`) + } }) PerformanceTester.end(PERFORMANCE_SDK_EVENTS.CONFIG_EVENTS.ACCESSIBILITY) From 7985b7b996214cf9878022cd5ce612f744b7194f Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Wed, 23 Sep 2026 12:36:24 +0530 Subject: [PATCH 11/13] test(a11y): restore commandsToWrap in a finally, not on the success path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The regression test assigned to the module-scope accessibilityScripts singleton and reset it as the last statement of the test body, so any earlier throw — a failed expect, or onBeforeExecute rejecting — left a populated commandsToWrap behind for every later test in the file, turning one real failure into a cascade. afterEach only calls vi.resetAllMocks(), which does not restore a plain property written onto the mocked module. Capture the original and restore it in a finally block, rather than resetting to a hardcoded [] that would be wrong if the shared mock ever gains a non-empty default. Co-Authored-By: Claude Opus 5 (1M context) --- .../cli/modules/accessibilityModule.test.ts | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/browserstack-service/tests/cli/modules/accessibilityModule.test.ts b/packages/browserstack-service/tests/cli/modules/accessibilityModule.test.ts index 8279e148..0cd78797 100644 --- a/packages/browserstack-service/tests/cli/modules/accessibilityModule.test.ts +++ b/packages/browserstack-service/tests/cli/modules/accessibilityModule.test.ts @@ -278,26 +278,29 @@ describe('AccessibilityModule', () => { vi.mocked(validateCapsWithA11y).mockReturnValue(true) vi.mocked(validateCapsWithAppA11y).mockReturnValue(true) - accessibilityScripts.commandsToWrap = [ - { name: 'click', class: 'Element' }, - { name: 'startA11yScanning', class: 'HttpCommandExecutor' }, - { name: 'addValue', class: 'Element' } - ] as any - mockBrowser.overwriteCommand = vi.fn((name: string) => { - if (name === 'startA11yScanning') { - throw new Error('overwriteCommand: no command to be overwritten: ' + name) - } - }) - - await accessibilityModule.onBeforeExecute() - - expect(mockBrowser.overwriteCommand).toHaveBeenCalledTimes(3) - expect(mockBrowser.overwriteCommand).toHaveBeenLastCalledWith('addValue', expect.any(Function), true) - expect(loggerErrorSpy).not.toHaveBeenCalledWith( - expect.stringContaining('Error in onBeforeExecute') - ) - - accessibilityScripts.commandsToWrap = [] + const originalCommandsToWrap = accessibilityScripts.commandsToWrap + try { + accessibilityScripts.commandsToWrap = [ + { name: 'click', class: 'Element' }, + { name: 'startA11yScanning', class: 'HttpCommandExecutor' }, + { name: 'addValue', class: 'Element' } + ] as any + mockBrowser.overwriteCommand = vi.fn((name: string) => { + if (name === 'startA11yScanning') { + throw new Error('overwriteCommand: no command to be overwritten: ' + name) + } + }) + + await accessibilityModule.onBeforeExecute() + + expect(mockBrowser.overwriteCommand).toHaveBeenCalledTimes(3) + expect(mockBrowser.overwriteCommand).toHaveBeenLastCalledWith('addValue', expect.any(Function), true) + expect(loggerErrorSpy).not.toHaveBeenCalledWith( + expect.stringContaining('Error in onBeforeExecute') + ) + } finally { + accessibilityScripts.commandsToWrap = originalCommandsToWrap + } }) }) From 44442975e3fe21f31a23c2f31fb93b0872bea039 Mon Sep 17 00:00:00 2001 From: Kamalpreet Kaur Date: Wed, 23 Sep 2026 15:17:57 +0530 Subject: [PATCH 12/13] test(a11y): cover the non-CLI wrap-loop guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DEF-12: the guard added to AccessibilityHandler.before() changed behaviour in a file the repo tests densely (accessibility-handler.test.ts, 26 cases) with no test of its own. Drives the real payload shape — an HttpCommandExecutor-class startA11yScanning between two Element commands — and asserts all three wraps are attempted and the trailing command still wraps. Verified to fail without the guard ("expected spy to be called 3 times, but got 2") and pass with it. The existing suite never reached this loop: the shared browser mock has no overwriteCommand, so `'overwriteCommand' in browser` was false in every other case. Co-Authored-By: Claude Opus 5 (1M context) --- .../tests/accessibility-handler.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/browserstack-service/tests/accessibility-handler.test.ts b/packages/browserstack-service/tests/accessibility-handler.test.ts index db21ea94..8aa8a6b8 100644 --- a/packages/browserstack-service/tests/accessibility-handler.test.ts +++ b/packages/browserstack-service/tests/accessibility-handler.test.ts @@ -8,6 +8,7 @@ import logger from '@wdio/logger' import AccessibilityHandler from '../src/accessibility-handler.js' import * as utils from '../src/util.js' +import accessibilityScripts from '../src/scripts/accessibility-scripts.js' import type { Capabilities, Options } from '@wdio/types' import * as bstackLogger from '../src/bstackLogger.js' import type { BrowserstackConfig, BrowserstackOptions } from '../src/types.js' @@ -154,6 +155,37 @@ describe('before', () => { (browser as WebdriverIO.Browser).getAccessibilityResults() expect(getA11yResultsSpy).toBeCalledTimes(1) }) + + // The server-sent commandsToWrap list can name a command this driver never registered + // (Selenium-shaped entries meant for another SDK). overwriteCommand throws on those, and + // before() has no try/catch of its own, so an unguarded loop rejects the whole hook. + it('skips a command the driver did not register without aborting the wrap loop', async () => { + const originalCommandsToWrap = accessibilityScripts.commandsToWrap + try { + isBrowserstackSessionSpy.mockReturnValue(true) + isAccessibilityAutomationSessionSpy.mockReturnValue(true) + vi.spyOn(utils, 'validateCapsWithA11y').mockReturnValue(true) + accessibilityScripts.commandsToWrap = [ + { name: 'click', class: 'Element' }, + { name: 'startA11yScanning', class: 'HttpCommandExecutor' }, + { name: 'addValue', class: 'Element' } + ] as any + const overwriteCommand = vi.fn((name: string) => { + if (name === 'startA11yScanning') { + throw new Error('overwriteCommand: no command to be overwritten: ' + name) + } + }); + (browser as any).overwriteCommand = overwriteCommand + + await accessibilityHandler.before('session123') + + expect(overwriteCommand).toHaveBeenCalledTimes(3) + expect(overwriteCommand).toHaveBeenLastCalledWith('addValue', expect.any(Function), true) + } finally { + accessibilityScripts.commandsToWrap = originalCommandsToWrap + delete (browser as any).overwriteCommand + } + }) }) describe('beforeScenario', () => { From 969aa7dd157a6a36f231bc2a6c9c0b8c26251c00 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2026 09:48:42 +0000 Subject: [PATCH 13/13] chore(changeset): auto-generate from PR template (patch) --- .changeset/pr-212.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/pr-212.md b/.changeset/pr-212.md index 5789e0e6..15d058f5 100644 --- a/.changeset/pr-212.md +++ b/.changeset/pr-212.md @@ -2,4 +2,5 @@ "@wdio/browserstack-service": patch --- -- Fixed accessibility command wrapping aborting on App Automate sessions, which logged `Error in onBeforeExecute` and left the remaining commands unwrapped for auto-scanning. +- Fixed accessibility command wrapping aborting on App Automate sessions, which logged `Error in onBeforeExecute` and left the remaining commands unwrapped for auto-scanning. Applies to both the CLI and the non-CLI flow. +- Accessibility results and summary failures now report the HTTP status, the response body and the identifiers the query used, instead of an empty result with no explanation.