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

- Fixed `BROWSERSTACK_PROJECT_NAME` and `BROWSERSTACK_BUILD_NAME` being ignored — the environment now takes precedence over the project and build names in your config file, matching the other BrowserStack SDKs.
62 changes: 58 additions & 4 deletions packages/browserstack-service/src/launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,18 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
capability['browserstack.wdioService'] = BSTACK_SERVICE_VERSION
}
}
const envNames = this._applyEnvBuildNames(capability)

// Need this details for sending data to Test Reporting and Analytics
this._buildIdentifier = capability['browserstack.buildIdentifier']?.toString()
// @ts-expect-error ToDo: fix invalid cap
this._buildName = capability.build?.toString()
this._buildName = envNames.buildName ?? capability.build?.toString()
if (envNames.projectName) {
this._projectName = envNames.projectName
}
} else {
capability['bstack:options'].wdioService = BSTACK_SERVICE_VERSION
this._applyEnvBuildNames(capability)
this._buildName = capability['bstack:options'].buildName
this._projectName = capability['bstack:options'].projectName
this._buildTag = capability['bstack:options'].buildTag
Expand Down Expand Up @@ -197,10 +202,18 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
(caps.capabilities as WebdriverIO.Capabilities)['browserstack.wdioService'] = BSTACK_SERVICE_VERSION
}
}
const envNames = this._applyEnvBuildNames(caps.capabilities as WebdriverIO.Capabilities)
if (envNames.buildName) {
this._buildName = envNames.buildName
}
if (envNames.projectName) {
this._projectName = envNames.projectName
}
this._buildIdentifier = (caps.capabilities as WebdriverIO.Capabilities)['browserstack.buildIdentifier']
} else {
const bstackOptions = (caps.capabilities as WebdriverIO.Capabilities)['bstack:options']
bstackOptions!.wdioService = BSTACK_SERVICE_VERSION
this._applyEnvBuildNames(caps.capabilities as WebdriverIO.Capabilities)
this._buildName = bstackOptions!.buildName
this._projectName = bstackOptions!.projectName
this._buildTag = bstackOptions!.buildTag
Expand Down Expand Up @@ -241,6 +254,45 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
}
}

/**
* SDK-7075: BROWSERSTACK_PROJECT_NAME / BROWSERSTACK_BUILD_NAME take precedence over the
* config file, as every other SDK and the binary's own env->config map already do. They are
* written back into the caps so the session itself carries them: into bstack:options for W3C
* caps (including one this constructor just created), and into the top-level project/build
* keys that legacy caps name a build with - added on a BrowserStack session that has no
* bstack:options, otherwise only overwritten where already present. Blank values are ignored.
* Returns the values applied, for the Test Reporting fields - none for the caps of a
* non-BrowserStack session, which are left untouched so the reported names match the session.
*/
private _applyEnvBuildNames (capability: WebdriverIO.Capabilities): { projectName?: string, buildName?: string } {
const bstackOptions = capability['bstack:options']
if (!bstackOptions && !isBStackSession(this._config)) {
return {}
}
const projectName = process.env.BROWSERSTACK_PROJECT_NAME?.trim() || undefined
const buildName = process.env.BROWSERSTACK_BUILD_NAME?.trim() || undefined
if (bstackOptions) {
if (projectName) {
bstackOptions.projectName = projectName
}
if (buildName) {
bstackOptions.buildName = buildName
}
}
// Legacy caps name a build with top-level keys. Without bstack:options they are the only
// place the name can go; next to bstack:options, any the caps already carry are aligned
// too, so the session never sends two different names.
const legacyCaps = capability as Record<string, unknown>
const writeLegacy = (key: string) => !bstackOptions || key in legacyCaps
if (projectName && writeLegacy('project')) {
legacyCaps.project = projectName
}
if (buildName && writeLegacy('build')) {
legacyCaps.build = buildName
}
return { projectName, buildName }
}

