diff --git a/.changeset/pr-100.md b/.changeset/pr-100.md new file mode 100644 index 0000000..14d083e --- /dev/null +++ b/.changeset/pr-100.md @@ -0,0 +1,5 @@ +--- +"@wdio/browserstack-service": patch +--- + +- Fixed App Automate session names not updating to the test title when the app is provided via the `appium:app` capability. diff --git a/packages/browserstack-service/src/cli/modules/automateModule.ts b/packages/browserstack-service/src/cli/modules/automateModule.ts index 1a0b6d9..1a0aa04 100644 --- a/packages/browserstack-service/src/cli/modules/automateModule.ts +++ b/packages/browserstack-service/src/cli/modules/automateModule.ts @@ -195,16 +195,23 @@ export default class AutomateModule extends BaseModule { this.sessionMap.clear() } + // An App Automate session is identified by the service-level app / skipAppOverride flag, + // OR an app supplied only via the appium:app / appium:options.app capability — which the + // service-level config (this.config) does not carry. Mirrors accessibilityModule.isAppAutomateSession. + private isAppAutomate(): boolean { + if (this.config.app || isTrue(this.config.skipAppOverride)) { + return true + } + return this.hasAppCapInFrameworkState() + } + async markSessionName(sessionId: string, sessionName: string, config: { user: string; key: string; }): Promise { return await PerformanceTester.measureWrapper( PERFORMANCE_SDK_EVENTS.AUTOMATE_EVENTS.SESSION_NAME, async (sessionId: string, sessionName: string, config: { user: string; key: string; }) => { try { const auth = Buffer.from(`${config.user}:${config.key}`).toString('base64') - // skipAppOverride runs App Automate without an app value, so route session - // name/status to the App Automate endpoint on the flag too (config echoed from - // the binary carries skipAppOverride via the binconfig service options). - const isAppAutomate = this.config.app || isTrue(this.config.skipAppOverride) + const isAppAutomate = this.isAppAutomate() if (isAppAutomate) { this.logger.info('Marking session name for App Automate') } else { @@ -244,10 +251,7 @@ export default class AutomateModule extends BaseModule { async (sessionId: string, sessionStatus: 'passed' | 'failed', sessionErrorMessage: string | undefined, config: { user: string; key: string; }) => { try { const auth = Buffer.from(`${config.user}:${config.key}`).toString('base64') - // skipAppOverride runs App Automate without an app value, so route session - // name/status to the App Automate endpoint on the flag too (config echoed from - // the binary carries skipAppOverride via the binconfig service options). - const isAppAutomate = this.config.app || isTrue(this.config.skipAppOverride) + const isAppAutomate = this.isAppAutomate() if (isAppAutomate) { this.logger.info('Marking session status for App Automate') } else { diff --git a/packages/browserstack-service/src/cli/modules/baseModule.ts b/packages/browserstack-service/src/cli/modules/baseModule.ts index 75baf1c..8ca4ae8 100644 --- a/packages/browserstack-service/src/cli/modules/baseModule.ts +++ b/packages/browserstack-service/src/cli/modules/baseModule.ts @@ -1,5 +1,8 @@ import { BStackLogger } from '../cliLogger.js' import type { SDKClient } from '../../grpc/index.js' +import AutomationFramework from '../frameworks/automationFramework.js' +import { AutomationFrameworkConstants } from '../frameworks/constants/automationFrameworkConstants.js' +import { hasAppCap } from '../../util.js' /** * Base class for BrowserStack modules @@ -54,4 +57,13 @@ export default class BaseModule { BStackLogger.debug(`Configured module ${this.getModuleName()} with binSessionId=${binSessionId}, platformIndex=${platformIndex}`) } + + // Shared App Automate detection from the tracked framework capabilities: the app cap survives + // only in the raw INPUT capabilities (the resolved caps BrowserStack echoes back drop it), so + // check both. Reused by AutomateModule + PercyModule to keep endpoint/product routing consistent. + protected hasAppCapInFrameworkState(): boolean { + const autoInstance = AutomationFramework.getTrackedInstance() + return [AutomationFrameworkConstants.KEY_INPUT_CAPABILITIES, AutomationFrameworkConstants.KEY_CAPABILITIES] + .some(key => hasAppCap(AutomationFramework.getState(autoInstance, key) as WebdriverIO.Capabilities | undefined)) + } } diff --git a/packages/browserstack-service/src/cli/modules/percyModule.ts b/packages/browserstack-service/src/cli/modules/percyModule.ts index 6792bff..b50bd49 100644 --- a/packages/browserstack-service/src/cli/modules/percyModule.ts +++ b/packages/browserstack-service/src/cli/modules/percyModule.ts @@ -8,6 +8,7 @@ import { AutomationFrameworkState } from '../states/automationFrameworkState.js' import PercyHandler from '../../Percy/Percy-Handler.js' import type { Capabilities } from '@wdio/types' import { TestFrameworkConstants } from '../frameworks/constants/testFrameworkConstants.js' +import { hasAppCap, isTrue } from '../../util.js' import type TestFrameworkInstance from '../instances/testFrameworkInstance.js' export default class PercyModule extends BaseModule { @@ -35,6 +36,19 @@ export default class PercyModule extends BaseModule { return PercyModule.MODULE_NAME } + // App supplied via the service `app` option / skipAppOverride, OR via an appium:app-style driver + // capability (which this.config does not carry) — resolved from the caps via the shared hasAppCap + // detector so Percy routes to the App Automate screenshot path. Mirrors automateModule.isAppAutomate(). + private isAppAutomateSession(): boolean { + if ('app' in this.config || isTrue(this.config.skipAppOverride)) { + return true + } + if (hasAppCap(this.browser?.capabilities as WebdriverIO.Capabilities | undefined)) { + return true + } + return this.hasAppCapInFrameworkState() + } + async onAfterCreate(args: Record) { this.browser = args.browser as WebdriverIO.Browser @@ -46,7 +60,7 @@ export default class PercyModule extends BaseModule { this.logger.warn('PercyModule: Percy capture mode is not defined in the configuration, skipping Percy initialization') return } - this.isAppAutomate = this.isAppAutomate || 'app' in this.config + this.isAppAutomate = this.isAppAutomate || this.isAppAutomateSession() this.percyHandler = new PercyHandler( (this.percyConfig as Record).percyCaptureMode as string, this.browser, diff --git a/packages/browserstack-service/src/config.ts b/packages/browserstack-service/src/config.ts index c406770..8012e53 100644 --- a/packages/browserstack-service/src/config.ts +++ b/packages/browserstack-service/src/config.ts @@ -2,23 +2,9 @@ import type { AppConfig, BrowserstackConfig } from './types.js' import type { Capabilities, Options } from '@wdio/types' import { v4 as uuidv4 } from 'uuid' import TestOpsConfig from './testOps/testOpsConfig.js' -import { isTrue, isUndefined } from './util.js' +import { hasAppCap, isTrue, isUndefined } from './util.js' import { BStackLogger } from './bstackLogger.js' -const APP_AUTOMATE_CAP_KEYS = ['appium:app', 'appium:bundleId', 'appium:appPackage', 'appium:appActivity'] as const - -function hasAppCap(cap: WebdriverIO.Capabilities | undefined): boolean { - if (!cap || typeof cap !== 'object') { - return false - } - const record = cap as Record - if (APP_AUTOMATE_CAP_KEYS.some(key => !isUndefined(record[key]))) { - return true - } - const appiumOptions = record['appium:options'] as Record | undefined - return !!(appiumOptions && !isUndefined(appiumOptions.app)) -} - function detectAppAutomate(capabilities?: Capabilities.TestrunnerCapabilities): boolean { if (!capabilities) { return false diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 3d0254d..6213ee8 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -554,6 +554,24 @@ export const isAccessibilityAutomationSession = (accessibilityFlag?: boolean | s return false } +export const APP_AUTOMATE_CAP_KEYS = ['appium:app', 'appium:bundleId', 'appium:appPackage', 'appium:appActivity'] as const + +// An App Automate session is identified by an app capability on the driver caps +// (appium:app / appium:bundleId / appium:appPackage / appium:appActivity / appium:options.app). +// Shared by config-time detection and the CLI Automate/Percy modules so App-Automate +// endpoint routing stays consistent across every place that resolves it from capabilities. +export function hasAppCap(cap?: WebdriverIO.Capabilities): boolean { + if (!cap || typeof cap !== 'object') { + return false + } + const record = cap as Record + if (APP_AUTOMATE_CAP_KEYS.some(key => !isUndefined(record[key]))) { + return true + } + const appiumOptions = record['appium:options'] as Record | undefined + return !!(appiumOptions && !isUndefined(appiumOptions.app)) +} + export const isAppAccessibilityAutomationSession = (accessibilityFlag?: boolean | string, isAppAutomate?: boolean) => { const accessibilityAutomation = isAccessibilityAutomationSession(accessibilityFlag) return accessibilityAutomation && isAppAutomate diff --git a/packages/browserstack-service/tests/cli/modules/automateModule.test.ts b/packages/browserstack-service/tests/cli/modules/automateModule.test.ts index c423553..d3f47c4 100644 --- a/packages/browserstack-service/tests/cli/modules/automateModule.test.ts +++ b/packages/browserstack-service/tests/cli/modules/automateModule.test.ts @@ -36,7 +36,8 @@ vi.mock('../../../src/cli/cliLogger.js', () => ({ vi.mock('../../../src/util.js', () => ({ isBrowserstackSession: vi.fn(() => true), - isTrue: vi.fn((value) => (value + '').toLowerCase() === 'true') + isTrue: vi.fn((value) => (value + '').toLowerCase() === 'true'), + hasAppCap: vi.fn(() => false) })) vi.mock('../../../src/instrumentation/performance/performance-tester.js', () => ({ diff --git a/packages/browserstack-service/tests/cli/modules/percyModules.test.ts b/packages/browserstack-service/tests/cli/modules/percyModules.test.ts index 33faf58..7641e75 100644 --- a/packages/browserstack-service/tests/cli/modules/percyModules.test.ts +++ b/packages/browserstack-service/tests/cli/modules/percyModules.test.ts @@ -9,7 +9,9 @@ vi.mock('../../../src/cli/frameworks/testFramework.js', () => ({ vi.mock('../../../src/cli/frameworks/automationFramework.js', () => ({ default: { - registerObserver: vi.fn() + registerObserver: vi.fn(), + getTrackedInstance: vi.fn(), + getState: vi.fn() } }))