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-151.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wdio/browserstack-service": patch
---

- Fixed `BROWSERSTACK_LOCAL` and `BROWSERSTACK_LOCAL_IDENTIFIER` being ignored. Both env vars now configure BrowserStack Local — the tunnel is launched and the `local` / `localIdentifier` capabilities reach the session — matching the other BrowserStack SDKs.
5 changes: 5 additions & 0 deletions .changeset/sdk-7075-honour-local-env-vars-v8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wdio/browserstack-service": patch
---

- Fixed `BROWSERSTACK_LOCAL` and `BROWSERSTACK_LOCAL_IDENTIFIER` being ignored. Both env vars now configure BrowserStack Local — the tunnel is launched and the `local` / `localIdentifier` capabilities reach the session — and take precedence over `browserstackLocal` / `opts.localIdentifier` in `wdio.conf.js`, matching the other BrowserStack SDKs. An identifier on its own still does not enable Local.
6 changes: 6 additions & 0 deletions packages/browserstack-service/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,12 @@ export const BSTACK_A11Y_POLLING_TIMEOUT = 'BSTACK_A11Y_POLLING_TIMEOUT'
// Whether session is a accessibility session
export const BROWSERSTACK_ACCESSIBILITY = 'BROWSERSTACK_ACCESSIBILITY'

// Whether to route the session through a BrowserStack Local tunnel
export const BROWSERSTACK_LOCAL = 'BROWSERSTACK_LOCAL'

// Identifier of the BrowserStack Local tunnel to bind the session to
export const BROWSERSTACK_LOCAL_IDENTIFIER = 'BROWSERSTACK_LOCAL_IDENTIFIER'

// Whether session is a test reporting session (new name)
export const BROWSERSTACK_TEST_REPORTING = 'BROWSERSTACK_TEST_REPORTING'

Expand Down
4 changes: 4 additions & 0 deletions packages/browserstack-service/src/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
mergeChromeOptions,
normalizeTestReportingConfig,
normalizeTestReportingEnvVariables,
normalizeLocalEnvVariables,
isValidEnabledValue,
isMultiRemoteCaps,
validateSkipAppOverride
Expand Down Expand Up @@ -98,6 +99,9 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
normalizeTestReportingConfig(this._options)

normalizeTestReportingEnvVariables()

