diff --git a/.changeset/restore-app-command-wrapping.md b/.changeset/restore-app-command-wrapping.md new file mode 100644 index 0000000..c8992a5 --- /dev/null +++ b/.changeset/restore-app-command-wrapping.md @@ -0,0 +1,14 @@ +--- +"@wdio/browserstack-service": patch +--- + +fix(a11y): restore per-command auto-scanning for App Automate accessibility sessions + +App Automate accessibility sessions were skipped by the per-command `overwriteCommand` +wrapping in `onBeforeExecute` (guarded to `!isAppAccessibility`), so app a11y scans only +fired via an explicit `performScan()` or the end-of-test lifecycle scan — per-command +auto-scanning that web sessions get was effectively disabled for app. Command wrapping now +applies to app sessions too, with each `overwriteCommand` call individually guarded so a +command the appium driver does not register is skipped (logged at debug) instead of aborting +`onBeforeExecute`. Commands appium does register (`click`, `setValue`, ...) now auto-scan on +App Automate, matching the web flow. diff --git a/packages/browserstack-service/src/cli/modules/accessibilityModule.ts b/packages/browserstack-service/src/cli/modules/accessibilityModule.ts index 5b7399e..e999191 100644 --- a/packages/browserstack-service/src/cli/modules/accessibilityModule.ts +++ b/packages/browserstack-service/src/cli/modules/accessibilityModule.ts @@ -138,20 +138,28 @@ export default class AccessibilityModule extends BaseModule { return } - // Web command wrapping (overwriteCommand) only applies to the web a11y - // flow. App Automate a11y scans run via the performScan/test-lifecycle - // path, and appium drivers don't register these commands, so - // overwriteCommand would throw and abort onBeforeExecute. - if (!this.isAppAccessibility && 'overwriteCommand' in browser && Array.isArray(this.scriptInstance.commandsToWrap)) { + // Per-command wrapping (overwriteCommand) drives auto-scanning on both the web and the + // App Automate a11y flows. App sessions were previously skipped (isAppAccessibility gate) + // out of a concern that appium drivers don't register these commands, so overwriteCommand + // would throw and abort onBeforeExecute — but that skip disabled per-command auto-scan for + // app entirely, so app a11y scans only fired via an explicit performScan()/lifecycle. + // Instead, wrap for every flow and 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 ('overwriteCommand' in browser && Array.isArray(this.scriptInstance.commandsToWrap)) { this.scriptInstance.commandsToWrap .filter((command) => command.name && command.class) .forEach((command) => { - browser.overwriteCommand( - // @ts-expect-error fix type - command.name, - this.commandWrapper.bind(this, command), - command.class === 'Element' - ) + try { + browser.overwriteCommand( + // @ts-expect-error fix type + 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 00c4443..cfe877a 100644 --- a/packages/browserstack-service/tests/cli/modules/accessibilityModule.test.ts +++ b/packages/browserstack-service/tests/cli/modules/accessibilityModule.test.ts @@ -369,10 +369,12 @@ describe('AccessibilityModule', () => { }) }) - // SDK-3813: App Automate + App Accessibility sessions were mis-routed onto the - // web a11y path (Chrome-only gate + overwriteCommand on commands absent from the - // appium driver), so App A11y scans never ran. The CLI module must detect app - // sessions from caps (like the classic flow) and skip web command wrapping. + // SDK-3813 (+ follow-up APPA11Y-5542): App Automate + App Accessibility sessions are detected + // from caps (like the classic flow) and take the app validation/scan path (not the Chrome-only + // web gate). Unlike the original SDK-3813 fix, per-command wrapping is NOT skipped for app: it is + // applied with each overwriteCommand individually guarded, so the commands appium DOES register + // (click, setValue, ...) auto-scan, while a command the driver doesn't register is skipped + // instead of aborting onBeforeExecute. This restores per-command app auto-scanning. describe('onBeforeExecute - App Automate (SDK-3813)', () => { const appGetState = (instance: any, key: string) => { if (key.includes('input_capabilities')) { @@ -391,7 +393,7 @@ describe('AccessibilityModule', () => { accessibilityScripts.commandsToWrap = [] }) - it('detects app session from caps and skips web command-overwrite', async () => { + it('detects app session from caps and wraps commands for per-command auto-scan', async () => { // binary flag is false; caps say app -> module must still take app path vi.mocked(validateCapsWithAppA11y).mockReturnValue(true) vi.mocked(validateCapsWithA11y).mockReturnValue(true) @@ -404,9 +406,9 @@ describe('AccessibilityModule', () => { await accessibilityModule.onBeforeExecute() expect(accessibilityModule.isAppAccessibility).toBe(true) - // Symptom 1: no web command-overwrite attempted on an app session - expect(mockBrowser.overwriteCommand).not.toHaveBeenCalled() - // Symptom 2: Chrome-only web gate never consulted; app validation used + // App sessions wrap commands too, so DOM commands (click, ...) auto-scan per-command. + expect(mockBrowser.overwriteCommand).toHaveBeenCalled() + // Chrome-only web gate never consulted; app validation used expect(validateCapsWithAppA11y).toHaveBeenCalled() expect(validateCapsWithA11y).not.toHaveBeenCalled() }) @@ -425,7 +427,7 @@ describe('AccessibilityModule', () => { expect(result).toEqual({ scanned: true }) }) - it('does not abort onBeforeExecute when web command wrapping would throw', async () => { + it('does not abort onBeforeExecute when an individual command wrap throws', async () => { vi.mocked(validateCapsWithAppA11y).mockReturnValue(true) vi.mocked(validateCapsWithA11y).mockReturnValue(true) accessibilityScripts.commandsToWrap = [{ name: 'startA11yScanning', class: 'Browser' }] @@ -437,7 +439,10 @@ describe('AccessibilityModule', () => { await accessibilityModule.onBeforeExecute() - expect(mockBrowser.overwriteCommand).not.toHaveBeenCalled() + // The wrap IS attempted (and throws for this unregistered command), but the per-command + // try/catch swallows it so the wrap loop and onBeforeExecute complete without hitting the + // outer error handler. + expect(mockBrowser.overwriteCommand).toHaveBeenCalled() expect(errorSpy).not.toHaveBeenCalledWith( expect.stringContaining('Error in onBeforeExecute') )