Skip to content

Commit a56cd70

Browse files
authored
Merge pull request #40 from browserstack/feat/upstream-parity-9.29
feat: port upstream service changes (9.28.0 → 9.29.1+) into the extract
2 parents 62223e6 + 1c6a95f commit a56cd70

22 files changed

Lines changed: 822 additions & 45 deletions

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/browserstack-service/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"undici": "^6.24.0",
7070
"uuid": "^11.1.0",
7171
"winston-transport": "^4.5.0",
72-
"yauzl": "^3.0.0",
72+
"yauzl": "^3.4.0",
7373
"@wdio/logger": "^9.0.0",
7474
"@wdio/reporter": "^9.0.0",
7575
"@wdio/types": "^9.0.0"
Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1+
// The WebdriverIO.Browser accessibility augmentations
2+
// (getAccessibilityResultsSummary, getAccessibilityResults, performScan,
3+
// startA11yScanning, stopA11yScanning) now live in src/index.ts inside
4+
// `declare global { namespace WebdriverIO { ... } }`, so that tsc emits them
5+
// into build/index.d.ts for consumers. They were moved out of this ambient
6+
// file to avoid duplicate-declaration conflicts. The setCustomTags augmentation
7+
// stays here (it is not part of the a11y type-shipping fix).
18
declare namespace WebdriverIO {
29
interface Browser {
3-
getAccessibilityResultsSummary: () => Promise<Record<string, unknown>>,
4-
getAccessibilityResults: () => Promise<Array<Record<string, unknown>>>,
5-
performScan: () => Promise<Record<string, unknown> | undefined>,
6-
startA11yScanning: () => Promise<void>,
7-
stopA11yScanning: () => Promise<void>
10+
setCustomTags: (key: string, value: string) => Promise<void>
811
}
912

1013
interface MultiRemoteBrowser {
11-
getAccessibilityResultsSummary: () => Promise<Record<string, unknown>>,
12-
getAccessibilityResults: () => Promise<Array<Record<string, unknown>>>,
13-
performScan: () => Promise<Record<string, unknown> | undefined>,
14-
startA11yScanning: () => Promise<void>,
15-
stopA11yScanning: () => Promise<void>
14+
setCustomTags: (key: string, value: string) => Promise<void>
1615
}
1716
}

packages/browserstack-service/src/accessibility-handler.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/// <reference path="./@types/bstack-service-types.d.ts" />
21
import util from 'node:util'
32

43
import type { Capabilities, Frameworks, Options } from '@wdio/types'