@PerformanceTester.Measure(PERFORMANCE_SDK_EVENTS.EVENTS.SDK_SETUP)
async onWorkerStart (cid: string, caps: WebdriverIO.Capabilities) {
try {
Expand Down Expand Up @@ -1197,9 +1249,11 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
* A buildIdentifier is only meaningful next to a buildName - the dashboard appends it
* to that name. BROWSERSTACK_BUILD_NAME used to force this branch as well, which
* silently discarded every explicitly configured buildIdentifier whenever that env var
* happened to be exported (SDK-4748). This service never reads BROWSERSTACK_BUILD_NAME
* as a buildName source, and unlike the yml-driven SDKs it has no default identifier to
* suppress, so the env var no longer takes part in this decision.
* happened to be exported (SDK-4748). SDK-7075 then made BROWSERSTACK_BUILD_NAME a
* buildName source - the constructor folds it into the caps of a BrowserStack session, or
* of any caps carrying bstack:options - so the env var no longer suppresses the identifier;
* it supplies the very name the identifier is appended to. This branch therefore fires
* only when neither the caps nor (for those caps) the environment name a build.
*/
if (!this._buildName) {
this._updateCaps(capabilities, 'buildIdentifier')
Expand Down
165 changes: 155 additions & 10 deletions packages/browserstack-service/tests/launcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,146 @@ describe('constructor', () => {
delete process.env[RERUN_TESTS_ENV]
})

describe('BROWSERSTACK_PROJECT_NAME / BROWSERSTACK_BUILD_NAME (SDK-7075)', () => {
const setEnvNames = () => {
process.env.BROWSERSTACK_PROJECT_NAME = 'project from env'
process.env.BROWSERSTACK_BUILD_NAME = 'build from env'
}

afterEach(() => {
delete process.env.BROWSERSTACK_PROJECT_NAME
delete process.env.BROWSERSTACK_BUILD_NAME
})

it('should prefer the env vars over the values in the caps', async () => {
setEnvNames()
const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }]
const service: any = new BrowserstackLauncher(options as any, caps, config)

expect(caps).toEqual([
{ 'bstack:options': { wdioService: pkg.version, projectName: 'project from env', buildName: 'build from env' } }
])
expect(service._projectName).toEqual('project from env')
expect(service._buildName).toEqual('build from env')
})

it('should keep the caps projectName / buildName when the env vars are not set', async () => {
const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }]
new BrowserstackLauncher(options as any, caps, config)

expect(caps).toEqual([
{ 'bstack:options': { wdioService: pkg.version, projectName: 'project from config', buildName: 'build from config' } }
])
})

it('should ignore blank env values', async () => {
process.env.BROWSERSTACK_PROJECT_NAME = ' '
process.env.BROWSERSTACK_BUILD_NAME = ''
const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }]
new BrowserstackLauncher(options as any, caps, config)

expect(caps[0]['bstack:options']).toMatchObject({ projectName: 'project from config', buildName: 'build from config' })
})

it('should apply the env vars to the bstack:options the service creates for extension caps', async () => {
setEnvNames()
const caps: any = [{ 'goog:chromeOptions': {} }]
const service: any = new BrowserstackLauncher(options as any, caps, config)

expect(caps).toEqual([{
'goog:chromeOptions': {},
'bstack:options': { wdioService: pkg.version, projectName: 'project from env', buildName: 'build from env' }
}])
expect(service._projectName).toEqual('project from env')
expect(service._buildName).toEqual('build from env')
})

it('should apply the env vars to the top-level project / build keys of legacy caps', async () => {
setEnvNames()
const caps: any = [{ project: 'project from config', build: 'build from config' }]
const service: any = new BrowserstackLauncher(options as any, caps, config)

expect(caps).toEqual([{
'browserstack.wdioService': pkg.version,
project: 'project from env',
build: 'build from env'
}])
expect(service._projectName).toEqual('project from env')
expect(service._buildName).toEqual('build from env')
})

it('should align top-level project / build keys that sit next to bstack:options', async () => {
setEnvNames()
const caps: any = [
{ 'bstack:options': { buildName: 'build from config' }, project: 'project from config', build: 'build from config' },
{ 'bstack:options': {} }
]
new BrowserstackLauncher(options as any, caps, config)

expect(caps[0]).toEqual({
'bstack:options': { wdioService: pkg.version, projectName: 'project from env', buildName: 'build from env' },
project: 'project from env',
build: 'build from env'
})
expect(caps[1]).toEqual({
'bstack:options': { wdioService: pkg.version, projectName: 'project from env', buildName: 'build from env' }
})
})

it('should prefer the env vars for an object of capabilities', async () => {
setEnvNames()
const caps: any = { browserA: { capabilities: { 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } } } }
new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

expect(caps).toEqual({ 'browserA': { 'capabilities': { 'bstack:options': { 'wdioService': pkg.version, 'projectName': 'project from env', 'buildName': 'build from env' } } } })
})

