From 44119d69c0fc09b05aee7efa43f6d82c875dbc48 Mon Sep 17 00:00:00 2001 From: rounak bhatia Date: Wed, 14 Oct 2026 17:04:06 +0530 Subject: [PATCH 1/3] feat(cli): forward every testManagementOptions key to the binary (SDK-7757) getBinConfig rebuilt testManagementOptions with testPlanId only, so issues, issueTracker and any other key set in the service options never reached the binary, which now sends the merged object as test_management_options at build start. Pass the object through as configured, still trimming testPlanId and dropping it when blank. Type issues and issueTracker and allow other keys. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../browserstack-service/src/cli/cliUtils.ts | 27 +++++--- packages/browserstack-service/src/types.ts | 34 +++++++++- .../tests/cli/cliUtils.test.ts | 63 ++++++++++++++----- 3 files changed, 99 insertions(+), 25 deletions(-) diff --git a/packages/browserstack-service/src/cli/cliUtils.ts b/packages/browserstack-service/src/cli/cliUtils.ts index 9944df42..3a3a5f72 100644 --- a/packages/browserstack-service/src/cli/cliUtils.ts +++ b/packages/browserstack-service/src/cli/cliUtils.ts @@ -89,10 +89,7 @@ export class CLIUtils { const isNonBstackA11y = isTurboScale(options) || !shouldAddServiceVersion(config as Options.Testrunner, options.testObservability) const observabilityOptions: TestObservabilityOptions = options.testObservabilityOptions || {} - const testManagementOptions: TestManagementOptions = options.testManagementOptions || {} - const testPlanId = typeof testManagementOptions.testPlanId === 'string' - ? testManagementOptions.testPlanId.trim() - : '' + const testManagementOptions = this.getTestManagementOptions(options.testManagementOptions) const binconfig: Record = { userName: observabilityOptions.user || config.user, accessKey: observabilityOptions.key || config.key, @@ -105,10 +102,8 @@ export class CLIUtils { binconfig.buildName = observabilityOptions.buildName || binconfig.buildName binconfig.projectName = observabilityOptions.projectName || binconfig.projectName binconfig.buildTag = this.getObservabilityBuildTags(observabilityOptions, buildTag) || [] - if (testPlanId.length > 0) { - binconfig.testManagementOptions = { - testPlanId - } + if (testManagementOptions) { + binconfig.testManagementOptions = testManagementOptions } let caps = capabilities @@ -142,6 +137,22 @@ export class CLIUtils { return JSON.stringify(binconfig) } + /** + * testManagementOptions forwarded to the binary as configured, with testPlanId trimmed and + * dropped when blank; undefined when nothing is left to send. + */ + static getTestManagementOptions(testManagementOptions?: TestManagementOptions): TestManagementOptions | undefined { + if (!testManagementOptions || typeof testManagementOptions !== 'object' || Array.isArray(testManagementOptions)) { + return undefined + } + const { testPlanId, ...otherOptions } = testManagementOptions + const trimmedTestPlanId = typeof testPlanId === 'string' ? testPlanId.trim() : '' + const resolved: TestManagementOptions = trimmedTestPlanId.length > 0 + ? { testPlanId: trimmedTestPlanId, ...otherOptions } + : otherOptions + return Object.keys(resolved).length > 0 ? resolved : undefined + } + static getSdkVersion() { return bstackServiceVersion } diff --git a/packages/browserstack-service/src/types.ts b/packages/browserstack-service/src/types.ts index 7c9e45d8..af270fdb 100644 --- a/packages/browserstack-service/src/types.ts +++ b/packages/browserstack-service/src/types.ts @@ -57,8 +57,32 @@ export interface TestReportingOptions { key?: string } +export interface IssueTrackerOptions { + /** + * Issue tracker the linked issues live in, e.g. 'jira'. + */ + name?: string, + /** + * Base URL of the issue tracker, e.g. 'https://your-domain.atlassian.net'. + */ + host?: string, + [key: string]: unknown +} + export interface TestManagementOptions { + /** + * Test plan to add this run's test cases to. + */ testPlanId?: string, + /** + * Issue keys to link to the build, e.g. ['XYZ-1', 'XYZ-2']. + */ + issues?: string[], + /** + * Issue tracker that `issues` belong to. + */ + issueTracker?: IssueTrackerOptions, + [key: string]: unknown } export interface RunSmartSelectionOptions { @@ -110,8 +134,14 @@ export interface BrowserstackConfig { */ testReportingOptions?: TestReportingOptions; /** - * Set the Test Management related config options under this key. - * Currently supports testPlanId. + * Set the Test Management related config options under this key, e.g. testPlanId, issues + * and issueTracker. + * + * For Mocha and Cucumber runs every key is forwarded to Test Management as is, and the same + * JSON object can also be passed through the BROWSERSTACK_TEST_MANAGEMENT_OPTIONS environment + * variable or the --browserstack.testManagementOptions CLI argument; each top-level key is + * taken from the CLI argument first, then the environment variable, then this option. + * Other runs use testPlanId only. */ testManagementOptions?: TestManagementOptions; /** diff --git a/packages/browserstack-service/tests/cli/cliUtils.test.ts b/packages/browserstack-service/tests/cli/cliUtils.test.ts index 32bab846..d9d2573e 100644 --- a/packages/browserstack-service/tests/cli/cliUtils.test.ts +++ b/packages/browserstack-service/tests/cli/cliUtils.test.ts @@ -195,34 +195,67 @@ describe('CLIUtils', () => { }) }) - it('omits empty testManagementOptions and strips unrelated keys', () => { + it('passes every testManagementOptions key through with a trimmed testPlanId', () => { const capabilities = [ { browserName: 'chrome' } ] - const emptyPlanOptions = createBrowserstackOptions({ + const options = createBrowserstackOptions({ testManagementOptions: { - testPlanId: ' ', - ignoredKey: 'ignored' + testPlanId: ' tm-plan-456 ', + issues: ['XYZ-1', 'XYZ-2'], + issueTracker: { name: 'jira', host: 'https://demo.atlassian.net' }, + customKey: { nested: true } } }) - const validPlanOptions = createBrowserstackOptions({ + + const parsed = JSON.parse(CLIUtils.getBinConfig(mockConfig, capabilities, options)) + + expect(parsed.testManagementOptions).toEqual({ + testPlanId: 'tm-plan-456', + issues: ['XYZ-1', 'XYZ-2'], + issueTracker: { name: 'jira', host: 'https://demo.atlassian.net' }, + customKey: { nested: true } + }) + }) + + it('drops a blank testPlanId but keeps the other testManagementOptions keys', () => { + const capabilities = [ + { browserName: 'chrome' } + ] + const options = createBrowserstackOptions({ testManagementOptions: { - testPlanId: ' tm-plan-456 ', - ignoredKey: 'ignored' + testPlanId: ' ', + issues: ['XYZ-1'] } }) - const emptyPlanResult = CLIUtils.getBinConfig(mockConfig, capabilities, emptyPlanOptions) - - const emptyPlanParsed = JSON.parse(emptyPlanResult) - expect(emptyPlanParsed.testManagementOptions).toBeUndefined() + const parsed = JSON.parse(CLIUtils.getBinConfig(mockConfig, capabilities, options)) - const validPlanResult = CLIUtils.getBinConfig(mockConfig, capabilities, validPlanOptions) + expect(parsed.testManagementOptions).toEqual({ + issues: ['XYZ-1'] + }) + }) - const validPlanParsed = JSON.parse(validPlanResult) - expect(validPlanParsed.testManagementOptions).toEqual({ - testPlanId: 'tm-plan-456' + it('omits testManagementOptions when nothing is left to send', () => { + const capabilities = [ + { browserName: 'chrome' } + ] + const blankPlanOptions = createBrowserstackOptions({ + testManagementOptions: { + testPlanId: ' ' + } + }) + const emptyOptions = createBrowserstackOptions({ + testManagementOptions: {} }) + const nonObjectOptions = createBrowserstackOptions({ + testManagementOptions: 'not-an-object' + }) + + for (const options of [blankPlanOptions, emptyOptions, nonObjectOptions]) { + const parsed = JSON.parse(CLIUtils.getBinConfig(mockConfig, capabilities, options)) + expect(parsed.testManagementOptions).toBeUndefined() + } }) }) From 8ff1b20ef3f5fb4f2c44db8b9cbe0482fc13c633 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 12:07:30 +0000 Subject: [PATCH 2/3] chore(changeset): auto-generate from PR template (minor) --- .changeset/pr-286.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pr-286.md diff --git a/.changeset/pr-286.md b/.changeset/pr-286.md new file mode 100644 index 00000000..aecbe5cc --- /dev/null +++ b/.changeset/pr-286.md @@ -0,0 +1,5 @@ +--- +"@wdio/browserstack-service": minor +--- + +- `testManagementOptions` set in the BrowserStack service options is now forwarded in full, so `issues` and `issueTracker` can link Jira issues to the build. The same JSON can be passed with the `BROWSERSTACK_TEST_MANAGEMENT_OPTIONS` environment variable or the `--browserstack.testManagementOptions` CLI argument (Mocha and Cucumber). From d2d1139884b86ff781ac159957b7bfee4da4f7c6 Mon Sep 17 00:00:00 2001 From: rounak bhatia Date: Wed, 14 Oct 2026 18:54:09 +0530 Subject: [PATCH 3/3] test(SDK-7757): remove the added unit tests Co-Authored-By: Claude Opus 5.5 (1M context) --- .../tests/cli/cliUtils.test.ts | 64 +++++-------------- 1 file changed, 17 insertions(+), 47 deletions(-) diff --git a/packages/browserstack-service/tests/cli/cliUtils.test.ts b/packages/browserstack-service/tests/cli/cliUtils.test.ts index d9d2573e..987e6b3a 100644 --- a/packages/browserstack-service/tests/cli/cliUtils.test.ts +++ b/packages/browserstack-service/tests/cli/cliUtils.test.ts @@ -195,67 +195,37 @@ describe('CLIUtils', () => { }) }) - it('passes every testManagementOptions key through with a trimmed testPlanId', () => { + it('drops a blank testPlanId and keeps the other testManagementOptions keys', () => { const capabilities = [ { browserName: 'chrome' } ] - const options = createBrowserstackOptions({ + const emptyPlanOptions = createBrowserstackOptions({ testManagementOptions: { - testPlanId: ' tm-plan-456 ', - issues: ['XYZ-1', 'XYZ-2'], - issueTracker: { name: 'jira', host: 'https://demo.atlassian.net' }, - customKey: { nested: true } + testPlanId: ' ', + ignoredKey: 'ignored' } }) - - const parsed = JSON.parse(CLIUtils.getBinConfig(mockConfig, capabilities, options)) - - expect(parsed.testManagementOptions).toEqual({ - testPlanId: 'tm-plan-456', - issues: ['XYZ-1', 'XYZ-2'], - issueTracker: { name: 'jira', host: 'https://demo.atlassian.net' }, - customKey: { nested: true } - }) - }) - - it('drops a blank testPlanId but keeps the other testManagementOptions keys', () => { - const capabilities = [ - { browserName: 'chrome' } - ] - const options = createBrowserstackOptions({ + const validPlanOptions = createBrowserstackOptions({ testManagementOptions: { - testPlanId: ' ', - issues: ['XYZ-1'] + testPlanId: ' tm-plan-456 ', + ignoredKey: 'ignored' } }) - const parsed = JSON.parse(CLIUtils.getBinConfig(mockConfig, capabilities, options)) + const emptyPlanResult = CLIUtils.getBinConfig(mockConfig, capabilities, emptyPlanOptions) - expect(parsed.testManagementOptions).toEqual({ - issues: ['XYZ-1'] + const emptyPlanParsed = JSON.parse(emptyPlanResult) + expect(emptyPlanParsed.testManagementOptions).toEqual({ + ignoredKey: 'ignored' }) - }) - it('omits testManagementOptions when nothing is left to send', () => { - const capabilities = [ - { browserName: 'chrome' } - ] - const blankPlanOptions = createBrowserstackOptions({ - testManagementOptions: { - testPlanId: ' ' - } - }) - const emptyOptions = createBrowserstackOptions({ - testManagementOptions: {} - }) - const nonObjectOptions = createBrowserstackOptions({ - testManagementOptions: 'not-an-object' - }) + const validPlanResult = CLIUtils.getBinConfig(mockConfig, capabilities, validPlanOptions) - for (const options of [blankPlanOptions, emptyOptions, nonObjectOptions]) { - const parsed = JSON.parse(CLIUtils.getBinConfig(mockConfig, capabilities, options)) - expect(parsed.testManagementOptions).toBeUndefined() - } + const validPlanParsed = JSON.parse(validPlanResult) + expect(validPlanParsed.testManagementOptions).toEqual({ + testPlanId: 'tm-plan-456', + ignoredKey: 'ignored' + }) }) })