packages/browserstack-service/src/cli/frameworks/constants/testFrameworkConstants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const TestFrameworkConstants = {
2121
KEY_TEST_FAILURE_REASON : 'test_failure_reason',
2222
KEY_TEST_LOGS : 'test_logs',
2323
KEY_TEST_META : 'test_meta',
24+
KEY_CUSTOM_TAGS : 'custom_metadata',
2425
KEY_TEST_DEFERRED : 'test_deferred',
2526
KEY_SESSION_NAME : 'test_session_name',
2627
KEY_AUTOMATE_SESSION_NAME : 'automate_session_name',

packages/browserstack-service/src/cli/grpcClient.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,17 @@ export class GrpcClient {
300300
}
301301
const { platformIndex, testFrameworkName, testFrameworkVersion, testFrameworkState, testHookState, testUuid, automationSessions, capabilities, executionContext } = data
302302
const sessions = automationSessions.map((automationSession) => {
303+
// LTS: pass through optional product field so testHubModule's
304+
// ltsActive override (sets product='loadTesting' on the
305+
// AutomationSession) actually reaches the binary via the
306+
// gRPC TestSessionEvent payload. Without this passthrough
307+
// the binary's makeIntegrations falls back to 'automate'
308+
// and test_run.origin lands as Automate instead of
309+
// LoadTesting. Mirror of py-sdk a245a814 +
310+
// browserstack-binary's existing s?.product read.
303311
return AutomationSessionConstructor.create({
304312
provider: automationSession.provider,
313+
product: automationSession.product,
305314
frameworkName: automationSession.frameworkName,
306315
frameworkVersion: automationSession.frameworkVersion,
307316
frameworkSessionId: automationSession.frameworkSessionId,

packages/browserstack-service/src/cli/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import WdioMochaTestFramework from './frameworks/wdioMochaTestFramework.js'
1919
import WdioAutomationFramework from './frameworks/wdioAutomationFramework.js'
2020
import WebdriverIOModule from './modules/webdriverIOModule.js'
2121
import AccessibilityModule from './modules/accessibilityModule.js'
22+
import CustomTagsModule from './modules/customTagsModule.js'
2223
import { isTurboScale, processAccessibilityResponse, shouldAddServiceVersion } from '../util.js'
2324
import ObservabilityModule from './modules/observabilityModule.js'
2425
import type { BrowserstackConfig, BrowserstackOptions, LaunchResponse } from '../types.js'
@@ -166,6 +167,10 @@ export class BrowserstackCLI {
166167

167168
this.modules[TestHubModule.MODULE_NAME] = new TestHubModule(startBinResponse.testhub)
168169

170+
// Custom-tag (multi Test-Case-ID) tagging rides the per-test event_json
171+
// to TestHub, so it is gated on the testhub pipeline being active.
172+
this.modules[CustomTagsModule.MODULE_NAME] = new CustomTagsModule()
173+
169174
if (startBinResponse.accessibility?.success){
170175
process.env[BROWSERSTACK_ACCESSIBILITY] = 'true'
171176
const options = this.options as BrowserstackConfig & BrowserstackOptions

packages/browserstack-service/src/cli/modules/accessibilityModule.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
/// <reference path="../../@types/bstack-service-types.d.ts" />
21
import BaseModule from './baseModule.js'
32
import { BrowserstackCLI } from '../index.js'
43
import { BStackLogger } from '../cliLogger.js'
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/// <reference path="../../@types/bstack-service-types.d.ts" />
2+
import BaseModule from './baseModule.js'
3+
import { BStackLogger } from '../cliLogger.js'
4+
import TestFramework from '../frameworks/testFramework.js'
5+
import AutomationFramework from '../frameworks/automationFramework.js'
6+
import type AutomationFrameworkInstance from '../instances/automationFrameworkInstance.js'
7+
import type TestFrameworkInstance from '../instances/testFrameworkInstance.js'
8+
import { AutomationFrameworkState } from '../states/automationFrameworkState.js'
9+
import { HookState } from '../states/hookState.js'
10+
import { TestFrameworkConstants } from '../frameworks/constants/testFrameworkConstants.js'
11+
import { CLIUtils } from '../cliUtils.js'
12+
import WdioMochaTestFramework from '../frameworks/wdioMochaTestFramework.js'
13+
import { mergeIntoTags, parseCommaSeparatedValues } from '../../customTags.js'
14+
import type { CustomMetadata } from '../../customTags.js'
15+
16+
/**
17+
* CustomTagsModule — CLI/gRPC path registration for `browser.setCustomTags`.
18+
*
19+
* Mirrors AccessibilityModule: registers the browser method in onBeforeExecute()
20+
* (observer-bound to AutomationFrameworkState.CREATE / HookState.POST), instantiated
21+
* from BrowserstackCLI.loadModules() whenever the binary is up.
22+
*
23+
* The per-test custom_metadata is merged directly into the tracked
24+
* TestFrameworkInstance map under KEY_CUSTOM_TAGS ('custom_metadata'). That whole
25+
* map is serialized verbatim into event_json (testHubModule.sendTestFrameworkEvent),
26+
* so the binary reads it as event.custom_metadata. The instance is created fresh
27+
* per test, so per-instance merge gives test-scoped union/dedupe; the binary is a
28+
* strict pass-through and does NOT consolidate across events.
29+
*/
30+
export default class CustomTagsModule extends BaseModule {
31+
32+
logger = BStackLogger
33+
name: string
34+
static MODULE_NAME = 'CustomTagsModule'
35+
36+
constructor() {
37+
super()
38+
this.name = 'CustomTagsModule'
39+
AutomationFramework.registerObserver(AutomationFrameworkState.CREATE, HookState.POST, this.onBeforeExecute.bind(this))
40+
}
41+
42+
getModuleName() {
43+
return CustomTagsModule.MODULE_NAME
44+
}
45+
46+
async onBeforeExecute() {
47+
try {
48+
const autoInstance: AutomationFrameworkInstance = AutomationFramework.getTrackedInstance()
49+
if (!autoInstance) {
50+
this.logger.debug('CustomTagsModule: No tracked automation instance found!')
51+
return
52+
}
53+
54+
const browser = AutomationFramework.getDriver(autoInstance) as WebdriverIO.Browser
55+
if (!browser) {
56+
this.logger.debug('CustomTagsModule: No browser instance found for setCustomTags registration')
57+
return
58+
}
59+
60+
(browser as WebdriverIO.Browser).setCustomTags = async (key: string, value: string): Promise<void> => {
61+
try {
62+
if (!key || !value) {
63+
this.logger.warn('setCustomTags: key and value are required; ignoring call')
64+
return
65+
}
66+
67+
const testInstance: TestFrameworkInstance = TestFramework.getTrackedInstance()
68+
if (!testInstance) {
69+
this.logger.warn('setCustomTags called outside of a resolvable test context; ignoring')
70+
return
71+
}
72+
73+
const values = parseCommaSeparatedValues(value)
74+
if (values.length === 0) {
75+
this.logger.warn(`setCustomTags: no usable values parsed from "${value}"; ignoring call`)
76+
return
77+
}
78+
79+
// Merge into the per-test instance map (test-scoped accumulator).
80+
const existing = (TestFramework.getState(testInstance, TestFrameworkConstants.KEY_CUSTOM_TAGS) as CustomMetadata) || {}
81+
mergeIntoTags(existing, key, values)
82+
testInstance.updateMultipleEntries({
83+
[TestFrameworkConstants.KEY_CUSTOM_TAGS]: existing
84+
})
85+
86+
// Hook-level: if a hook is currently active, also stamp custom_metadata
87+
// onto the active hook so binary reads hook.custom_metadata.
88+
this.applyToActiveHook(testInstance, key, values)
89+
90+
this.logger.debug(`setCustomTags: merged key=${key} values=${JSON.stringify(values)}`)
91+
} catch (error) {
92+
this.logger.warn(`setCustomTags: error while recording custom tags: ${error}`)
93+
}
94+
}
95+
} catch (error) {
96+
this.logger.error(`Error in CustomTagsModule.onBeforeExecute: ${error}`)
97+
}
98+
}
99+
100+
/**
101+
* If the test framework is currently inside a hook, merge custom_metadata onto
102+
* the last-started hook record so the binary receives hook.custom_metadata.
103+
* Best-effort: silently no-ops when no active hook is resolvable.
104+
*/
105+
private applyToActiveHook(testInstance: TestFrameworkInstance, key: string, values: string[]) {
106+
try {
107+
const currentState = testInstance.getCurrentTestState().toString()
108+
if (!CLIUtils.matchHookRegex(currentState)) {
109+
return
110+
}
111+
const hook = WdioMochaTestFramework.lastActiveHook(testInstance, WdioMochaTestFramework.KEY_HOOK_LAST_STARTED)
112+
if (!hook) {
113+
return
114+
}
115+
const hookTags = (hook[TestFrameworkConstants.KEY_CUSTOM_TAGS] as CustomMetadata) || {}
116+
mergeIntoTags(hookTags, key, values)
117+
hook[TestFrameworkConstants.KEY_CUSTOM_TAGS] = hookTags
118+
} catch (error) {
119+
this.logger.debug(`setCustomTags: could not apply to active hook: ${error}`)
120+
}
121+
}
122+
}

packages/browserstack-service/src/cli/modules/testHubModule.ts

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import WdioMochaTestFramework from '../frameworks/wdioMochaTestFramework.js'
1515
import type AutomationFrameworkInstance from '../instances/automationFrameworkInstance.js'
1616
import AutomationFramework from '../frameworks/automationFramework.js'
1717
import { AutomationFrameworkConstants } from '../frameworks/constants/automationFrameworkConstants.js'
18+
import { isLoadTestingSession, getLtsSessionId } from '../../util.js'
1819

1920
/**
2021
* TestHub Module for BrowserStack
@@ -182,22 +183,49 @@ export default class TestHubModule extends BaseModule {
182183
}
183184

184185
this.logger.debug(`sendTestSessionEvent: instance iteration ${JSON.stringify(autoInstances)}`)
186+
// LTS: BLU runner pods route through a local Selenium hub
187+
// so KEY_IS_BROWSERSTACK_HUB resolves to false — without
188+
// this gate the AutomationSession lands with provider=
189+
// unknown_grid and TestHub's o11y classifier sets
190+
// test_run.origin=UnknownGrid. Mirrors py-sdk 3af3bba6
191+
// (force provider='browserstack' + product='loadTesting'
192+
// under LTS) and 0efca1ae (override frameworkSessionId
193+
// with the LTS pod-iteration env id).
194+
// Hoisted above the loop — env vars are stable for the
195+
// process lifetime, no need to re-read per iteration.
196+
const ltsActive = isLoadTestingSession()
197+
const ltsSessionId = ltsActive ? getLtsSessionId() : ''
185198
// Process automation instances
186199
for (const autoInstance of autoInstances) {
187-
const sessionProvider = AutomationFramework.getState(autoInstance, AutomationFrameworkConstants.KEY_IS_BROWSERSTACK_HUB) as boolean
200+
const sessionProvider = ltsActive
188201
? 'browserstack'
189-
: 'unknown_grid'
202+
: (AutomationFramework.getState(autoInstance, AutomationFrameworkConstants.KEY_IS_BROWSERSTACK_HUB) as boolean
203+
? 'browserstack'
204+
: 'unknown_grid')
205+
206+
// Null-safe: under LTS the local-Selenium AutomationFrameworkInstance
207+
// may not have KEY_FRAMEWORK_SESSION_ID populated yet (the binary's
208+
// hub assigns sessionId later than KEY_IS_BROWSERSTACK_HUB). Without
209+
// the guard, AutomationFramework.getState returns undefined and the
210+
// chained .toString() throws TypeError before the (ltsActive &&
211+
// ltsSessionId) ternary on line 215 has a chance to fall through to
212+
// ltsSessionId. Default to '' so the ternary path stays untouched
213+
// and the non-LTS fall-through is still safe.
214+
const driverFrameworkSessionId = (
215+
AutomationFramework.getState(
216+
autoInstance,
217+
AutomationFrameworkConstants.KEY_FRAMEWORK_SESSION_ID,
218+
)?.toString() ?? ''
219+
)
190220

191221
const automationSession: AutomationSession = {
192222
provider: sessionProvider,
193223
ref: autoInstance.getRef(),
194224
hubUrl: this.config.hubUrl as string,
195-
frameworkSessionId: AutomationFramework.getState(
196-
autoInstance,
197-
AutomationFrameworkConstants.KEY_FRAMEWORK_SESSION_ID,
198-
).toString(),
225+
frameworkSessionId: (ltsActive && ltsSessionId) ? ltsSessionId : driverFrameworkSessionId,
199226
frameworkName: autoInstance.frameworkName,
200-
frameworkVersion: autoInstance.frameworkVersion
227+
frameworkVersion: autoInstance.frameworkVersion,
228+
...(ltsActive ? { product: 'loadTesting' } : {})
201229
}
202230
this.logger.debug(`sendTestSessionEvent: automationSession: ${JSON.stringify(automationSession)}`)
203231

0 commit comments

Comments
 (0)