it('should apply the env vars for an object of capabilities without bstack:options', async () => {
setEnvNames()
const caps: any = { browserA: { capabilities: { 'goog:chromeOptions': {} } }, browserB: { capabilities: {} } }
const service: any = new BrowserstackLauncher(options as BrowserstackConfig & Options.Testrunner, caps, config)

expect(caps.browserA.capabilities['bstack:options']).toEqual({ wdioService: pkg.version, projectName: 'project from env', buildName: 'build from env' })
expect(caps.browserB.capabilities).toMatchObject({ project: 'project from env', build: 'build from env' })
expect(service._projectName).toEqual('project from env')
expect(service._buildName).toEqual('build from env')
})

it('should leave a non-BrowserStack session untouched', async () => {
setEnvNames()
const spy = vi.spyOn(utils, 'isBStackSession').mockReturnValue(false)
try {
const caps: any = [{ build: 'build from config' }]
const service: any = new BrowserstackLauncher(options as any, caps, config)

expect(caps).toEqual([{ build: 'build from config' }])
expect(service._buildName).toEqual('build from config')
expect(service._projectName).toBeUndefined()
} finally {
spy.mockRestore()
}
})

it('should trim padded env values', async () => {
process.env.BROWSERSTACK_PROJECT_NAME = ' project from env '
process.env.BROWSERSTACK_BUILD_NAME = ' build from env '
const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }]
new BrowserstackLauncher(options as any, caps, config)

expect(caps[0]['bstack:options']).toMatchObject({ projectName: 'project from env', buildName: 'build from env' })
})

it('should override only the name the env sets', async () => {
process.env.BROWSERSTACK_BUILD_NAME = 'build from env'
const caps: any = [{ 'bstack:options': { projectName: 'project from config', buildName: 'build from config' } }]
const service: any = new BrowserstackLauncher(options as any, caps, config)

expect(caps[0]['bstack:options']).toMatchObject({ projectName: 'project from config', buildName: 'build from env' })
expect(service._projectName).toEqual('project from config')
expect(service._buildName).toEqual('build from env')
})
})

describe('#non-bstack session', () => {
const spy = vi.spyOn(utils, 'isBStackSession')
it('should not add service version to caps', async () => {
Expand Down Expand Up @@ -1394,8 +1534,8 @@ describe('_handleBuildIdentifier', () => {

/**
* SDK-4748: BROWSERSTACK_BUILD_NAME used to delete an explicitly configured
* buildIdentifier outright. It is not a buildName source for this service, so it must
* not influence the identifier at all once a buildName is present in the caps.
* buildIdentifier outright. Since SDK-7075 it is a buildName source, so it must not
* suppress the identifier - it names the build the identifier is appended to.
*/
it('should keep buildIdentifier when BROWSERSTACK_BUILD_NAME is defined as env var and buildName is in caps', async() => {
process.env.BROWSERSTACK_BUILD_NAME = 'browserstack wdio build'
Expand Down Expand Up @@ -1428,23 +1568,28 @@ describe('_handleBuildIdentifier', () => {
expect(caps[0]['bstack:options']?.buildIdentifier).toEqual('2026-09-27_14-35-36')
})

it('should still delete buildIdentifier if buildName is absent and BROWSERSTACK_BUILD_NAME is defined as env var', async() => {
/**
* SDK-7075: the constructor now folds BROWSERSTACK_BUILD_NAME into the caps, so a buildName
* IS present even when the caps themselves carried none, and the identifier is appended to
* that env-sourced name instead of being dropped. A buildName counts as absent only when
* neither the caps nor the environment supply one - see the BUILD_RUN_IDENTIFIER test below.
*/
it('should resolve buildIdentifier against the env buildName when the caps carry no buildName', async() => {
process.env.BROWSERSTACK_BUILD_NAME = 'browserstack wdio build'
const caps: any = [{
'bstack:options': {
buildIdentifier: '#${BUILD_NUMBER}'
}
}]
const updatedcaps: any = [{
'bstack:options': {
wdioService: pkg.version
}
}]
const service = new BrowserstackLauncher(options as any, caps, config)

vi.spyOn(utils, 'getCiInfo').mockReturnValueOnce(null)
vi.spyOn(service, '_getLocalBuildNumber').mockReturnValueOnce('1')
vi.spyOn(service, '_updateLocalBuildCache').mockImplementation(() => {})
service._handleBuildIdentifier(caps)
expect(caps[0]).toMatchObject(updatedcaps[0])
expect(caps[0]['bstack:options']?.buildIdentifier).toBeUndefined()

expect(caps[0]['bstack:options']?.buildName).toEqual('browserstack wdio build')
expect(caps[0]['bstack:options']?.buildIdentifier).toEqual('#1')
})

it('should prefer BROWSERSTACK_BUILD_RUN_IDENTIFIER over the configured buildIdentifier', async() => {
Expand Down
Loading