From 2e2f4996058e391369f9de36efc9d640c5293981 Mon Sep 17 00:00:00 2001 From: rounak bhatia Date: Wed, 14 Oct 2026 17:03:58 +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 | 28 ++++++--- packages/browserstack-service/src/types.ts | 34 +++++++++- .../tests/cli/cliUtils.test.ts | 63 ++++++++++++++----- 3 files changed, 100 insertions(+), 25 deletions(-) diff --git a/packages/browserstack-service/src/cli/cliUtils.ts b/packages/browserstack-service/src/cli/cliUtils.ts index 3c6e5c05..5b1454aa 100644 --- a/packages/browserstack-service/src/cli/cliUtils.ts +++ b/packages/browserstack-service/src/cli/cliUtils.ts @@ -116,11 +116,7 @@ export class CLIUtils { ) 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, @@ -133,10 +129,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 } const caps = Array.isArray(capabilities) ? capabilities : [capabilities] @@ -167,6 +161,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 BSTACK_SERVICE_VERSION } diff --git a/packages/browserstack-service/src/types.ts b/packages/browserstack-service/src/types.ts index e0fd88b4..75a50af2 100644 --- a/packages/browserstack-service/src/types.ts +++ b/packages/browserstack-service/src/types.ts @@ -46,8 +46,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 { @@ -99,8 +123,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 aa82167c..de6ce300 100644 --- a/packages/browserstack-service/tests/cli/cliUtils.test.ts +++ b/packages/browserstack-service/tests/cli/cliUtils.test.ts @@ -199,32 +199,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) - const validPlanParsed = JSON.parse(validPlanResult) - expect(validPlanParsed.testManagementOptions).toEqual({ - testPlanId: 'tm-plan-456' + expect(parsed.testManagementOptions).toEqual({ + issues: ['XYZ-1'] + }) + }) + + 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 e0009ae02adb3b89d6323457b8a2dda292ce5892 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:28 +0000 Subject: [PATCH 2/3] chore(changeset): auto-generate from PR template (minor) --- .changeset/pr-285.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pr-285.md diff --git a/.changeset/pr-285.md b/.changeset/pr-285.md new file mode 100644 index 00000000..aecbe5cc --- /dev/null +++ b/.changeset/pr-285.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 6bddbd1116ecaf93e1b2d318d74318b1af6201f9 Mon Sep 17 00:00:00 2001 From: rounak bhatia Date: Wed, 14 Oct 2026 18:53:38 +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, 16 insertions(+), 48 deletions(-) diff --git a/packages/browserstack-service/tests/cli/cliUtils.test.ts b/packages/browserstack-service/tests/cli/cliUtils.test.ts index de6ce300..bbc371c6 100644 --- a/packages/browserstack-service/tests/cli/cliUtils.test.ts +++ b/packages/browserstack-service/tests/cli/cliUtils.test.ts @@ -199,67 +199,35 @@ 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)) - - expect(parsed.testManagementOptions).toEqual({ - issues: ['XYZ-1'] + const emptyPlanResult = CLIUtils.getBinConfig(mockConfig, capabilities, emptyPlanOptions) + 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) + const validPlanParsed = JSON.parse(validPlanResult) + expect(validPlanParsed.testManagementOptions).toEqual({ + testPlanId: 'tm-plan-456', + ignoredKey: 'ignored' }) - - for (const options of [blankPlanOptions, emptyOptions, nonObjectOptions]) { - const parsed = JSON.parse(CLIUtils.getBinConfig(mockConfig, capabilities, options)) - expect(parsed.testManagementOptions).toBeUndefined() - } }) })