From 3befc20a8fb0661d268a15b01cf06f619d141bdc Mon Sep 17 00:00:00 2001 From: Ashish Sharma Date: Thu, 23 Jul 2026 12:53:49 +0530 Subject: [PATCH 1/2] feat(LCAM-1282): central-user test metadata, GRR URL guard, build grouping identifier Port of ashish0305/webdriveriolc#1 ("support of setting metadata") onto the v8 line of the extracted standalone repo (packages/browserstack-service/src). - Add BrowserStackSDK.setTestMetadata() + TestMetadata store, gated on the app_lcnc central user (BROWSERSTACK_CENTRAL_USER). Metadata is keyed per test-run uuid with a fallback and attached to the TestFramework event and the reporter's BTCER payload. - Emit central-user keys in the TestHub product maps. - APIUtils.updateURLSForGRR: validate the GRR URL set via a hasValidGRRUrls type guard and no-op (return false) when incomplete, preventing the TypeError -> prod-collector fallback. - Send grouping_identifier (BROWSERSTACK_BUILD_GROUPING_IDENTIFIER) on build launch. - Force result='skipped' for TestRunSkipped so downstream status is correct. Existing specs updated for the new app_lcnc product-map key and the onBeforeTest instance arg. Build + full test suite pass locally on v8. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/browserStackSdk.ts | 15 +++++ .../browserstack-service/src/cli/apiUtils.ts | 22 +++++++- .../src/cli/modules/testHubModule.ts | 28 +++++++++- .../browserstack-service/src/constants.ts | 4 ++ packages/browserstack-service/src/index.ts | 1 + packages/browserstack-service/src/metadata.ts | 55 +++++++++++++++++++ packages/browserstack-service/src/reporter.ts | 11 ++++ .../browserstack-service/src/testHub/utils.ts | 8 ++- packages/browserstack-service/src/types.ts | 1 + packages/browserstack-service/src/util.ts | 16 ++++++ .../tests/cli/modules/testHubModule.test.ts | 1 + .../funnelInstrumentation.test.ts | 21 ++++--- .../tests/testHub/utils.test.ts | 3 +- 13 files changed, 171 insertions(+), 15 deletions(-) create mode 100644 packages/browserstack-service/src/browserStackSdk.ts create mode 100644 packages/browserstack-service/src/metadata.ts diff --git a/packages/browserstack-service/src/browserStackSdk.ts b/packages/browserstack-service/src/browserStackSdk.ts new file mode 100644 index 0000000..d3b4f94 --- /dev/null +++ b/packages/browserstack-service/src/browserStackSdk.ts @@ -0,0 +1,15 @@ +import TestMetadata from './metadata.js' + +export class BrowserStackSDK { + /** + * Attach metadata to the current test run. + * + * @param metadata - Metadata object. Must include an `identifier` string + * (max 40 characters); calls without a valid identifier are ignored. + */ + static setTestMetadata(metadata: Record = {}) { + TestMetadata.set(metadata) + } +} + +export default BrowserStackSDK diff --git a/packages/browserstack-service/src/cli/apiUtils.ts b/packages/browserstack-service/src/cli/apiUtils.ts index 9eabcae..eb6b780 100644 --- a/packages/browserstack-service/src/cli/apiUtils.ts +++ b/packages/browserstack-service/src/cli/apiUtils.ts @@ -10,7 +10,25 @@ export default class APIUtils { static UPLOAD_LOGS_ADDRESS = 'https://upload-observability.browserstack.com' static EDS_URL = 'https://eds.browserstack.com' - static updateURLSForGRR(apis: GRRUrls) { + static hasValidGRRUrls(apis?: Partial): apis is GRRUrls { + return Boolean( + apis?.automate?.api && + apis?.automate?.upload && + apis?.appAutomate?.api && + apis?.appAutomate?.upload && + apis?.percy?.api && + apis?.appAccessibility?.api && + apis?.observability?.api && + apis?.observability?.upload && + apis?.edsInstrumentation?.api + ) + } + + static updateURLSForGRR(apis?: Partial) { + if (!this.hasValidGRRUrls(apis)) { + return false + } + this.FUNNEL_INSTRUMENTATION_URL = `${apis.automate.api}/sdk/v1/event` this.BROWSERSTACK_AUTOMATE_API_URL = apis.automate.api this.BROWSERSTACK_AA_API_URL = apis.appAutomate.api @@ -21,5 +39,7 @@ export default class APIUtils { this.DATA_ENDPOINT = apis.observability.api this.UPLOAD_LOGS_ADDRESS = apis.observability.upload this.EDS_URL = apis.edsInstrumentation.api + + return true } } diff --git a/packages/browserstack-service/src/cli/modules/testHubModule.ts b/packages/browserstack-service/src/cli/modules/testHubModule.ts index cb1f13c..930f1a9 100644 --- a/packages/browserstack-service/src/cli/modules/testHubModule.ts +++ b/packages/browserstack-service/src/cli/modules/testHubModule.ts @@ -15,6 +15,7 @@ import WdioMochaTestFramework from '../frameworks/wdioMochaTestFramework.js' import type AutomationFrameworkInstance from '../instances/automationFrameworkInstance.js' import AutomationFramework from '../frameworks/automationFramework.js' import { AutomationFrameworkConstants } from '../frameworks/constants/automationFrameworkConstants.js' +import TestMetadata from '../../metadata.js' /** * TestHub Module for BrowserStack @@ -34,13 +35,15 @@ export default class TestHubModule extends BaseModule { this.name = 'TestHubModule' this.testhubConfig = testhubConfig - TestFramework.registerObserver(TestFrameworkState.TEST, HookState.PRE, this.onBeforeTest.bind(this)) - Object.values(TestFrameworkState).forEach(state => { Object.values(HookState).forEach(hook => { TestFramework.registerObserver(state, hook, this.onAllTestEvents.bind(this)) }) }) + // TEST/PRE: sendTestFrameworkEvent must run before onBeforeTest mutates + // TestMetadata.currentTestRunUuid, so sequence them explicitly instead of + // relying on observer registration order. + TestFramework.registerObserver(TestFrameworkState.TEST, HookState.PRE, this.onBeforeTest.bind(this)) } /** @@ -51,8 +54,16 @@ export default class TestHubModule extends BaseModule { return TestHubModule.MODULE_NAME } + private getCurrentTestRunUuid(instance: TestFrameworkInstance): string | undefined { + return (TestFramework.getState(instance, TestFrameworkConstants.KEY_TEST_UUID) as string | undefined) ?? instance.getRef() + } + onBeforeTest(args: Record) { this.logger.debug('onBeforeTest: Called after test hook from cli configured module!!!') + const instance = args.instance as TestFrameworkInstance + const testUuid = this.getCurrentTestRunUuid(instance) + TestMetadata.setCurrentTestRunUuid(testUuid) + const autoInstace = AutomationFramework.getTrackedInstance() as AutomationFrameworkInstance const instances = [autoInstace] args.autoInstance = instances @@ -95,6 +106,10 @@ export default class TestHubModule extends BaseModule { if (testState === TestFrameworkState.TEST || CLIUtils.matchHookRegex(testState.toString().split('.')[1])) { this.sendTestFrameworkEvent(args) } + + if (testState === TestFrameworkState.TEST && hookState === HookState.POST) { + TestMetadata.reset() + } } async sendTestFrameworkEvent(args: Record) { @@ -113,7 +128,14 @@ export default class TestHubModule extends BaseModule { this.logger.debug(`sendTestFrameworkEvent for testState: ${testFrameworkState} hookState: ${testHookState}`) const platformIndex = process.env.WDIO_WORKER_ID ? parseInt(process.env.WDIO_WORKER_ID.split('-')[0]) : 0 const uuid = TestFramework.getState(instance, TestFrameworkConstants.KEY_TEST_UUID) || instance.getRef() - const eventJson = Buffer.from(JSON.stringify(Object.fromEntries(testData))) + const testDataObj = Object.fromEntries(testData) + if (!testDataObj.app_lcnc) { + const appLcncMeta = TestMetadata.get(uuid as string) + if (appLcncMeta && Object.keys(appLcncMeta).length > 0) { + testDataObj.app_lcnc = appLcncMeta + } + } + const eventJson = Buffer.from(JSON.stringify(testDataObj)) const executionContext = { hash: trackedContext.getId(), threadId: trackedContext.getThreadId().toString(), processId: trackedContext.getProcessId().toString() } const payload: Omit = { platformIndex, diff --git a/packages/browserstack-service/src/constants.ts b/packages/browserstack-service/src/constants.ts index 48f53f3..c78b654 100644 --- a/packages/browserstack-service/src/constants.ts +++ b/packages/browserstack-service/src/constants.ts @@ -106,6 +106,10 @@ export const BROWSERSTACK_TESTHUB_UUID = 'BROWSERSTACK_TESTHUB_UUID' // To store test run uuid export const TEST_ANALYTICS_ID = 'TEST_ANALYTICS_ID' +// Central user mode for BrowserStack integrations. +export const BROWSERSTACK_CENTRAL_USER = 'BROWSERSTACK_CENTRAL_USER' +export const BROWSERSTACK_BUILD_GROUPING_IDENTIFIER = 'BROWSERSTACK_BUILD_GROUPING_IDENTIFIER' + // Whether to collect performance instrumentation or not export const PERF_MEASUREMENT_ENV = 'BROWSERSTACK_O11Y_PERF_MEASUREMENT' diff --git a/packages/browserstack-service/src/index.ts b/packages/browserstack-service/src/index.ts index 3affa66..6b97aab 100644 --- a/packages/browserstack-service/src/index.ts +++ b/packages/browserstack-service/src/index.ts @@ -12,6 +12,7 @@ export const log4jsAppender = { configure } export const BStackTestOpsLogger = logReportingAPI import * as Percy from './Percy/PercySDK.js' +export { BrowserStackSDK } from './browserStackSdk.js' export const PercySDK = Percy export * from './types.js' diff --git a/packages/browserstack-service/src/metadata.ts b/packages/browserstack-service/src/metadata.ts new file mode 100644 index 0000000..fecd47c --- /dev/null +++ b/packages/browserstack-service/src/metadata.ts @@ -0,0 +1,55 @@ +import { BStackLogger } from './bstackLogger.js' +import { getCentralUser } from './util.js' + +type Metadata = Record + +class TestMetadata { + private static currentTestRunUuid?: string + private static metadataByTestRunUuid: Record = {} + private static fallbackMetadata: Metadata = {} + + static setCurrentTestRunUuid(testRunUuid?: string) { + TestMetadata.currentTestRunUuid = testRunUuid + } + + static set(metadata: Metadata = {}) { + if (!getCentralUser().app_lcnc) { + return + } + + const testRunIdentifier = metadata.identifier + if (typeof testRunIdentifier !== 'string') { + BStackLogger.warn('setTestMetadata: metadata.identifier must be a string.') + return + } + if (testRunIdentifier.length > 40) { + BStackLogger.warn(`setTestMetadata: identifier "${testRunIdentifier}" exceeds the 40-character limit.`) + return + } + TestMetadata.fallbackMetadata = metadata + + if (TestMetadata.currentTestRunUuid) { + TestMetadata.metadataByTestRunUuid[TestMetadata.currentTestRunUuid] = metadata + } + } + + static get(testRunUuid?: string): Metadata { + if (!getCentralUser().app_lcnc) { + return {} + } + + if (testRunUuid) { + return TestMetadata.metadataByTestRunUuid[testRunUuid] || TestMetadata.fallbackMetadata || {} + } + + return TestMetadata.fallbackMetadata || {} + } + + static reset() { + TestMetadata.currentTestRunUuid = undefined + TestMetadata.metadataByTestRunUuid = {} + TestMetadata.fallbackMetadata = {} + } +} + +export default TestMetadata diff --git a/packages/browserstack-service/src/reporter.ts b/packages/browserstack-service/src/reporter.ts index 736a1a6..1cdd44d 100644 --- a/packages/browserstack-service/src/reporter.ts +++ b/packages/browserstack-service/src/reporter.ts @@ -20,6 +20,7 @@ import { import { BStackLogger } from './bstackLogger.js' import type { Capabilities } from '@wdio/types' import Listener from './testOps/listener.js' +import TestMetadata from './metadata.js' class _TestReporter extends WDIOReporter { private _capabilities: WebdriverIO.Capabilities = {} @@ -288,6 +289,16 @@ class _TestReporter extends WDIOReporter { testData.hook_type = testData.name?.toLowerCase() ? getHookType(testData.name.toLowerCase()) : 'undefined' } + // For TestRunSkipped (mocha this.skip()), mocha sets testStats.state = 'pending'. + // Force result to 'skipped' so the BTCER event downstream gets the correct status. + if (eventType === 'TestRunSkipped') { + testData.result = 'skipped' + const appLcncMetaData = TestMetadata.get(testData.uuid) + if (Object.keys(appLcncMetaData).length > 0) { + testData.app_lcnc = appLcncMetaData + } + } + return testData } } diff --git a/packages/browserstack-service/src/testHub/utils.ts b/packages/browserstack-service/src/testHub/utils.ts index 215ddbe..8a8e9b1 100644 --- a/packages/browserstack-service/src/testHub/utils.ts +++ b/packages/browserstack-service/src/testHub/utils.ts @@ -2,7 +2,7 @@ import { BROWSERSTACK_PERCY, BROWSERSTACK_OBSERVABILITY, BROWSERSTACK_ACCESSIBILITY } from '../constants.js' import type BrowserStackConfig from '../config.js' import { BStackLogger } from '../bstackLogger.js' -import { isTrue } from '../util.js' +import { getCentralUser, isTrue } from '../util.js' export const getProductMap = (config: BrowserStackConfig): any => { return { @@ -10,7 +10,8 @@ export const getProductMap = (config: BrowserStackConfig): any => { 'accessibility': config.accessibility as boolean, 'percy': config.percy, 'automate': config.automate, - 'app_automate': config.appAutomate + 'app_automate': config.appAutomate, + ...getCentralUser() } } @@ -70,6 +71,7 @@ export const getProductMapForBuildStartCall = (config: BrowserStackConfig, acces accessibility: accessibilityAutomation, percy: config.percy, automate: config.automate, - app_automate: config.appAutomate + app_automate: config.appAutomate, + ...getCentralUser() } } diff --git a/packages/browserstack-service/src/types.ts b/packages/browserstack-service/src/types.ts index a4ce3b2..e154ffa 100644 --- a/packages/browserstack-service/src/types.ts +++ b/packages/browserstack-service/src/types.ts @@ -284,6 +284,7 @@ export interface TestData { hook_type?: string, hooks?: string[], meta?: TestMeta, + app_lcnc?: Record, tags?: string[], test_run_id?: string, product_map?: {} diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 2e8ca78..1cea05c 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -33,6 +33,8 @@ import { BSTACK_A11Y_POLLING_TIMEOUT, TESTOPS_SCREENSHOT_ENV, BROWSERSTACK_TESTHUB_UUID, + BROWSERSTACK_CENTRAL_USER, + BROWSERSTACK_BUILD_GROUPING_IDENTIFIER, PERF_MEASUREMENT_ENV, RERUN_ENV, TESTOPS_BUILD_COMPLETED_ENV, @@ -370,6 +372,19 @@ export const processLaunchBuildResponse = (response: LaunchResponse, options: Br processAccessibilityResponse(response, options) } +export type CentralUser = { + app_lcnc: boolean; +} + +export const getCentralUser = (): Partial => { + switch (process.env[BROWSERSTACK_CENTRAL_USER]) { + case 'app_lcnc': + return { app_lcnc: true } + default: + return { app_lcnc: false } + } +} + export const launchTestSession = PerformanceTester.measureWrapper(PERFORMANCE_SDK_EVENTS.TESTHUB_EVENTS.START, o11yErrorHandler(async function launchTestSession(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig, bStackConfig: BrowserStackConfig, accessibilityAutomation: boolean | null) { const launchBuildUsage = UsageStats.getInstance().launchBuildUsage launchBuildUsage.triggered() @@ -396,6 +411,7 @@ export const launchTestSession = PerformanceTester.measureWrapper(PERFORMANCE_SD settings: options.accessibilityOptions }, browserstackAutomation: shouldAddServiceVersion(config, options.testObservability), + grouping_identifier: process.env[BROWSERSTACK_BUILD_GROUPING_IDENTIFIER] || '', framework_details: { frameworkName: WDIO_NAMING_PREFIX + config.framework, frameworkVersion: bsConfig.bstackServiceVersion, diff --git a/packages/browserstack-service/tests/cli/modules/testHubModule.test.ts b/packages/browserstack-service/tests/cli/modules/testHubModule.test.ts index 14660db..cb65fad 100644 --- a/packages/browserstack-service/tests/cli/modules/testHubModule.test.ts +++ b/packages/browserstack-service/tests/cli/modules/testHubModule.test.ts @@ -141,6 +141,7 @@ describe('TestHubModule', () => { const sendTestSessionEventSpy = vi.spyOn(testHubModule, 'sendTestSessionEvent').mockResolvedValue() const mockArgs = { + instance: { getRef: vi.fn(() => 'test-run-ref') }, test: { title: 'Test Login Functionality' } as Frameworks.Test, suiteTitle: 'Login Suite' } diff --git a/packages/browserstack-service/tests/instrumentation/funnelInstrumentation.test.ts b/packages/browserstack-service/tests/instrumentation/funnelInstrumentation.test.ts index fe476a6..aa092e8 100644 --- a/packages/browserstack-service/tests/instrumentation/funnelInstrumentation.test.ts +++ b/packages/browserstack-service/tests/instrumentation/funnelInstrumentation.test.ts @@ -45,7 +45,8 @@ const expectedEventData = { 'accessibility': true, 'percy': true, 'automate': true, - 'app_automate': false + 'app_automate': false, + 'app_lcnc': false }, product: expect.arrayContaining(['observability', 'automate', 'percy', 'accessibility']), framework: 'framework', @@ -106,7 +107,8 @@ describe('funnelInstrumentation', () => { 'accessibility': false, 'percy': false, 'automate': true, - 'app_automate': false + 'app_automate': false, + 'app_lcnc': false }, product: expect.arrayContaining(['observability', 'automate']), productUsage: expect.objectContaining({ @@ -219,7 +221,8 @@ describe('funnelInstrumentation', () => { 'accessibility': true, 'percy': true, 'automate': true, - 'app_automate': false + 'app_automate': false, + 'app_lcnc': false }, product: expect.arrayContaining(['observability', 'automate', 'percy', 'accessibility']), framework: 'framework', @@ -256,7 +259,8 @@ describe('funnelInstrumentation', () => { 'accessibility': true, 'percy': true, 'automate': true, - 'app_automate': false + 'app_automate': false, + 'app_lcnc': false }, product: expect.arrayContaining(['observability', 'automate', 'percy', 'accessibility']), framework: 'framework', @@ -302,7 +306,8 @@ describe('funnelInstrumentation', () => { 'accessibility': true, 'percy': true, 'automate': true, - 'app_automate': false + 'app_automate': false, + 'app_lcnc': false }, product: expect.arrayContaining(['observability', 'automate', 'percy', 'accessibility']), framework: 'framework', @@ -344,7 +349,8 @@ describe('funnelInstrumentation', () => { 'accessibility': true, 'percy': true, 'automate': true, - 'app_automate': false + 'app_automate': false, + 'app_lcnc': false }, product: expect.arrayContaining(['observability', 'automate', 'percy', 'accessibility']), framework: 'framework', @@ -389,7 +395,8 @@ describe('funnelInstrumentation', () => { 'accessibility': true, 'percy': true, 'automate': true, - 'app_automate': false + 'app_automate': false, + 'app_lcnc': false }, product: expect.arrayContaining(['observability', 'automate', 'percy', 'accessibility']), framework: 'framework', diff --git a/packages/browserstack-service/tests/testHub/utils.test.ts b/packages/browserstack-service/tests/testHub/utils.test.ts index 4910e09..93a8971 100644 --- a/packages/browserstack-service/tests/testHub/utils.test.ts +++ b/packages/browserstack-service/tests/testHub/utils.test.ts @@ -27,7 +27,8 @@ describe('getProductMap', () => { 'accessibility': false, 'percy': false, 'automate': true, - 'app_automate': false + 'app_automate': false, + 'app_lcnc': false } expect(productMap).toEqual(expectedProductMap) }) From ce9fdb42195810f2007639ef68ab94c9d4264f75 Mon Sep 17 00:00:00 2001 From: Ashish Sharma Date: Thu, 23 Jul 2026 15:12:56 +0530 Subject: [PATCH 2/2] UT converage --- .../tests/metadata.test.ts | 144 ++++++++++++++++++ .../browserstack-service/tests/util.test.ts | 27 ++++ 2 files changed, 171 insertions(+) create mode 100644 packages/browserstack-service/tests/metadata.test.ts diff --git a/packages/browserstack-service/tests/metadata.test.ts b/packages/browserstack-service/tests/metadata.test.ts new file mode 100644 index 0000000..2664051 --- /dev/null +++ b/packages/browserstack-service/tests/metadata.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' + +import TestMetadata from '../src/metadata.js' +import BrowserStackSDK from '../src/browserStackSdk.js' +import * as bstackLogger from '../src/bstackLogger.js' +import { BROWSERSTACK_CENTRAL_USER } from '../src/constants.js' + +describe('TestMetadata', () => { + let warnSpy: any + const originalCentralUser = process.env[BROWSERSTACK_CENTRAL_USER] + + beforeEach(() => { + process.env[BROWSERSTACK_CENTRAL_USER] = 'app_lcnc' + TestMetadata.reset() + warnSpy = vi.spyOn(bstackLogger.BStackLogger, 'warn').mockImplementation(() => {}) + }) + + afterEach(() => { + TestMetadata.reset() + warnSpy.mockRestore() + if (originalCentralUser === undefined) { + delete process.env[BROWSERSTACK_CENTRAL_USER] + } else { + process.env[BROWSERSTACK_CENTRAL_USER] = originalCentralUser + } + }) + + describe('central-user gating', () => { + it('set() is a no-op and get() returns {} when central user is not app_lcnc', () => { + delete process.env[BROWSERSTACK_CENTRAL_USER] + TestMetadata.set({ identifier: 'abc' }) + expect(TestMetadata.get()).toEqual({}) + }) + + it('get() returns {} when central user is not app_lcnc even if data was stored while enabled', () => { + TestMetadata.set({ identifier: 'abc' }) + delete process.env[BROWSERSTACK_CENTRAL_USER] + expect(TestMetadata.get()).toEqual({}) + }) + }) + + describe('set() validation', () => { + it('warns and ignores when identifier is not a string', () => { + TestMetadata.set({ identifier: 123 as any }) + expect(warnSpy).toHaveBeenCalled() + expect(TestMetadata.get()).toEqual({}) + }) + + it('warns and ignores when identifier is missing', () => { + TestMetadata.set({ foo: 'bar' }) + expect(warnSpy).toHaveBeenCalled() + expect(TestMetadata.get()).toEqual({}) + }) + + it('warns and ignores when identifier exceeds 40 characters', () => { + const tooLong = 'x'.repeat(41) + TestMetadata.set({ identifier: tooLong }) + expect(warnSpy).toHaveBeenCalled() + expect(TestMetadata.get()).toEqual({}) + }) + + it('accepts an identifier of exactly 40 characters', () => { + const exactly40 = 'x'.repeat(40) + TestMetadata.set({ identifier: exactly40 }) + expect(warnSpy).not.toHaveBeenCalled() + expect(TestMetadata.get()).toEqual({ identifier: exactly40 }) + }) + }) + + describe('fallback vs per-uuid storage', () => { + it('stores only as fallback when no current test-run uuid is set', () => { + TestMetadata.set({ identifier: 'run-1' }) + expect(TestMetadata.get()).toEqual({ identifier: 'run-1' }) + expect(TestMetadata.get('unknown-uuid')).toEqual({ identifier: 'run-1' }) + }) + + it('stores per-uuid when a current test-run uuid is set', () => { + TestMetadata.setCurrentTestRunUuid('uuid-1') + TestMetadata.set({ identifier: 'run-1' }) + expect(TestMetadata.get('uuid-1')).toEqual({ identifier: 'run-1' }) + }) + + it('falls back to the latest metadata for an unknown uuid', () => { + TestMetadata.setCurrentTestRunUuid('uuid-1') + TestMetadata.set({ identifier: 'run-1' }) + expect(TestMetadata.get('uuid-2')).toEqual({ identifier: 'run-1' }) + }) + + it('returns the correct metadata per uuid across multiple test runs', () => { + TestMetadata.setCurrentTestRunUuid('uuid-1') + TestMetadata.set({ identifier: 'run-1' }) + TestMetadata.setCurrentTestRunUuid('uuid-2') + TestMetadata.set({ identifier: 'run-2' }) + expect(TestMetadata.get('uuid-1')).toEqual({ identifier: 'run-1' }) + expect(TestMetadata.get('uuid-2')).toEqual({ identifier: 'run-2' }) + }) + }) + + describe('reset()', () => { + it('clears current uuid, per-uuid store and fallback', () => { + TestMetadata.setCurrentTestRunUuid('uuid-1') + TestMetadata.set({ identifier: 'run-1' }) + TestMetadata.reset() + expect(TestMetadata.get()).toEqual({}) + expect(TestMetadata.get('uuid-1')).toEqual({}) + }) + }) +}) + +describe('BrowserStackSDK.setTestMetadata', () => { + const originalCentralUser = process.env[BROWSERSTACK_CENTRAL_USER] + + beforeEach(() => { + process.env[BROWSERSTACK_CENTRAL_USER] = 'app_lcnc' + TestMetadata.reset() + vi.spyOn(bstackLogger.BStackLogger, 'warn').mockImplementation(() => {}) + }) + + afterEach(() => { + TestMetadata.reset() + vi.restoreAllMocks() + if (originalCentralUser === undefined) { + delete process.env[BROWSERSTACK_CENTRAL_USER] + } else { + process.env[BROWSERSTACK_CENTRAL_USER] = originalCentralUser + } + }) + + it('delegates to TestMetadata.set', () => { + const setSpy = vi.spyOn(TestMetadata, 'set') + BrowserStackSDK.setTestMetadata({ identifier: 'run-1' }) + expect(setSpy).toHaveBeenCalledWith({ identifier: 'run-1' }) + }) + + it('makes the metadata retrievable via TestMetadata.get', () => { + BrowserStackSDK.setTestMetadata({ identifier: 'run-1' }) + expect(TestMetadata.get()).toEqual({ identifier: 'run-1' }) + }) + + it('defaults to an empty object when called with no argument', () => { + expect(() => BrowserStackSDK.setTestMetadata()).not.toThrow() + expect(TestMetadata.get()).toEqual({}) + }) +}) diff --git a/packages/browserstack-service/tests/util.test.ts b/packages/browserstack-service/tests/util.test.ts index 2b507b4..a49436a 100644 --- a/packages/browserstack-service/tests/util.test.ts +++ b/packages/browserstack-service/tests/util.test.ts @@ -2255,3 +2255,30 @@ describe('isMultiRemoteCaps', () => { expect(isMultiRemoteCaps(capsWithNull as any)).toBe(false) }) }) + +describe('getCentralUser', () => { + const originalCentralUser = process.env.BROWSERSTACK_CENTRAL_USER + + afterEach(() => { + if (originalCentralUser === undefined) { + delete process.env.BROWSERSTACK_CENTRAL_USER + } else { + process.env.BROWSERSTACK_CENTRAL_USER = originalCentralUser + } + }) + + it('returns { app_lcnc: true } when the central user is app_lcnc', () => { + process.env.BROWSERSTACK_CENTRAL_USER = 'app_lcnc' + expect(utils.getCentralUser()).toEqual({ app_lcnc: true }) + }) + + it('returns { app_lcnc: false } when the central user env var is unset', () => { + delete process.env.BROWSERSTACK_CENTRAL_USER + expect(utils.getCentralUser()).toEqual({ app_lcnc: false }) + }) + + it('returns { app_lcnc: false } for an unrecognised central user', () => { + process.env.BROWSERSTACK_CENTRAL_USER = 'some_other_user' + expect(utils.getCentralUser()).toEqual({ app_lcnc: false }) + }) +})