From 780e7a0ac24729253bfff95eab1c7844bd9d011a Mon Sep 17 00:00:00 2001 From: Harshit Date: Tue, 9 Jun 2026 14:24:49 +0530 Subject: [PATCH] fix(wdio-browserstack-service): exclude `action` from a11y command wrapping (SDK-6265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src/accessibility-handler.ts | 5 +++++ .../src/cli/modules/accessibilityModule.ts | 5 +++++ packages/wdio-browserstack-service/src/constants.ts | 10 ++++++++++ 3 files changed, 20 insertions(+) diff --git a/packages/wdio-browserstack-service/src/accessibility-handler.ts b/packages/wdio-browserstack-service/src/accessibility-handler.ts index 76ae8329cc0..c8fbe89c91d 100644 --- a/packages/wdio-browserstack-service/src/accessibility-handler.ts +++ b/packages/wdio-browserstack-service/src/accessibility-handler.ts @@ -73,6 +73,7 @@ import { import accessibilityScripts from './scripts/accessibility-scripts.js' import PerformanceTester from './instrumentation/performance/performance-tester.js' import * as PERFORMANCE_SDK_EVENTS from './instrumentation/performance/constants.js' +import { ACCESSIBILITY_WRAP_EXCLUDED_COMMANDS } from './constants.js' import { BStackLogger } from './bstackLogger.js' @@ -254,6 +255,10 @@ class _AccessibilityHandler { accessibilityScripts.commandsToWrap .filter((command) => command.name && command.class) + // Skip synchronous chainable commands (e.g. `action`) — the async + // wrapper would turn their return value into a Promise and break + // chaining. See SDK-6265. + .filter((command) => !ACCESSIBILITY_WRAP_EXCLUDED_COMMANDS.includes(command.name)) .forEach((command) => { const browser = this._browser as WebdriverIO.Browser try { diff --git a/packages/wdio-browserstack-service/src/cli/modules/accessibilityModule.ts b/packages/wdio-browserstack-service/src/cli/modules/accessibilityModule.ts index d71be0fe25c..6ab8b678486 100644 --- a/packages/wdio-browserstack-service/src/cli/modules/accessibilityModule.ts +++ b/packages/wdio-browserstack-service/src/cli/modules/accessibilityModule.ts @@ -13,6 +13,7 @@ import type { Command } from '../../scripts/accessibility-scripts.js' import accessibilityScripts from '../../scripts/accessibility-scripts.js' import { _getParamsForAppAccessibility, formatString, getAppA11yResults, getAppA11yResultsSummary, shouldScanTestForAccessibility, validateCapsWithA11y, validateCapsWithAppA11y, isBrowserstackSession } from '../../util.js' import { AutomationFrameworkConstants } from '../frameworks/constants/automationFrameworkConstants.js' +import { ACCESSIBILITY_WRAP_EXCLUDED_COMMANDS } from '../../constants.js' import util from 'node:util' import type { Accessibility } from '@browserstack/wdio-browserstack-service' import PerformanceTester from '../../instrumentation/performance/performance-tester.js' @@ -138,6 +139,10 @@ export default class AccessibilityModule extends BaseModule { if (this.scriptInstance.commandsToWrap && this.scriptInstance.commandsToWrap.length > 0) { this.scriptInstance.commandsToWrap .filter((command) => command.name && command.class) + // Skip synchronous chainable commands (e.g. `action`) — the async + // wrapper would turn their return value into a Promise and break + // chaining. See SDK-6265. + .filter((command) => !ACCESSIBILITY_WRAP_EXCLUDED_COMMANDS.includes(command.name)) .forEach((command) => { browser.overwriteCommand( // @ts-expect-error fix type diff --git a/packages/wdio-browserstack-service/src/constants.ts b/packages/wdio-browserstack-service/src/constants.ts index 1a97ffba197..9d17a020d2c 100644 --- a/packages/wdio-browserstack-service/src/constants.ts +++ b/packages/wdio-browserstack-service/src/constants.ts @@ -62,6 +62,16 @@ export const PERCY_DOM_CHANGING_COMMANDS_ENDPOINTS = [ '/session/:sessionId/appium/device/shake' ] +/** + * Commands that synchronously return a chainable builder (rather than a Promise) + * and therefore must NOT be wrapped by the accessibility command wrapper. The + * wrapper is async, so wrapping a synchronous command converts its return value + * into a Promise and breaks chaining (e.g. `browser.action('pointer').move(...)` + * fails with "move is not a function" because `action` now resolves to a Promise). + * See SDK-6265. + */ +export const ACCESSIBILITY_WRAP_EXCLUDED_COMMANDS = ['action'] + export const CAPTURE_MODES = ['click', 'auto', 'screenshot', 'manual', 'testcase'] export const LOG_KIND_USAGE_MAP = { 'TEST_LOG': 'log',