//normalizing BrowserStack Local config from env variables
normalizeLocalEnvVariables(this._options)
this.browserStackConfig = BrowserStackConfig.getInstance(_options, _config)
if (Array.isArray(capabilities)) {
capabilities
Expand Down
31 changes: 30 additions & 1 deletion packages/browserstack-service/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ import {
APP_ALLY_ISSUES_ENDPOINT,
TEST_REPORTING_PROJECT_NAME,
CLI_DEBUG_LOGS_FILE,
WDIO_NAMING_PREFIX
WDIO_NAMING_PREFIX,
BROWSERSTACK_LOCAL,
BROWSERSTACK_LOCAL_IDENTIFIER
} from './constants.js'
import CrashReporter from './crash-reporter.js'
import { BStackLogger } from './bstackLogger.js'
Expand Down Expand Up @@ -1289,6 +1291,33 @@ export function normalizeTestReportingEnvVariables(){

}

/**
* Resolve BrowserStack Local settings from the environment onto the service options.
*
* `BROWSERSTACK_LOCAL` / `BROWSERSTACK_LOCAL_IDENTIFIER` are the SDK-wide env vars for Local
* (the `browserstackLocal` / `localIdentifier` entries of the binary's EnvCapsMapping). This
* service reads Local purely off the `wdio.conf.js` service options, so without this the env
* vars were silently dropped — no tunnel was launched and no `local` / `localIdentifier`
* capability reached the session (SDK-7075).
*
* The env var wins over `wdio.conf.js`, matching the binary's `updateConfigWithEnvVars` and
* `getObservabilityUser` / `getObservabilityKey` / `getObservabilityProject` below.
*/
export function normalizeLocalEnvVariables(_options: BrowserstackConfig & Options.Testrunner) {
if (!isUndefined(process.env[BROWSERSTACK_LOCAL])) {
_options.browserstackLocal = isTrue(process.env[BROWSERSTACK_LOCAL])
}

/**
* An identifier on its own must not turn Local on — enablement keys off `browserstackLocal`
* alone, the same way `getLocalConfig()` does in the binary. A stale
* `BROWSERSTACK_LOCAL_IDENTIFIER` left in a CI environment therefore stays inert.
*/
if (!isUndefined(process.env[BROWSERSTACK_LOCAL_IDENTIFIER])) {
_options.opts = { ..._options.opts, localIdentifier: process.env[BROWSERSTACK_LOCAL_IDENTIFIER] }
}
}

export function getObservabilityUser(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner) {
if (process.env.BROWSERSTACK_USERNAME) {
return process.env.BROWSERSTACK_USERNAME
Expand Down
132 changes: 131 additions & 1 deletion packages/browserstack-service/tests/launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'

import { describe, expect, it, vi, beforeEach } from 'vitest'
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
// @ts-expect-error mock feature
import { Local, mockStart } from 'browserstack-local'
import got from 'got'
Expand Down Expand Up @@ -617,6 +617,136 @@ describe('onPrepare', () => {
})
})

describe('onPrepare with BrowserStack Local env variables (SDK-7075)', () => {
const caps: any = [{}]
const config = {
user: 'foobaruser',
key: '12345678901234567890',
capabilities: []
}
vi.spyOn(utils, 'launchTestSession').mockImplementation(() => {})
vi.spyOn(utils, 'isBStackSession').mockImplementation(() => {return true})

beforeEach(() => {
delete process.env.BROWSERSTACK_LOCAL
delete process.env.BROWSERSTACK_LOCAL_IDENTIFIER
})

afterEach(() => {
delete process.env.BROWSERSTACK_LOCAL
delete process.env.BROWSERSTACK_LOCAL_IDENTIFIER
})

it('should start Local when BROWSERSTACK_LOCAL is set and the config says nothing', async () => {
process.env.BROWSERSTACK_LOCAL = 'true'
const service = new BrowserstackLauncher({ testObservability: false, percy: false } as any, caps, config)
const capabilities = [{ 'bstack:options': {} }]

await service.onPrepare(config, capabilities)

expect(service.browserstackLocal).toBeDefined()
expect(capabilities[0]['bstack:options']).toEqual({ local: true, 'testhubBuildUuid': buildHashedId, 'buildProductMap': productMap })
})

it('should add the "localIdentifier" capability from BROWSERSTACK_LOCAL_IDENTIFIER', async () => {
process.env.BROWSERSTACK_LOCAL = 'true'
process.env.BROWSERSTACK_LOCAL_IDENTIFIER = 'local_identifier_TFnBzD'
const service = new BrowserstackLauncher({ testObservability: false, percy: false } as any, caps, config)
const capabilities = [{ 'bstack:options': {} }]

await service.onPrepare(config, capabilities)

expect(service.browserstackLocal).toBeDefined()
expect(capabilities[0]['bstack:options']).toEqual({ local: true, localIdentifier: 'local_identifier_TFnBzD', 'testhubBuildUuid': buildHashedId, 'buildProductMap': productMap })
})

it('should add the "browserstack.localIdentifier" capability when no "bstack:options" is present', async () => {
process.env.BROWSERSTACK_LOCAL = 'true'
process.env.BROWSERSTACK_LOCAL_IDENTIFIER = 'local_identifier_TFnBzD'
const service = new BrowserstackLauncher({ testObservability: false, percy: false } as any, caps, config)
const capabilities = [{}]

await service.onPrepare(config, capabilities)

expect(capabilities[0]).toEqual({ 'browserstack.local': true, 'browserstack.localIdentifier': 'local_identifier_TFnBzD', 'browserstack.testhubBuildUuid': buildHashedId, 'browserstack.buildProductMap': productMap })
})

it('should pass the identifier from BROWSERSTACK_LOCAL_IDENTIFIER to the Local binary', async () => {
process.env.BROWSERSTACK_LOCAL = 'true'
process.env.BROWSERSTACK_LOCAL_IDENTIFIER = 'local_identifier_TFnBzD'
const service = new BrowserstackLauncher({ testObservability: false, percy: false } as any, caps, config)

await service.onPrepare(config, [{ 'bstack:options': {} }])

expect(mockStart).toHaveBeenCalledWith(
expect.objectContaining({ localIdentifier: 'local_identifier_TFnBzD' }),
expect.anything()
)
})

it('should let BROWSERSTACK_LOCAL override browserstackLocal from the config', async () => {
process.env.BROWSERSTACK_LOCAL = 'true'
const service = new BrowserstackLauncher({ browserstackLocal: false, testObservability: false, percy: false } as any, caps, config)

await service.onPrepare(config, [{ 'bstack:options': {} }])

expect(service.browserstackLocal).toBeDefined()
})

it('should let BROWSERSTACK_LOCAL=false override browserstackLocal from the config', async () => {
process.env.BROWSERSTACK_LOCAL = 'false'
const service = new BrowserstackLauncher({ browserstackLocal: true, testObservability: false, percy: false } as any, caps, config)
const capabilities = [{ 'bstack:options': {} }]

await service.onPrepare(config, capabilities)

expect(service.browserstackLocal).toBeUndefined()
expect(capabilities[0]['bstack:options']).not.toHaveProperty('local')
})

it('should let BROWSERSTACK_LOCAL_IDENTIFIER override opts.localIdentifier from the config', async () => {
process.env.BROWSERSTACK_LOCAL = 'true'
process.env.BROWSERSTACK_LOCAL_IDENTIFIER = 'from-env'
const service = new BrowserstackLauncher({
browserstackLocal: true,
opts: { localIdentifier: 'from-config' },
testObservability: false,
percy: false
} as any, caps, config)
const capabilities = [{ 'bstack:options': {} }]

await service.onPrepare(config, capabilities)

expect(capabilities[0]['bstack:options']).toEqual({ local: true, localIdentifier: 'from-env', 'testhubBuildUuid': buildHashedId, 'buildProductMap': productMap })
})

it('should not enable Local from BROWSERSTACK_LOCAL_IDENTIFIER alone', async () => {
process.env.BROWSERSTACK_LOCAL_IDENTIFIER = 'local_identifier_TFnBzD'
const service = new BrowserstackLauncher({ testObservability: false, percy: false } as any, caps, config)
const capabilities = [{ 'bstack:options': {} }]

await service.onPrepare(config, capabilities)

expect(service.browserstackLocal).toBeUndefined()
expect(capabilities[0]['bstack:options']).not.toHaveProperty('localIdentifier')
})

it('should keep honouring the config when no Local env variable is set', async () => {
const service = new BrowserstackLauncher({
browserstackLocal: true,
opts: { localIdentifier: 'from-config' },
testObservability: false,
percy: false
} as any, caps, config)
const capabilities = [{ 'bstack:options': {} }]

await service.onPrepare(config, capabilities)

expect(service.browserstackLocal).toBeDefined()
expect(capabilities[0]['bstack:options']).toEqual({ local: true, localIdentifier: 'from-config', 'testhubBuildUuid': buildHashedId, 'buildProductMap': productMap })
})
})

describe('onComplete', () => {
it('should do nothing if browserstack local is turned on, but not running', () => {
const service = new BrowserstackLauncher({} as any, [{}] as any, {} as any)
Expand Down
Loading