diff --git a/.changeset/pr-151.md b/.changeset/pr-151.md new file mode 100644 index 0000000..cf1e655 --- /dev/null +++ b/.changeset/pr-151.md @@ -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. diff --git a/.changeset/sdk-7075-honour-local-env-vars-v8.md b/.changeset/sdk-7075-honour-local-env-vars-v8.md new file mode 100644 index 0000000..9dca9d4 --- /dev/null +++ b/.changeset/sdk-7075-honour-local-env-vars-v8.md @@ -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. diff --git a/packages/browserstack-service/src/constants.ts b/packages/browserstack-service/src/constants.ts index 839033e..0f75ac9 100644 --- a/packages/browserstack-service/src/constants.ts +++ b/packages/browserstack-service/src/constants.ts @@ -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' diff --git a/packages/browserstack-service/src/launcher.ts b/packages/browserstack-service/src/launcher.ts index 4db272e..1181eb4 100644 --- a/packages/browserstack-service/src/launcher.ts +++ b/packages/browserstack-service/src/launcher.ts @@ -44,6 +44,7 @@ import { mergeChromeOptions, normalizeTestReportingConfig, normalizeTestReportingEnvVariables, + normalizeLocalEnvVariables, isValidEnabledValue, isMultiRemoteCaps, validateSkipAppOverride @@ -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 diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 11e1bdd..ce1d81f 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -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' @@ -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 diff --git a/packages/browserstack-service/tests/launcher.test.ts b/packages/browserstack-service/tests/launcher.test.ts index ec6b015..a01c969 100644 --- a/packages/browserstack-service/tests/launcher.test.ts +++ b/packages/browserstack-service/tests/launcher.test.ts @@ -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' @@ -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)