Skip to content

Commit 780e7a0

Browse files
fix(wdio-browserstack-service): exclude action from a11y command wrapping (SDK-6265)
The accessibility command wrapper is declared async, so wrapping WebdriverIO's synchronous `browser.action()` turned its return value into a Promise — breaking `browser.action(...).move()` / `.down()`, and therefore `browser.moveTo()` and `browser.keys(Key.Tab)`, which are built on top of `action()`. With `accessibility: true` users saw "browser.action(...).move is not a function" / "keyAction.down is not a function"; the same code works with accessibility disabled. Exclude `action` (the only synchronous chainable command) from wrapping in both install sites (direct flow + CLI/gRPC flow). Already-async commands (`keys`, `actions`, `moveTo`) stay wrapped, so accessibility scan coverage is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 98ba397 commit 780e7a0

3 files changed

Lines changed: 20 additions & 0 deletions

File tree

packages/wdio-browserstack-service/src/accessibility-handler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import {
7373
import accessibilityScripts from './scripts/accessibility-scripts.js'
7474
import PerformanceTester from './instrumentation/performance/performance-tester.js'
7575
import * as PERFORMANCE_SDK_EVENTS from './instrumentation/performance/constants.js'
76+
import { ACCESSIBILITY_WRAP_EXCLUDED_COMMANDS } from './constants.js'
7677

7778
import { BStackLogger } from './bstackLogger.js'
7879

@@ -254,6 +255,10 @@ class _AccessibilityHandler {
254255

255256
accessibilityScripts.commandsToWrap
256257
.filter((command) => command.name && command.class)
258+
// Skip synchronous chainable commands (e.g. `action`) — the async
259+
// wrapper would turn their return value into a Promise and break
260+
// chaining. See SDK-6265.
261+
.filter((command) => !ACCESSIBILITY_WRAP_EXCLUDED_COMMANDS.includes(command.name))
257262
.forEach((command) => {
258263
const browser = this._browser as WebdriverIO.Browser
259264
try {

packages/wdio-browserstack-service/src/cli/modules/accessibilityModule.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type { Command } from '../../scripts/accessibility-scripts.js'
1313
import accessibilityScripts from '../../scripts/accessibility-scripts.js'
1414
import { _getParamsForAppAccessibility, formatString, getAppA11yResults, getAppA11yResultsSummary, shouldScanTestForAccessibility, validateCapsWithA11y, validateCapsWithAppA11y, isBrowserstackSession } from '../../util.js'
1515
import { AutomationFrameworkConstants } from '../frameworks/constants/automationFrameworkConstants.js'
16+
import { ACCESSIBILITY_WRAP_EXCLUDED_COMMANDS } from '../../constants.js'
1617
import util from 'node:util'
1718
import type { Accessibility } from '@browserstack/wdio-browserstack-service'
1819
import PerformanceTester from '../../instrumentation/performance/performance-tester.js'
@@ -138,6 +139,10 @@ export default class AccessibilityModule extends BaseModule {
138139
if (this.scriptInstance.commandsToWrap && this.scriptInstance.commandsToWrap.length > 0) {
139140
this.scriptInstance.commandsToWrap
140141
.filter((command) => command.name && command.class)
142+
// Skip synchronous chainable commands (e.g. `action`) — the async
143+
// wrapper would turn their return value into a Promise and break
144+
// chaining. See SDK-6265.
145+
.filter((command) => !ACCESSIBILITY_WRAP_EXCLUDED_COMMANDS.includes(command.name))
141146
.forEach((command) => {
142147
browser.overwriteCommand(
143148
// @ts-expect-error fix type

packages/wdio-browserstack-service/src/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ export const PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS = [
6262
'/session/:sessionId/appium/device/shake'
6363
]
6464

65+
/**
66+
* Commands that synchronously return a chainable builder (rather than a Promise)
67+
* and therefore must NOT be wrapped by the accessibility command wrapper. The
68+
* wrapper is async, so wrapping a synchronous command converts its return value
69+
* into a Promise and breaks chaining (e.g. `browser.action('pointer').move(...)`
70+
* fails with "move is not a function" because `action` now resolves to a Promise).
71+
* See SDK-6265.
72+
*/
73+
export const ACCESSIBILITY_WRAP_EXCLUDED_COMMANDS = ['action']
74+
6575
export const CAPTURE_MODES = ['click', 'auto', 'screenshot', 'manual', 'testcase']
6676
export const LOG_KIND_USAGE_MAP = {
6777
'TEST_LOG': 'log',

0 commit comments

Comments
 (0)