Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pr-100.md
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 17 additions & 9 deletions packages/browserstack-service/src/cli/modules/automateModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { HookState } from '../states/hookState.js'
import type { Frameworks, Options } from '@wdio/types'
import AutomationFramework from '../frameworks/automationFramework.js'
import { AutomationFrameworkConstants } from '../frameworks/constants/automationFrameworkConstants.js'
import { isBrowserstackSession, isTrue } from '../../util.js'
import { hasAppCap, isBrowserstackSession, isTrue } from '../../util.js'
import type TestFrameworkInstance from '../instances/testFrameworkInstance.js'
import { TestFrameworkConstants } from '../frameworks/constants/testFrameworkConstants.js'
import PerformanceTester from '../../instrumentation/performance/performance-tester.js'
Expand Down Expand Up @@ -195,16 +195,27 @@ 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
}
// The app capability survives only in the raw INPUT capabilities; the resolved
// capabilities BrowserStack echoes back drop it. Check both, via the shared hasAppCap detector.
const autoInstance = AutomationFramework.getTrackedInstance()
return [AutomationFrameworkConstants.KEY_INPUT_CAPABILITIES, AutomationFrameworkConstants.KEY_CAPABILITIES]
.some(key => hasAppCap(AutomationFramework.getState(autoInstance, key) as WebdriverIO.Capabilities | undefined))
Comment on lines +207 to +209

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const autoInstance = AutomationFramework.getTrackedInstance()
return [AutomationFrameworkConstants.KEY_INPUT_CAPABILITIES, AutomationFrameworkConstants.KEY_CAPABILITIES]
.some(key => hasAppCap(AutomationFramework.getState(autoInstance, key) as WebdriverIO.Capabilities | undefined))
return hasAppCapInFrameworkState()

}

async markSessionName(sessionId: string, sessionName: string, config: { user: string; key: string; }): Promise<void> {
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 {
Expand Down Expand Up @@ -244,10 +255,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 {
Expand Down
19 changes: 18 additions & 1 deletion packages/browserstack-service/src/cli/modules/percyModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ 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 { AutomationFrameworkConstants } from '../frameworks/constants/automationFrameworkConstants.js'
import { hasAppCap, isTrue } from '../../util.js'
import type TestFrameworkInstance from '../instances/testFrameworkInstance.js'

export default class PercyModule extends BaseModule {
Expand Down Expand Up @@ -35,6 +37,21 @@ 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
}
const autoInstance = AutomationFramework.getTrackedInstance()
return [AutomationFrameworkConstants.KEY_INPUT_CAPABILITIES, AutomationFrameworkConstants.KEY_CAPABILITIES]
.some(key => hasAppCap(AutomationFramework.getState(autoInstance, key) as WebdriverIO.Capabilities | undefined))
Comment on lines +50 to +52

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const autoInstance = AutomationFramework.getTrackedInstance()
return [AutomationFrameworkConstants.KEY_INPUT_CAPABILITIES, AutomationFrameworkConstants.KEY_CAPABILITIES]
.some(key => hasAppCap(AutomationFramework.getState(autoInstance, key) as WebdriverIO.Capabilities | undefined))
return hasAppCapInFrameworkState()

}

async onAfterCreate(args: Record<string, unknown>) {
this.browser = args.browser as WebdriverIO.Browser

Expand All @@ -46,7 +63,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<string, unknown>).percyCaptureMode as string,
this.browser,
Expand Down
16 changes: 1 addition & 15 deletions packages/browserstack-service/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>
if (APP_AUTOMATE_CAP_KEYS.some(key => !isUndefined(record[key]))) {
return true
}
const appiumOptions = record['appium:options'] as Record<string, unknown> | undefined
return !!(appiumOptions && !isUndefined(appiumOptions.app))
}

function detectAppAutomate(capabilities?: Capabilities.TestrunnerCapabilities): boolean {
if (!capabilities) {
return false
Expand Down
18 changes: 18 additions & 0 deletions packages/browserstack-service/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>
if (APP_AUTOMATE_CAP_KEYS.some(key => !isUndefined(record[key]))) {
return true
}
const appiumOptions = record['appium:options'] as Record<string, unknown> | undefined
return !!(appiumOptions && !isUndefined(appiumOptions.app))
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can export another function here:

Suggested change
import AutomationFramework from './cli/frameworks/automationFramework.js'
import { AutomationFrameworkConstants } from './cli/frameworks/constants/automationFrameworkConstants.js'
export function hasAppCapInFrameworkState(): boolean {
const autoInstance = AutomationFramework.getTrackedInstance()
const keys = [
AutomationFrameworkConstants.KEY_INPUT_CAPABILITIES,
AutomationFrameworkConstants.KEY_CAPABILITIES
]
return keys.some(key =>
hasAppCap(AutomationFramework.getState(autoInstance, key) as WebdriverIO.Capabilities | undefined)
)
}

export const isAppAccessibilityAutomationSession = (accessibilityFlag?: boolean | string, isAppAutomate?: boolean) => {
const accessibilityAutomation = isAccessibilityAutomationSession(accessibilityFlag)
return accessibilityAutomation && isAppAutomate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}))

Expand Down
Loading