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
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/wdio-browserstack-service/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"formdata-node": "5.0.1",
"git-repo-info": "^2.1.1",
"gitconfiglocal": "^2.1.0",
"glob": "^10.3.10",
"got": "^12.6.1",
"tar": "^6.1.15",
"uuid": "^10.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class _AccessibilityHandler {
const caps = (this._browser as WebdriverIO.Browser).capabilities as WebdriverIO.Capabilities

this._platformA11yMeta = {
browser_name: caps.browserName,
browser_name: caps?.browserName,
browser_version: caps?.browserVersion || (caps as Capabilities.DesiredCapabilities)?.version || 'latest',
platform_name: caps?.platformName,
platform_version: this._getCapabilityValue(caps, 'appium:platformVersion', 'platformVersion'),
Expand Down
2 changes: 1 addition & 1 deletion packages/wdio-browserstack-service/src/insights-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class _InsightsHandler {
this._options = _options

this._platformMeta = {
browserName: caps.browserName,
browserName: caps?.browserName,
browserVersion: caps?.browserVersion,
platformName: caps?.platformName,
caps: caps,
Expand Down
9 changes: 7 additions & 2 deletions packages/wdio-browserstack-service/src/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ import {
mergeChromeOptions,
normalizeTestReportingConfig,
normalizeTestReportingEnvVariables,
isValidEnabledValue
isValidEnabledValue,
isMultiRemoteCaps
} from './util.js'
import { getProductMap } from './testHub/utils.js'
import CrashReporter from './crash-reporter.js'
Expand Down Expand Up @@ -278,7 +279,11 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
}

try {
if (CLIUtils.checkCLISupportedFrameworks(config.framework)) {
// Detect if multi-remote and disable CLI for those sessions
const isMultiremote = isMultiRemoteCaps(capabilities as Capabilities.RemoteCapabilities)
process.env.BROWSERSTACK_IS_MULTIREMOTE = String(isMultiremote)

if (CLIUtils.checkCLISupportedFrameworks(config.framework) && !isMultiremote) {
CLIUtils.setFrameworkDetail(WDIO_NAMING_PREFIX + config.framework, 'WebdriverIO') // TODO: make this constant
const binconfig = CLIUtils.getBinConfig(config, capabilities, this._options, this._buildTag)
await BrowserstackCLI.getInstance().bootstrap(this._options, config, binconfig)
Expand Down
3 changes: 2 additions & 1 deletion packages/wdio-browserstack-service/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ export default class BrowserstackService implements Services.ServiceInstance {
this._config.key = config.key

try {
if (CLIUtils.checkCLISupportedFrameworks(this._config.framework)) {
// Detect if multi-remote and disable CLI for those sessions
if (CLIUtils.checkCLISupportedFrameworks(this._config.framework) && process.env.BROWSERSTACK_IS_MULTIREMOTE !== 'true') {
// Connect to Browserstack CLI from worker
await BrowserstackCLI.getInstance().bootstrap(this._options, this._config)

Expand Down
43 changes: 42 additions & 1 deletion packages/wdio-browserstack-service/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,15 @@ export function getUniqueIdentifierForCucumber(world: ITestCaseHookParameter): s
}

export function getCloudProvider(browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser): string {
if (browser.options && browser.options.hostname && browser.options.hostname.includes('browserstack')) {
if (browser && 'instances' in browser) {
// Loop through all instances
for (const instanceName of browser.instances) {
const instance = (browser as any)[instanceName] as WebdriverIO.Browser
if (instance.options && instance.options.hostname && instance.options.hostname.includes('browserstack')) {
return 'browserstack'
}
}
} else if (browser.options && browser.options.hostname && browser.options.hostname.includes('browserstack')) { // Single browser instance
return 'browserstack'
}
return 'unknown_grid'
Expand Down Expand Up @@ -1833,3 +1841,36 @@ export function getMochaTestHierarchy(test: Frameworks.Test) {
}
return value.reverse()
}

/**
* Checks if the capabilities represent a multiremote configuration
* @param capabilities - The capabilities to check
* @returns true if capabilities represent any multiremote configuration (regular or parallel)
*
* @example
* Regular multiremote (object):
* { browserA: { capabilities: {...} }, browserB: { capabilities: {...} } }
*
* Parallel multiremote (array with nested structure):
* [{ browserA: { capabilities: {...} }, browserB: { capabilities: {...} } }]
*
* Regular capabilities (array):
* [{ browserName: 'chrome', ... }]
*/
export function isMultiRemoteCaps(capabilities: Capabilities.RemoteCapabilities): boolean {
// Regular multiremote is an object (not array)
if (!Array.isArray(capabilities)) {
return true
}

// Empty array is not multiremote
if (capabilities.length === 0) {
return false
}

// Parallel multiremote is an array with nested capabilities structure
return capabilities.every(cap =>
Object.values(cap).length > 0 &&
Object.values(cap).every(c => c !== null && typeof c === 'object' && (c as { capabilities: WebdriverIO.Capabilities }).capabilities)
)
}
108 changes: 106 additions & 2 deletions packages/wdio-browserstack-service/tests/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ import {
getAppA11yResults,
getAppA11yResultsSummary,
mergeDeep,
mergeChromeOptions
mergeChromeOptions,
isMultiRemoteCaps
} from '../src/util.js'
import * as bstackLogger from '../src/bstackLogger.js'
import { BROWSERSTACK_OBSERVABILITY, TESTOPS_BUILD_COMPLETED_ENV, BROWSERSTACK_TESTHUB_JWT, BROWSERSTACK_ACCESSIBILITY } from '../src/constants.js'
Expand Down Expand Up @@ -455,6 +456,16 @@ describe('getCloudProvider', () => {
it('return Browserstack if test being run on browserstack', () => {
expect(getCloudProvider({ options: { hostname: 'hub.browserstack.com' } })).toEqual('browserstack')
})
it('return Browserstack if test being run on browserstack with multiremote', () => {
const browser = {
isMultiremote: true,
instances: ['browserA'],
browserA: {
options: { hostname: 'hub.browserstack.com' }
}
} as unknown as WebdriverIO.MultiRemoteBrowser
expect(getCloudProvider(browser)).toEqual('browserstack')
})
})

describe('isBrowserstackSession', () => {
Expand Down Expand Up @@ -2052,4 +2063,97 @@ describe('mergeChromeOptions', () => {
newtab: 'https://newtab.com'
})
})
})
})

describe('isMultiRemoteCaps', () => {
it('should return true for regular multiremote capabilities (object)', () => {
const multiremoteCaps = {
browserA: {
capabilities: {
browserName: 'chrome'
}
},
browserB: {
capabilities: {
browserName: 'firefox'
}
}
}
expect(isMultiRemoteCaps(multiremoteCaps as any)).toBe(true)
})

it('should return true for parallel multiremote capabilities (array with nested structure)', () => {
const parallelMultiremoteCaps = [
{
browserA: {
capabilities: {
browserName: 'chrome'
}
},
browserB: {
capabilities: {
browserName: 'firefox'
}
}
}
]
expect(isMultiRemoteCaps(parallelMultiremoteCaps as any)).toBe(true)
})

it('should return false for regular capabilities array', () => {
const regularCaps = [
{
browserName: 'chrome',
'bstack:options': {
os: 'Windows'
}
}
]
expect(isMultiRemoteCaps(regularCaps as any)).toBe(false)
})

it('should return true for empty array', () => {
expect(isMultiRemoteCaps([] as any)).toBe(false)
})

it('should return false for array with mixed structure', () => {
const mixedCaps = [
{
browserA: {
capabilities: {
browserName: 'chrome'
}
}
},
{
browserName: 'firefox' // This is not multiremote structure
}
]
expect(isMultiRemoteCaps(mixedCaps as any)).toBe(false)
})

it('should return false for array with empty objects', () => {
const emptyCaps = [{}]
expect(isMultiRemoteCaps(emptyCaps as any)).toBe(false)
})

it('should handle array with objects containing non-capabilities properties', () => {
const invalidCaps = [
{
browserA: {
somethingElse: 'value' // Missing capabilities property
}
}
]
expect(isMultiRemoteCaps(invalidCaps as any)).toBe(false)
})

it('should return false for array with null values in nested structure', () => {
const capsWithNull = [
{
browserA: null
}
]
expect(isMultiRemoteCaps(capsWithNull as any)).toBe(false)
})
})