@wdio/browserstack-service 0.0.0-canary-20260722132416

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/LICENSE-MIT +20 -0
  2. package/README.md +392 -0
  3. package/browserstack-service.d.ts +43 -0
  4. package/build/Percy/Percy-Handler.d.ts +32 -0
  5. package/build/Percy/Percy-Handler.d.ts.map +1 -0
  6. package/build/Percy/Percy-Handler.js +165 -0
  7. package/build/Percy/Percy-Handler.js.map +1 -0
  8. package/build/Percy/Percy.d.ts +19 -0
  9. package/build/Percy/Percy.d.ts.map +1 -0
  10. package/build/Percy/Percy.js +164 -0
  11. package/build/Percy/Percy.js.map +1 -0
  12. package/build/Percy/PercyBinary.d.ts +10 -0
  13. package/build/Percy/PercyBinary.d.ts.map +1 -0
  14. package/build/Percy/PercyBinary.js +161 -0
  15. package/build/Percy/PercyBinary.js.map +1 -0
  16. package/build/Percy/PercyCaptureMap.d.ts +9 -0
  17. package/build/Percy/PercyCaptureMap.d.ts.map +1 -0
  18. package/build/Percy/PercyCaptureMap.js +36 -0
  19. package/build/Percy/PercyCaptureMap.js.map +1 -0
  20. package/build/Percy/PercyHelper.d.ts +8 -0
  21. package/build/Percy/PercyHelper.d.ts.map +1 -0
  22. package/build/Percy/PercyHelper.js +68 -0
  23. package/build/Percy/PercyHelper.js.map +1 -0
  24. package/build/Percy/PercyLogger.d.ts +15 -0
  25. package/build/Percy/PercyLogger.d.ts.map +1 -0
  26. package/build/Percy/PercyLogger.js +68 -0
  27. package/build/Percy/PercyLogger.js.map +1 -0
  28. package/build/Percy/PercySDK.d.ts +4 -0
  29. package/build/Percy/PercySDK.d.ts.map +1 -0
  30. package/build/Percy/PercySDK.js +86 -0
  31. package/build/Percy/PercySDK.js.map +1 -0
  32. package/build/accessibility-handler.d.ts +52 -0
  33. package/build/accessibility-handler.d.ts.map +1 -0
  34. package/build/accessibility-handler.js +361 -0
  35. package/build/accessibility-handler.js.map +1 -0
  36. package/build/ai-handler.d.ts +23 -0
  37. package/build/ai-handler.d.ts.map +1 -0
  38. package/build/ai-handler.js +184 -0
  39. package/build/ai-handler.js.map +1 -0
  40. package/build/bstackLogger.d.ts +16 -0
  41. package/build/bstackLogger.d.ts.map +1 -0
  42. package/build/bstackLogger.js +71 -0
  43. package/build/bstackLogger.js.map +1 -0
  44. package/build/cleanup.d.ts +9 -0
  45. package/build/cleanup.d.ts.map +1 -0
  46. package/build/cleanup.js +101 -0
  47. package/build/cleanup.js.map +1 -0
  48. package/build/cli/apiUtils.d.ts +14 -0
  49. package/build/cli/apiUtils.d.ts.map +1 -0
  50. package/build/cli/apiUtils.js +25 -0
  51. package/build/cli/apiUtils.js.map +1 -0
  52. package/build/cli/cliLogger.d.ts +17 -0
  53. package/build/cli/cliLogger.d.ts.map +1 -0
  54. package/build/cli/cliLogger.js +82 -0
  55. package/build/cli/cliLogger.js.map +1 -0
  56. package/build/cli/cliUtils.d.ts +64 -0
  57. package/build/cli/cliUtils.d.ts.map +1 -0
  58. package/build/cli/cliUtils.js +461 -0
  59. package/build/cli/cliUtils.js.map +1 -0
  60. package/build/cli/eventDispatcher.d.ts +28 -0
  61. package/build/cli/eventDispatcher.d.ts.map +1 -0
  62. package/build/cli/eventDispatcher.js +49 -0
  63. package/build/cli/eventDispatcher.js.map +1 -0
  64. package/build/cli/frameworks/automationFramework.d.ts +89 -0
  65. package/build/cli/frameworks/automationFramework.d.ts.map +1 -0
  66. package/build/cli/frameworks/automationFramework.js +132 -0
  67. package/build/cli/frameworks/automationFramework.js.map +1 -0
  68. package/build/cli/frameworks/constants/automationFrameworkConstants.d.ts +18 -0
  69. package/build/cli/frameworks/constants/automationFrameworkConstants.d.ts.map +1 -0
  70. package/build/cli/frameworks/constants/automationFrameworkConstants.js +18 -0
  71. package/build/cli/frameworks/constants/automationFrameworkConstants.js.map +1 -0
  72. package/build/cli/frameworks/constants/testFrameworkConstants.d.ts +43 -0
  73. package/build/cli/frameworks/constants/testFrameworkConstants.d.ts.map +1 -0
  74. package/build/cli/frameworks/constants/testFrameworkConstants.js +43 -0
  75. package/build/cli/frameworks/constants/testFrameworkConstants.js.map +1 -0
  76. package/build/cli/frameworks/testFramework.d.ts +89 -0
  77. package/build/cli/frameworks/testFramework.d.ts.map +1 -0
  78. package/build/cli/frameworks/testFramework.js +126 -0
  79. package/build/cli/frameworks/testFramework.js.map +1 -0
  80. package/build/cli/frameworks/wdioAutomationFramework.d.ts +28 -0
  81. package/build/cli/frameworks/wdioAutomationFramework.d.ts.map +1 -0
  82. package/build/cli/frameworks/wdioAutomationFramework.js +67 -0
  83. package/build/cli/frameworks/wdioAutomationFramework.js.map +1 -0
  84. package/build/cli/frameworks/wdioMochaTestFramework.d.ts +82 -0
  85. package/build/cli/frameworks/wdioMochaTestFramework.d.ts.map +1 -0
  86. package/build/cli/frameworks/wdioMochaTestFramework.js +322 -0
  87. package/build/cli/frameworks/wdioMochaTestFramework.js.map +1 -0
  88. package/build/cli/grpcClient.d.ts +86 -0
  89. package/build/cli/grpcClient.d.ts.map +1 -0
  90. package/build/cli/grpcClient.js +532 -0
  91. package/build/cli/grpcClient.js.map +1 -0
  92. package/build/cli/index.d.ts +150 -0
  93. package/build/cli/index.d.ts.map +1 -0
  94. package/build/cli/index.js +464 -0
  95. package/build/cli/index.js.map +1 -0
  96. package/build/cli/instances/automationFrameworkInstance.d.ts +33 -0
  97. package/build/cli/instances/automationFrameworkInstance.d.ts.map +1 -0
  98. package/build/cli/instances/automationFrameworkInstance.js +45 -0
  99. package/build/cli/instances/automationFrameworkInstance.js.map +1 -0
  100. package/build/cli/instances/testFrameworkInstance.d.ts +62 -0
  101. package/build/cli/instances/testFrameworkInstance.d.ts.map +1 -0
  102. package/build/cli/instances/testFrameworkInstance.js +97 -0
  103. package/build/cli/instances/testFrameworkInstance.js.map +1 -0
  104. package/build/cli/instances/trackedContext.d.ts +32 -0
  105. package/build/cli/instances/trackedContext.d.ts.map +1 -0
  106. package/build/cli/instances/trackedContext.js +48 -0
  107. package/build/cli/instances/trackedContext.js.map +1 -0
  108. package/build/cli/instances/trackedInstance.d.ts +40 -0
  109. package/build/cli/instances/trackedInstance.d.ts.map +1 -0
  110. package/build/cli/instances/trackedInstance.js +64 -0
  111. package/build/cli/instances/trackedInstance.js.map +1 -0
  112. package/build/cli/modules/accessibilityModule.d.ts +46 -0
  113. package/build/cli/modules/accessibilityModule.d.ts.map +1 -0
  114. package/build/cli/modules/accessibilityModule.js +447 -0
  115. package/build/cli/modules/accessibilityModule.js.map +1 -0
  116. package/build/cli/modules/automateModule.d.ts +26 -0
  117. package/build/cli/modules/automateModule.d.ts.map +1 -0
  118. package/build/cli/modules/automateModule.js +223 -0
  119. package/build/cli/modules/automateModule.js.map +1 -0
  120. package/build/cli/modules/baseModule.d.ts +33 -0
  121. package/build/cli/modules/baseModule.d.ts.map +1 -0
  122. package/build/cli/modules/baseModule.js +52 -0
  123. package/build/cli/modules/baseModule.js.map +1 -0
  124. package/build/cli/modules/observabilityModule.d.ts +22 -0
  125. package/build/cli/modules/observabilityModule.d.ts.map +1 -0
  126. package/build/cli/modules/observabilityModule.js +46 -0
  127. package/build/cli/modules/observabilityModule.js.map +1 -0
  128. package/build/cli/modules/percyModule.d.ts +19 -0
  129. package/build/cli/modules/percyModule.d.ts.map +1 -0
  130. package/build/cli/modules/percyModule.js +78 -0
  131. package/build/cli/modules/percyModule.js.map +1 -0
  132. package/build/cli/modules/testHubModule.d.ts +30 -0
  133. package/build/cli/modules/testHubModule.d.ts.map +1 -0
  134. package/build/cli/modules/testHubModule.js +233 -0
  135. package/build/cli/modules/testHubModule.js.map +1 -0
  136. package/build/cli/modules/webdriverIOModule.d.ts +29 -0
  137. package/build/cli/modules/webdriverIOModule.d.ts.map +1 -0
  138. package/build/cli/modules/webdriverIOModule.js +129 -0
  139. package/build/cli/modules/webdriverIOModule.js.map +1 -0
  140. package/build/cli/states/automationFrameworkState.d.ts +65 -0
  141. package/build/cli/states/automationFrameworkState.d.ts.map +1 -0
  142. package/build/cli/states/automationFrameworkState.js +62 -0
  143. package/build/cli/states/automationFrameworkState.js.map +1 -0
  144. package/build/cli/states/hookState.d.ts +45 -0
  145. package/build/cli/states/hookState.d.ts.map +1 -0
  146. package/build/cli/states/hookState.js +44 -0
  147. package/build/cli/states/hookState.js.map +1 -0
  148. package/build/cli/states/testFrameworkState.d.ts +125 -0
  149. package/build/cli/states/testFrameworkState.d.ts.map +1 -0
  150. package/build/cli/states/testFrameworkState.js +116 -0
  151. package/build/cli/states/testFrameworkState.js.map +1 -0
  152. package/build/config.d.ts +29 -0
  153. package/build/config.d.ts.map +1 -0
  154. package/build/config.js +52 -0
  155. package/build/config.js.map +1 -0
  156. package/build/constants.d.ts +96 -0
  157. package/build/constants.d.ts.map +1 -0
  158. package/build/constants.js +156 -0
  159. package/build/constants.js.map +1 -0
  160. package/build/crash-reporter.d.ts +18 -0
  161. package/build/crash-reporter.d.ts.map +1 -0
  162. package/build/crash-reporter.js +134 -0
  163. package/build/crash-reporter.js.map +1 -0
  164. package/build/cucumber-types.d.ts +194 -0
  165. package/build/cucumber-types.d.ts.map +1 -0
  166. package/build/cucumber-types.js +2 -0
  167. package/build/cucumber-types.js.map +1 -0
  168. package/build/data-store.d.ts +4 -0
  169. package/build/data-store.d.ts.map +1 -0
  170. package/build/data-store.js +42 -0
  171. package/build/data-store.js.map +1 -0
  172. package/build/exitHandler.d.ts +4 -0
  173. package/build/exitHandler.d.ts.map +1 -0
  174. package/build/exitHandler.js +105 -0
  175. package/build/exitHandler.js.map +1 -0
  176. package/build/fileStream.d.ts +9 -0
  177. package/build/fileStream.d.ts.map +1 -0
  178. package/build/fileStream.js +13 -0
  179. package/build/fileStream.js.map +1 -0
  180. package/build/grpc/generated/sdk-messages-accessibility.d.ts +76 -0
  181. package/build/grpc/generated/sdk-messages-accessibility.d.ts.map +1 -0
  182. package/build/grpc/generated/sdk-messages-accessibility.js +908 -0
  183. package/build/grpc/generated/sdk-messages-accessibility.js.map +1 -0
  184. package/build/grpc/generated/sdk-messages-ai.d.ts +52 -0
  185. package/build/grpc/generated/sdk-messages-ai.d.ts.map +1 -0
  186. package/build/grpc/generated/sdk-messages-ai.js +643 -0
  187. package/build/grpc/generated/sdk-messages-ai.js.map +1 -0
  188. package/build/grpc/generated/sdk-messages-observability.d.ts +44 -0
  189. package/build/grpc/generated/sdk-messages-observability.d.ts.map +1 -0
  190. package/build/grpc/generated/sdk-messages-observability.js +505 -0
  191. package/build/grpc/generated/sdk-messages-observability.js.map +1 -0
  192. package/build/grpc/generated/sdk-messages-percy.d.ts +16 -0
  193. package/build/grpc/generated/sdk-messages-percy.d.ts.map +1 -0
  194. package/build/grpc/generated/sdk-messages-percy.js +111 -0
  195. package/build/grpc/generated/sdk-messages-percy.js.map +1 -0
  196. package/build/grpc/generated/sdk-messages-testhub.d.ts +16 -0
  197. package/build/grpc/generated/sdk-messages-testhub.d.ts.map +1 -0
  198. package/build/grpc/generated/sdk-messages-testhub.js +132 -0
  199. package/build/grpc/generated/sdk-messages-testhub.js.map +1 -0
  200. package/build/grpc/generated/sdk-messages.d.ts +288 -0
  201. package/build/grpc/generated/sdk-messages.d.ts.map +1 -0
  202. package/build/grpc/generated/sdk-messages.js +3958 -0
  203. package/build/grpc/generated/sdk-messages.js.map +1 -0
  204. package/build/grpc/generated/sdk.d.ts +282 -0
  205. package/build/grpc/generated/sdk.d.ts.map +1 -0
  206. package/build/grpc/generated/sdk.js +197 -0
  207. package/build/grpc/generated/sdk.js.map +1 -0
  208. package/build/grpc/index.d.ts +43 -0
  209. package/build/grpc/index.d.ts.map +1 -0
  210. package/build/grpc/index.js +154 -0
  211. package/build/grpc/index.js.map +1 -0
  212. package/build/index.d.ts +20 -0
  213. package/build/index.d.ts.map +1 -0
  214. package/build/index.js +13 -0
  215. package/build/index.js.map +1 -0
  216. package/build/insights-handler.d.ts +76 -0
  217. package/build/insights-handler.d.ts.map +1 -0
  218. package/build/insights-handler.js +855 -0
  219. package/build/insights-handler.js.map +1 -0
  220. package/build/instrumentation/funnelInstrumentation.d.ts +8 -0
  221. package/build/instrumentation/funnelInstrumentation.d.ts.map +1 -0
  222. package/build/instrumentation/funnelInstrumentation.js +235 -0
  223. package/build/instrumentation/funnelInstrumentation.js.map +1 -0
  224. package/build/instrumentation/performance/constants.d.ts +151 -0
  225. package/build/instrumentation/performance/constants.d.ts.map +1 -0
  226. package/build/instrumentation/performance/constants.js +158 -0
  227. package/build/instrumentation/performance/constants.js.map +1 -0
  228. package/build/instrumentation/performance/performance-tester.d.ts +55 -0
  229. package/build/instrumentation/performance/performance-tester.d.ts.map +1 -0
  230. package/build/instrumentation/performance/performance-tester.js +403 -0
  231. package/build/instrumentation/performance/performance-tester.js.map +1 -0
  232. package/build/launcher.d.ts +49 -0
  233. package/build/launcher.d.ts.map +1 -0
  234. package/build/launcher.js +1077 -0
  235. package/build/launcher.js.map +1 -0
  236. package/build/log4jsAppender.d.ts +2 -0
  237. package/build/log4jsAppender.d.ts.map +1 -0
  238. package/build/log4jsAppender.js +20 -0
  239. package/build/log4jsAppender.js.map +1 -0
  240. package/build/logPatcher.d.ts +13 -0
  241. package/build/logPatcher.d.ts.map +1 -0
  242. package/build/logPatcher.js +39 -0
  243. package/build/logPatcher.js.map +1 -0
  244. package/build/logReportingAPI.d.ts +12 -0
  245. package/build/logReportingAPI.d.ts.map +1 -0
  246. package/build/logReportingAPI.js +57 -0
  247. package/build/logReportingAPI.js.map +1 -0
  248. package/build/reporter.d.ts +40 -0
  249. package/build/reporter.d.ts.map +1 -0
  250. package/build/reporter.js +254 -0
  251. package/build/reporter.js.map +1 -0
  252. package/build/request-handler.d.ts +19 -0
  253. package/build/request-handler.d.ts.map +1 -0
  254. package/build/request-handler.js +78 -0
  255. package/build/request-handler.js.map +1 -0
  256. package/build/scripts/accessibility-scripts.d.ts +26 -0
  257. package/build/scripts/accessibility-scripts.d.ts.map +1 -0
  258. package/build/scripts/accessibility-scripts.js +91 -0
  259. package/build/scripts/accessibility-scripts.js.map +1 -0
  260. package/build/service.d.ts +68 -0
  261. package/build/service.d.ts.map +1 -0
  262. package/build/service.js +740 -0
  263. package/build/service.js.map +1 -0
  264. package/build/testHub/utils.d.ts +10 -0
  265. package/build/testHub/utils.d.ts.map +1 -0
  266. package/build/testHub/utils.js +67 -0
  267. package/build/testHub/utils.js.map +1 -0
  268. package/build/testOps/featureStats.d.ts +45 -0
  269. package/build/testOps/featureStats.d.ts.map +1 -0
  270. package/build/testOps/featureStats.js +117 -0
  271. package/build/testOps/featureStats.js.map +1 -0
  272. package/build/testOps/featureUsage.d.ts +22 -0
  273. package/build/testOps/featureUsage.d.ts.map +1 -0
  274. package/build/testOps/featureUsage.js +48 -0
  275. package/build/testOps/featureUsage.js.map +1 -0
  276. package/build/testOps/listener.d.ts +40 -0
  277. package/build/testOps/listener.d.ts.map +1 -0
  278. package/build/testOps/listener.js +269 -0
  279. package/build/testOps/listener.js.map +1 -0
  280. package/build/testOps/requestUtils.d.ts +4 -0
  281. package/build/testOps/requestUtils.d.ts.map +1 -0
  282. package/build/testOps/requestUtils.js +41 -0
  283. package/build/testOps/requestUtils.js.map +1 -0
  284. package/build/testOps/testOpsConfig.d.ts +11 -0
  285. package/build/testOps/testOpsConfig.d.ts.map +1 -0
  286. package/build/testOps/testOpsConfig.js +20 -0
  287. package/build/testOps/testOpsConfig.js.map +1 -0
  288. package/build/testOps/usageStats.d.ts +404 -0
  289. package/build/testOps/usageStats.d.ts.map +1 -0
  290. package/build/testOps/usageStats.js +115 -0
  291. package/build/testOps/usageStats.js.map +1 -0
  292. package/build/testorchestration/apply-orchestration.d.ts +7 -0
  293. package/build/testorchestration/apply-orchestration.d.ts.map +1 -0
  294. package/build/testorchestration/apply-orchestration.js +43 -0
  295. package/build/testorchestration/apply-orchestration.js.map +1 -0
  296. package/build/testorchestration/helpers.d.ts +20 -0
  297. package/build/testorchestration/helpers.d.ts.map +1 -0
  298. package/build/testorchestration/helpers.js +413 -0
  299. package/build/testorchestration/helpers.js.map +1 -0
  300. package/build/testorchestration/request-utils.d.ts +23 -0
  301. package/build/testorchestration/request-utils.d.ts.map +1 -0
  302. package/build/testorchestration/request-utils.js +82 -0
  303. package/build/testorchestration/request-utils.js.map +1 -0
  304. package/build/testorchestration/test-ordering-server.d.ts +33 -0
  305. package/build/testorchestration/test-ordering-server.d.ts.map +1 -0
  306. package/build/testorchestration/test-ordering-server.js +169 -0
  307. package/build/testorchestration/test-ordering-server.js.map +1 -0
  308. package/build/testorchestration/testorcherstrationhandler.d.ts +50 -0
  309. package/build/testorchestration/testorcherstrationhandler.d.ts.map +1 -0
  310. package/build/testorchestration/testorcherstrationhandler.js +134 -0
  311. package/build/testorchestration/testorcherstrationhandler.js.map +1 -0
  312. package/build/testorchestration/testorcherstrationutils.d.ts +86 -0
  313. package/build/testorchestration/testorcherstrationutils.d.ts.map +1 -0
  314. package/build/testorchestration/testorcherstrationutils.js +380 -0
  315. package/build/testorchestration/testorcherstrationutils.js.map +1 -0
  316. package/build/types.d.ts +413 -0
  317. package/build/types.d.ts.map +1 -0
  318. package/build/types.js +2 -0
  319. package/build/types.js.map +1 -0
  320. package/build/util.d.ts +241 -0
  321. package/build/util.d.ts.map +1 -0
  322. package/build/util.js +1665 -0
  323. package/build/util.js.map +1 -0
  324. package/package.json +96 -0
  325. package/src/proto/browserstack/sdk/v1/sdk-messages-accessibility.proto +71 -0
  326. package/src/proto/browserstack/sdk/v1/sdk-messages-ai.proto +48 -0
  327. package/src/proto/browserstack/sdk/v1/sdk-messages-observability.proto +39 -0
  328. package/src/proto/browserstack/sdk/v1/sdk-messages-percy.proto +10 -0
  329. package/src/proto/browserstack/sdk/v1/sdk-messages-testhub.proto +10 -0
  330. package/src/proto/browserstack/sdk/v1/sdk-messages.proto +276 -0
  331. package/src/proto/browserstack/sdk/v1/sdk.proto +41 -0
package/build/util.js ADDED
@@ -0,0 +1,1665 @@
1
+ import { hostname, platform, type, version, arch } from 'node:os';
2
+ import fs from 'node:fs';
3
+ import zlib from 'node:zlib';
4
+ import { promisify } from 'node:util';
5
+ import http from 'node:http';
6
+ import https from 'node:https';
7
+ import path from 'node:path';
8
+ import util from 'node:util';
9
+ import got, { HTTPError } from 'got';
10
+ import gitRepoInfo from 'git-repo-info';
11
+ import gitconfig from 'gitconfiglocal';
12
+ import { FormData } from 'formdata-node';
13
+ import { performance } from 'node:perf_hooks';
14
+ import logPatcher from './logPatcher.js';
15
+ import PerformanceTester from './instrumentation/performance/performance-tester.js';
16
+ import * as PERFORMANCE_SDK_EVENTS from './instrumentation/performance/constants.js';
17
+ import { logBuildError, handleErrorForObservability, handleErrorForAccessibility, getProductMapForBuildStartCall } from './testHub/utils.js';
18
+ import { OrchestrationUtils } from './testorchestration/testorcherstrationutils.js';
19
+ import { BROWSER_DESCRIPTION, UPLOAD_LOGS_ENDPOINT, consoleHolder, BSTACK_A11Y_POLLING_TIMEOUT, TESTOPS_SCREENSHOT_ENV, BROWSERSTACK_TESTHUB_UUID, PERF_MEASUREMENT_ENV, RERUN_ENV, TESTOPS_BUILD_COMPLETED_ENV, BROWSERSTACK_TESTHUB_JWT, BROWSERSTACK_OBSERVABILITY, BROWSERSTACK_TEST_REPORTING, BROWSERSTACK_TEST_PLAN_ID, BROWSERSTACK_ACCESSIBILITY, MAX_GIT_META_DATA_SIZE_IN_BYTES, GIT_META_DATA_TRUNCATED, APP_ALLY_ISSUES_SUMMARY_ENDPOINT, APP_ALLY_ISSUES_ENDPOINT, TEST_REPORTING_PROJECT_NAME, CLI_DEBUG_LOGS_FILE, WDIO_NAMING_PREFIX } from './constants.js';
20
+ import CrashReporter from './crash-reporter.js';
21
+ import { BStackLogger } from './bstackLogger.js';
22
+ import AccessibilityScripts from './scripts/accessibility-scripts.js';
23
+ import UsageStats from './testOps/usageStats.js';
24
+ import TestOpsConfig from './testOps/testOpsConfig.js';
25
+ import APIUtils from './cli/apiUtils.js';
26
+ import tar from 'tar';
27
+ import { fileFromPath } from 'formdata-node/file-from-path';
28
+ const pGitconfig = promisify(gitconfig);
29
+ export const DEFAULT_REQUEST_CONFIG = {
30
+ agent: {
31
+ http: new http.Agent({ keepAlive: true }),
32
+ https: new https.Agent({ keepAlive: true }),
33
+ },
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ 'X-BSTACK-OBS': 'true'
37
+ },
38
+ };
39
+ export const COLORS = {
40
+ error: 'red',
41
+ warn: 'yellow',
42
+ info: 'cyanBright',
43
+ debug: 'green',
44
+ trace: 'cyan',
45
+ progress: 'magenta'
46
+ };
47
+ /**
48
+ * get browser description for Browserstack service
49
+ * @param cap browser capablities
50
+ */
51
+ export function getBrowserDescription(cap) {
52
+ cap = cap || {};
53
+ if (cap['bstack:options']) {
54
+ cap = { ...cap, ...cap['bstack:options'] };
55
+ }
56
+ /**
57
+ * These keys describe the browser the test was run on
58
+ */
59
+ return BROWSER_DESCRIPTION
60
+ .map((k) => cap[k])
61
+ .filter(Boolean)
62
+ .join(' ');
63
+ }
64
+ /**
65
+ * get correct browser capabilities object in both multiremote and normal setups
66
+ * @param browser browser object
67
+ * @param caps browser capbilities object. In case of multiremote, the object itself should have a property named 'capabilities'
68
+ * @param browserName browser name in case of multiremote
69
+ */
70
+ export function getBrowserCapabilities(browser, caps, browserName) {
71
+ if (!browser.isMultiremote) {
72
+ return { ...browser.capabilities, ...caps };
73
+ }
74
+ const multiCaps = caps;
75
+ const globalCap = browserName && browser.getInstance(browserName) ? browser.getInstance(browserName).capabilities : {};
76
+ const cap = browserName && multiCaps[browserName] ? multiCaps[browserName].capabilities : {};
77
+ return { ...globalCap, ...cap };
78
+ }
79
+ /**
80
+ * check for browserstack W3C capabilities. Does not support legacy capabilities
81
+ * @param cap browser capabilities
82
+ */
83
+ export function isBrowserstackCapability(cap) {
84
+ return Boolean(cap &&
85
+ cap['bstack:options'] &&
86
+ // return false if the only cap in bstack:options is wdioService,
87
+ // as that is added by the service and not present in user passed caps
88
+ !(Object.keys(cap['bstack:options']).length === 1 &&
89
+ cap['bstack:options'].wdioService));
90
+ }
91
+ export function getParentSuiteName(fullTitle, testSuiteTitle) {
92
+ const fullTitleWords = fullTitle.split(' ');
93
+ const testSuiteTitleWords = testSuiteTitle.split(' ');
94
+ const shortestLength = Math.min(fullTitleWords.length, testSuiteTitleWords.length);
95
+ let c = 0;
96
+ let parentSuiteName = '';
97
+ while (c < shortestLength && fullTitleWords[c] === testSuiteTitleWords[c]) {
98
+ parentSuiteName += fullTitleWords[c++] + ' ';
99
+ }
100
+ return parentSuiteName.trim();
101
+ }
102
+ function processError(error, fn, args) {
103
+ BStackLogger.error(`Error in executing ${fn.name} with args ${args}: ${error}`);
104
+ let argsString;
105
+ try {
106
+ argsString = JSON.stringify(args);
107
+ }
108
+ catch (e) {
109
+ argsString = util.inspect(args, { depth: 2 });
110
+ }
111
+ CrashReporter.uploadCrashReport(`Error in executing ${fn.name} with args ${argsString} : ${error}`, error && error.stack);
112
+ }
113
+ export function o11yErrorHandler(fn) {
114
+ return function (...args) {
115
+ try {
116
+ let functionToHandle = fn;
117
+ if (process.env[PERF_MEASUREMENT_ENV]) {
118
+ functionToHandle = performance.timerify(functionToHandle);
119
+ }
120
+ const result = functionToHandle(...args);
121
+ if (result instanceof Promise) {
122
+ return result.catch(error => processError(error, fn, args));
123
+ }
124
+ return result;
125
+ }
126
+ catch (error) {
127
+ processError(error, fn, args);
128
+ }
129
+ };
130
+ }
131
+ export function errorHandler(fn) {
132
+ return function (...args) {
133
+ try {
134
+ const functionToHandle = fn;
135
+ const result = functionToHandle(...args);
136
+ if (result instanceof Promise) {
137
+ return result.catch(error => BStackLogger.error(`Error in executing ${fn.name} with args ${args}: ${error}`));
138
+ }
139
+ return result;
140
+ }
141
+ catch (error) {
142
+ BStackLogger.error(`Error in executing ${fn.name} with args ${args}: ${error}`);
143
+ }
144
+ };
145
+ }
146
+ export async function nodeRequest(requestType, apiEndpoint, options, apiUrl, timeout = 120000) {
147
+ try {
148
+ const response = await got(`${apiUrl}/${apiEndpoint}`, {
149
+ method: requestType,
150
+ timeout: {
151
+ request: timeout
152
+ },
153
+ ...options
154
+ }).json();
155
+ return response;
156
+ }
157
+ catch (error) {
158
+ const isLogUpload = apiEndpoint === UPLOAD_LOGS_ENDPOINT;
159
+ if (error instanceof HTTPError && error.response) {
160
+ const errorMessageJson = error.response.body ? JSON.parse(error.response.body.toString()) : null;
161
+ const errorMessage = errorMessageJson ? errorMessageJson.message : null;
162
+ if (errorMessage) {
163
+ isLogUpload ? BStackLogger.debug(`${errorMessage} - ${error.stack}`) : BStackLogger.error(`${errorMessage} - ${error.stack}`);
164
+ }
165
+ else {
166
+ isLogUpload ? BStackLogger.debug(`${error.stack}`) : BStackLogger.error(`${error.stack}`);
167
+ }
168
+ if (isLogUpload) {
169
+ return;
170
+ }
171
+ throw error;
172
+ }
173
+ else {
174
+ if (isLogUpload) {
175
+ BStackLogger.debug(`Failed to fire api request due to ${error} - ${error.stack}`);
176
+ return;
177
+ }
178
+ BStackLogger.debug(`Failed to fire api request due to ${error} - ${error.stack}`);
179
+ throw error;
180
+ }
181
+ }
182
+ }
183
+ export function o11yClassErrorHandler(errorClass) {
184
+ const prototype = errorClass.prototype;
185
+ if (Object.getOwnPropertyNames(prototype).length < 2) {
186
+ return errorClass;
187
+ }
188
+ Object.getOwnPropertyNames(prototype).forEach((methodName) => {
189
+ const method = prototype[methodName];
190
+ if (typeof method === 'function' && methodName !== 'constructor' && methodName !== 'commandWrapper') {
191
+ // In order to preserve this context, need to define like this
192
+ Object.defineProperty(prototype, methodName, {
193
+ writable: true,
194
+ value: function (...args) {
195
+ try {
196
+ const result = (process.env[PERF_MEASUREMENT_ENV] ? performance.timerify(method) : method).call(this, ...args);
197
+ if (result instanceof Promise) {
198
+ return result.catch(error => processError(error, method, args));
199
+ }
200
+ return result;
201
+ }
202
+ catch (err) {
203
+ processError(err, method, args);
204
+ }
205
+ }
206
+ });
207
+ }
208
+ });
209
+ return errorClass;
210
+ }
211
+ export const processTestObservabilityResponse = (response) => {
212
+ if (!response.observability) {
213
+ handleErrorForObservability(null);
214
+ return;
215
+ }
216
+ if (!response.observability.success) {
217
+ handleErrorForObservability(response.observability);
218
+ return;
219
+ }
220
+ process.env[BROWSERSTACK_OBSERVABILITY] = 'true';
221
+ if (response.observability.options.allow_screenshots) {
222
+ process.env[TESTOPS_SCREENSHOT_ENV] = response.observability.options.allow_screenshots.toString();
223
+ }
224
+ };
225
+ export const performO11ySync = async (browser) => {
226
+ if (isBrowserstackSession(browser)) {
227
+ await browser.execute(`browserstack_executor: ${JSON.stringify({
228
+ action: 'annotate',
229
+ arguments: {
230
+ data: `ObservabilitySync:${Date.now()}`,
231
+ level: 'debug'
232
+ }
233
+ })}`);
234
+ }
235
+ };
236
+ export const jsonifyAccessibilityArray = (dataArray, keyName, valueName) => {
237
+ const result = {};
238
+ dataArray.forEach((element) => {
239
+ result[element[keyName]] = element[valueName];
240
+ });
241
+ return result;
242
+ };
243
+ export const processAccessibilityResponse = (response, options) => {
244
+ if (!response.accessibility) {
245
+ if (options.accessibility === true) {
246
+ handleErrorForAccessibility(null);
247
+ }
248
+ return;
249
+ }
250
+ if (!response.accessibility.success) {
251
+ handleErrorForAccessibility(response.accessibility);
252
+ return;
253
+ }
254
+ if (response.accessibility.options) {
255
+ const { accessibilityToken, pollingTimeout, scannerVersion } = jsonifyAccessibilityArray(response.accessibility.options.capabilities, 'name', 'value');
256
+ const result = jsonifyAccessibilityArray(response.accessibility.options.capabilities, 'name', 'value');
257
+ const scriptsJson = {
258
+ 'scripts': jsonifyAccessibilityArray(response.accessibility.options.scripts, 'name', 'command'),
259
+ 'commands': response.accessibility.options.commandsToWrap?.commands ?? [],
260
+ 'nonBStackInfraA11yChromeOptions': result['goog:chromeOptions']
261
+ };
262
+ if (scannerVersion) {
263
+ process.env.BSTACK_A11Y_SCANNER_VERSION = scannerVersion;
264
+ BStackLogger.debug(`Accessibility scannerVersion ${scannerVersion}`);
265
+ }
266
+ if (accessibilityToken) {
267
+ process.env.BSTACK_A11Y_JWT = accessibilityToken;
268
+ process.env[BROWSERSTACK_ACCESSIBILITY] = 'true';
269
+ }
270
+ if (pollingTimeout) {
271
+ process.env.BSTACK_A11Y_POLLING_TIMEOUT = pollingTimeout;
272
+ }
273
+ if (scriptsJson) {
274
+ AccessibilityScripts.update(scriptsJson);
275
+ AccessibilityScripts.store();
276
+ }
277
+ }
278
+ };
279
+ export const processLaunchBuildResponse = (response, options) => {
280
+ if (options.testObservability) {
281
+ processTestObservabilityResponse(response);
282
+ }
283
+ processAccessibilityResponse(response, options);
284
+ };
285
+ export const launchTestSession = PerformanceTester.measureWrapper(PERFORMANCE_SDK_EVENTS.TESTHUB_EVENTS.START, o11yErrorHandler(async function launchTestSession(options, config, bsConfig, bStackConfig, accessibilityAutomation) {
286
+ const launchBuildUsage = UsageStats.getInstance().launchBuildUsage;
287
+ launchBuildUsage.triggered();
288
+ const data = {
289
+ format: 'json',
290
+ project_name: getObservabilityProject(options, bsConfig.projectName),
291
+ name: getObservabilityBuild(options, bsConfig.buildName),
292
+ build_identifier: bsConfig.buildIdentifier,
293
+ started_at: (new Date()).toISOString(),
294
+ tags: getObservabilityBuildTags(options, bsConfig.buildTag),
295
+ host_info: {
296
+ hostname: hostname(),
297
+ platform: platform(),
298
+ type: type(),
299
+ version: version(),
300
+ arch: arch()
301
+ },
302
+ ci_info: getCiInfo(),
303
+ build_run_identifier: process.env.BROWSERSTACK_BUILD_RUN_IDENTIFIER,
304
+ failed_tests_rerun: process.env[RERUN_ENV] || false,
305
+ version_control: await getGitMetaData(),
306
+ accessibility: {
307
+ settings: options.accessibilityOptions
308
+ },
309
+ browserstackAutomation: shouldAddServiceVersion(config, options.testObservability),
310
+ framework_details: {
311
+ frameworkName: WDIO_NAMING_PREFIX + config.framework,
312
+ frameworkVersion: bsConfig.bstackServiceVersion,
313
+ sdkVersion: bsConfig.bstackServiceVersion,
314
+ language: 'ECMAScript',
315
+ testFramework: {
316
+ name: 'WebdriverIO',
317
+ version: bsConfig.bstackServiceVersion
318
+ }
319
+ },
320
+ product_map: getProductMapForBuildStartCall(bStackConfig, accessibilityAutomation),
321
+ config: {},
322
+ test_orchestration: OrchestrationUtils.getInstance(config)?.getBuildStartData() || {},
323
+ test_management: {
324
+ test_plan_id: getTestPlanId(options)
325
+ }
326
+ };
327
+ if (accessibilityAutomation && (isTurboScale(options) || data.browserstackAutomation === false)) {
328
+ data.accessibility.settings ??= {};
329
+ data.accessibility.settings.includeEncodedExtension = true;
330
+ }
331
+ try {
332
+ if (Object.keys(CrashReporter.userConfigForReporting).length === 0) {
333
+ CrashReporter.userConfigForReporting = process.env.USER_CONFIG_FOR_REPORTING !== undefined ? JSON.parse(process.env.USER_CONFIG_FOR_REPORTING) : {};
334
+ }
335
+ }
336
+ catch (error) {
337
+ return BStackLogger.error(`[Crash_Report_Upload] Failed to parse user config while sending build start event due to ${error}`);
338
+ }
339
+ data.config = CrashReporter.userConfigForReporting;
340
+ try {
341
+ const url = `${APIUtils.DATA_ENDPOINT}/api/v2/builds`;
342
+ const response = await got.post(url, {
343
+ ...DEFAULT_REQUEST_CONFIG,
344
+ username: getObservabilityUser(options, config),
345
+ password: getObservabilityKey(options, config),
346
+ json: data
347
+ }).json();
348
+ delete data?.accessibility?.settings?.includeEncodedExtension;
349
+ BStackLogger.debug(`[Start_Build] Success response: ${JSON.stringify(response)}`);
350
+ BStackLogger.debug(`Test Plan Id sent in request: ${getTestPlanId(options)}`);
351
+ process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true';
352
+ if (response.jwt) {
353
+ process.env[BROWSERSTACK_TESTHUB_JWT] = response.jwt;
354
+ }
355
+ if (response.build_hashed_id) {
356
+ process.env[BROWSERSTACK_TESTHUB_UUID] = response.build_hashed_id;
357
+ TestOpsConfig.getInstance().buildHashedId = response.build_hashed_id;
358
+ BStackLogger.info(`Testhub started with id: ${TestOpsConfig.getInstance().buildHashedId}`);
359
+ }
360
+ processLaunchBuildResponse(response, options);
361
+ launchBuildUsage.success();
362
+ return response;
363
+ }
364
+ catch (error) {
365
+ if (!error.success) {
366
+ launchBuildUsage.failed(error);
367
+ logBuildError(error);
368
+ }
369
+ return null;
370
+ }
371
+ }));
372
+ export const validateCapsWithAppA11y = (platformMeta) => {
373
+ /* Check if the current driver platform is eligible for AppAccessibility scan */
374
+ BStackLogger.debug(`platformMeta ${JSON.stringify(platformMeta)}`);
375
+ if (platformMeta?.platform_name &&
376
+ String(platformMeta?.platform_name).toLowerCase() === 'android' &&
377
+ platformMeta?.platform_version &&
378
+ parseInt(platformMeta?.platform_version?.toString()) < 11) {
379
+ BStackLogger.warn('App Accessibility Automation tests are supported on OS version 11 and above for Android devices.');
380
+ return false;
381
+ }
382
+ return true;
383
+ };
384
+ export const validateCapsWithA11y = (deviceName, platformMeta, chromeOptions) => {
385
+ /* Check if the current driver platform is eligible for Accessibility scan */
386
+ try {
387
+ if (deviceName) {
388
+ BStackLogger.warn('Accessibility Automation will run only on Desktop browsers.');
389
+ return false;
390
+ }
391
+ if (platformMeta?.browser_name?.toLowerCase() !== 'chrome') {
392
+ BStackLogger.warn('Accessibility Automation will run only on Chrome browsers.');
393
+ return false;
394
+ }
395
+ const browserVersion = platformMeta?.browser_version;
396
+ if (!isUndefined(browserVersion) && !(browserVersion === 'latest' || parseFloat(browserVersion + '') > 94)) {
397
+ BStackLogger.warn('Accessibility Automation will run only on Chrome browser version greater than 94.');
398
+ return false;
399
+ }
400
+ if (chromeOptions?.args?.includes('--headless')) {
401
+ BStackLogger.warn('Accessibility Automation will not run on legacy headless mode. Switch to new headless mode or avoid using headless mode.');
402
+ return false;
403
+ }
404
+ return true;
405
+ }
406
+ catch (error) {
407
+ BStackLogger.debug(`Exception in checking capabilities compatibility with Accessibility. Error: ${error}`);
408
+ }
409
+ return false;
410
+ };
411
+ export const validateCapsWithNonBstackA11y = (browserName, browserVersion) => {
412
+ if (browserName?.toLowerCase() !== 'chrome') {
413
+ BStackLogger.warn('Accessibility Automation will run only on Chrome browsers.');
414
+ return false;
415
+ }
416
+ if (!isUndefined(browserVersion) && !(browserVersion === 'latest' || parseFloat(browserVersion + '') > 100)) {
417
+ BStackLogger.warn('Accessibility Automation will run only on Chrome browser version greater than 100.');
418
+ return false;
419
+ }
420
+ return true;
421
+ };
422
+ export const shouldScanTestForAccessibility = (suiteTitle, testTitle, accessibilityOptions, world, isCucumber) => {
423
+ try {
424
+ const includeTags = Array.isArray(accessibilityOptions?.includeTagsInTestingScope) ? accessibilityOptions?.includeTagsInTestingScope : [];
425
+ const excludeTags = Array.isArray(accessibilityOptions?.excludeTagsInTestingScope) ? accessibilityOptions?.excludeTagsInTestingScope : [];
426
+ if (isCucumber) {
427
+ const tagsList = [];
428
+ world?.pickle?.tags.map((tag) => tagsList.push(tag.name));
429
+ const excluded = excludeTags?.some((exclude) => tagsList.includes(exclude));
430
+ const included = includeTags?.length === 0 || includeTags?.some((include) => tagsList.includes(include));
431
+ return !excluded && included;
432
+ }
433
+ const fullTestName = suiteTitle + ' ' + testTitle;
434
+ const excluded = excludeTags?.some((exclude) => fullTestName.includes(exclude));
435
+ const included = includeTags?.length === 0 || includeTags?.some((include) => fullTestName.includes(include));
436
+ return !excluded && included;
437
+ }
438
+ catch (error) {
439
+ BStackLogger.debug(`Error while validating test case for accessibility before scanning. Error : ${error}`);
440
+ }
441
+ return false;
442
+ };
443
+ export const isAccessibilityAutomationSession = (accessibilityFlag) => {
444
+ try {
445
+ const hasA11yJwtToken = typeof process.env.BSTACK_A11Y_JWT === 'string' && process.env.BSTACK_A11Y_JWT.length > 0 && process.env.BSTACK_A11Y_JWT !== 'null' && process.env.BSTACK_A11Y_JWT !== 'undefined';
446
+ return accessibilityFlag && hasA11yJwtToken;
447
+ }
448
+ catch (error) {
449
+ BStackLogger.debug(`Exception in verifying the Accessibility session with error : ${error}`);
450
+ }
451
+ return false;
452
+ };
453
+ export const isAppAccessibilityAutomationSession = (accessibilityFlag, isAppAutomate) => {
454
+ const accessibilityAutomation = isAccessibilityAutomationSession(accessibilityFlag);
455
+ return accessibilityAutomation && isAppAutomate;
456
+ };
457
+ export const formatString = (template, ...values) => {
458
+ let i = 0;
459
+ if (template === null) {
460
+ return '';
461
+ }
462
+ return template.replace(/%s/g, () => {
463
+ const value = values[i++];
464
+ return value !== null && value !== undefined ? value : '';
465
+ });
466
+ };
467
+ export const _getParamsForAppAccessibility = (commandName) => {
468
+ return {
469
+ 'thTestRunUuid': process.env.TEST_ANALYTICS_ID,
470
+ 'thBuildUuid': process.env.BROWSERSTACK_TESTHUB_UUID,
471
+ 'thJwtToken': process.env.BROWSERSTACK_TESTHUB_JWT,
472
+ 'authHeader': process.env.BSTACK_A11Y_JWT,
473
+ 'scanTimestamp': Date.now(),
474
+ 'method': commandName
475
+ };
476
+ };
477
+ export const performA11yScan = async (isAppAutomate, browser, isBrowserStackSession, isAccessibility, commandName) => {
478
+ return await PerformanceTester.measureWrapper(PERFORMANCE_SDK_EVENTS.A11Y_EVENTS.PERFORM_SCAN, async () => {
479
+ if (!isAccessibilityAutomationSession(isAccessibility)) {
480
+ BStackLogger.warn('Not an Accessibility Automation session, cannot perform Accessibility scan.');
481
+ return;
482
+ }
483
+ try {
484
+ if (isAppAccessibilityAutomationSession(isAccessibility, isAppAutomate)) {
485
+ const results = await browser.execute(formatString(AccessibilityScripts.performScan, JSON.stringify(_getParamsForAppAccessibility(commandName))), {});
486
+ BStackLogger.debug(util.format(results));
487
+ return results;
488
+ }
489
+ const results = await browser.executeAsync(AccessibilityScripts.performScan, { 'method': commandName || '' });
490
+ BStackLogger.debug(util.format(results));
491
+ return results;
492
+ }
493
+ catch (err) {
494
+ BStackLogger.error('Accessibility Scan could not be performed : ' + err);
495
+ return;
496
+ }
497
+ }, { command: commandName })();
498
+ };
499
+ export const getA11yResults = PerformanceTester.measureWrapper(PERFORMANCE_SDK_EVENTS.A11Y_EVENTS.GET_RESULTS, async (isAppAutomate, browser, isBrowserStackSession, isAccessibility) => {
500
+ if (!isAccessibilityAutomationSession(isAccessibility)) {
501
+ BStackLogger.warn('Not an Accessibility Automation session, cannot retrieve Accessibility results.');
502
+ return [];
503
+ }
504
+ try {
505
+ BStackLogger.debug('Performing scan before getting results');
506
+ await performA11yScan(isAppAutomate, browser, isBrowserStackSession, isAccessibility);
507
+ const results = await browser.executeAsync(AccessibilityScripts.getResults);
508
+ return results;
509
+ }
510
+ catch (error) {
511
+ BStackLogger.error('No accessibility results were found.');
512
+ BStackLogger.debug(`getA11yResults Failed. Error: ${error}`);
513
+ return [];
514
+ }
515
+ });
516
+ export const getAppA11yResults = PerformanceTester.measureWrapper(PERFORMANCE_SDK_EVENTS.A11Y_EVENTS.GET_RESULTS, async (isAppAutomate, browser, isBrowserStackSession, isAccessibility, sessionId) => {
517
+ if (!isBrowserStackSession) {
518
+ return []; // since we are running only on Automate as of now
519
+ }
520
+ if (!isAppAccessibilityAutomationSession(isAccessibility, isAppAutomate)) {
521
+ BStackLogger.warn('Not an Accessibility Automation session, cannot retrieve Accessibility results summary.');
522
+ return [];
523
+ }
524
+ try {
525
+ const apiUrl = `${APIUtils.APP_ALLY_ENDPOINT}/${APP_ALLY_ISSUES_ENDPOINT}`;
526
+ const apiRespone = await getAppA11yResultResponse(apiUrl, isAppAutomate, browser, isBrowserStackSession, isAccessibility, sessionId);
527
+ const result = apiRespone?.data?.data?.issues;
528
+ BStackLogger.debug(`Polling Result: ${JSON.stringify(result)}`);
529
+ return result;
530
+ }
531
+ catch (error) {
532
+ BStackLogger.error('No accessibility summary was found.');
533
+ BStackLogger.debug(`getAppA11yResults Failed. Error: ${error}`);
534
+ return [];
535
+ }
536
+ });
537
+ export const getAppA11yResultsSummary = PerformanceTester.measureWrapper(PERFORMANCE_SDK_EVENTS.A11Y_EVENTS.GET_RESULTS_SUMMARY, async (isAppAutomate, browser, isBrowserStackSession, isAccessibility, sessionId) => {
538
+ if (!isBrowserStackSession) {
539
+ return {}; // since we are running only on Automate as of now
540
+ }
541
+ if (!isAppAccessibilityAutomationSession(isAccessibility, isAppAutomate)) {
542
+ BStackLogger.warn('Not an Accessibility Automation session, cannot retrieve Accessibility results summary.');
543
+ return {};
544
+ }
545
+ try {
546
+ const apiUrl = `${APIUtils.APP_ALLY_ENDPOINT}/${APP_ALLY_ISSUES_SUMMARY_ENDPOINT}`;
547
+ const apiRespone = await getAppA11yResultResponse(apiUrl, isAppAutomate, browser, isBrowserStackSession, isAccessibility, sessionId);
548
+ const result = apiRespone?.data?.data?.summary;
549
+ BStackLogger.debug(`Polling Result: ${JSON.stringify(result)}`);
550
+ return result;
551
+ }
552
+ catch {
553
+ BStackLogger.error('No accessibility summary was found.');
554
+ return {};
555
+ }
556
+ });
557
+ const getAppA11yResultResponse = async (apiUrl, isAppAutomate, browser, isBrowserStackSession, isAccessibility, sessionId) => {
558
+ BStackLogger.debug('Performing scan before getting results summary');
559
+ await performA11yScan(isAppAutomate, browser, isBrowserStackSession, isAccessibility);
560
+ const upperTimeLimit = process.env[BSTACK_A11Y_POLLING_TIMEOUT] ? Date.now() + parseInt(process.env[BSTACK_A11Y_POLLING_TIMEOUT]) * 1000 : Date.now() + 30000;
561
+ const params = { test_run_uuid: process.env.TEST_ANALYTICS_ID, session_id: sessionId, timestamp: Date.now() }; // Query params to pass
562
+ const header = { Authorization: `Bearer ${process.env.BSTACK_A11Y_JWT}` };
563
+ const apiRespone = await pollApi(apiUrl, params, header, upperTimeLimit);
564
+ BStackLogger.debug(`Polling Result: ${JSON.stringify(apiRespone)}`);
565
+ return apiRespone;
566
+ };
567
+ export const getA11yResultsSummary = PerformanceTester.measureWrapper(PERFORMANCE_SDK_EVENTS.A11Y_EVENTS.GET_RESULTS_SUMMARY, async (isAppAutomate, browser, isBrowserStackSession, isAccessibility) => {
568
+ if (!isAccessibilityAutomationSession(isAccessibility)) {
569
+ BStackLogger.warn('Not an Accessibility Automation session, cannot retrieve Accessibility results summary.');
570
+ return {};
571
+ }
572
+ try {
573
+ BStackLogger.debug('Performing scan before getting results summary');
574
+ await performA11yScan(isAppAutomate, browser, isBrowserStackSession, isAccessibility);
575
+ const summaryResults = await browser.executeAsync(AccessibilityScripts.getResultsSummary);
576
+ return summaryResults;
577
+ }
578
+ catch {
579
+ BStackLogger.error('No accessibility summary was found.');
580
+ return {};
581
+ }
582
+ });
583
+ export const stopBuildUpstream = PerformanceTester.measureWrapper(PERFORMANCE_SDK_EVENTS.TESTHUB_EVENTS.STOP, o11yErrorHandler(async function stopBuildUpstream(killSignal = null) {
584
+ const stopBuildUsage = UsageStats.getInstance().stopBuildUsage;
585
+ stopBuildUsage.triggered();
586
+ if (!process.env[TESTOPS_BUILD_COMPLETED_ENV]) {
587
+ stopBuildUsage.failed('Build is not completed yet');
588
+ return {
589
+ status: 'error',
590
+ message: 'Build is not completed yet'
591
+ };
592
+ }
593
+ const jwtToken = process.env[BROWSERSTACK_TESTHUB_JWT];
594
+ if (!jwtToken) {
595
+ stopBuildUsage.failed('Token/buildID is undefined, build creation might have failed');
596
+ BStackLogger.debug('[STOP_BUILD] Missing Authentication Token/ Build ID');
597
+ return {
598
+ status: 'error',
599
+ message: 'Token/buildID is undefined, build creation might have failed'
600
+ };
601
+ }
602
+ const data = {
603
+ 'finished_at': (new Date()).toISOString(),
604
+ 'finished_metadata': [],
605
+ };
606
+ if (killSignal) {
607
+ data.finished_metadata.push({
608
+ reason: 'user_killed',
609
+ signal: killSignal
610
+ });
611
+ }
612
+ try {
613
+ const url = `${APIUtils.DATA_ENDPOINT}/api/v1/builds/${process.env[BROWSERSTACK_TESTHUB_UUID]}/stop`;
614
+ const response = await got.put(url, {
615
+ agent: DEFAULT_REQUEST_CONFIG.agent,
616
+ headers: {
617
+ ...DEFAULT_REQUEST_CONFIG.headers,
618
+ 'Authorization': `Bearer ${jwtToken}`
619
+ },
620
+ json: data,
621
+ retry: {
622
+ limit: 3,
623
+ methods: ['GET', 'POST']
624
+ }
625
+ }).json();
626
+ BStackLogger.debug(`[STOP_BUILD] Success response: ${JSON.stringify(response)}`);
627
+ stopBuildUsage.success();
628
+ return {
629
+ status: 'success',
630
+ message: ''
631
+ };
632
+ }
633
+ catch (error) {
634
+ stopBuildUsage.failed(error);
635
+ BStackLogger.debug(`[STOP_BUILD] Failed. Error: ${error}`);
636
+ return {
637
+ status: 'error',
638
+ message: error.message
639
+ };
640
+ }
641
+ }));
642
+ export function getCiInfo() {
643
+ const env = process.env;
644
+ // Jenkins
645
+ if ((typeof env.JENKINS_URL === 'string' && env.JENKINS_URL.length > 0) || (typeof env.JENKINS_HOME === 'string' && env.JENKINS_HOME.length > 0)) {
646
+ return {
647
+ name: 'Jenkins',
648
+ build_url: env.BUILD_URL,
649
+ job_name: env.JOB_NAME,
650
+ build_number: env.BUILD_NUMBER
651
+ };
652
+ }
653
+ // CircleCI
654
+ if (isTrue(env.CI) && isTrue(env.CIRCLECI)) {
655
+ return {
656
+ name: 'CircleCI',
657
+ build_url: env.CIRCLE_BUILD_URL,
658
+ job_name: env.CIRCLE_JOB,
659
+ build_number: env.CIRCLE_BUILD_NUM
660
+ };
661
+ }
662
+ // Travis CI
663
+ if (isTrue(env.CI) && isTrue(env.TRAVIS)) {
664
+ return {
665
+ name: 'Travis CI',
666
+ build_url: env.TRAVIS_BUILD_WEB_URL,
667
+ job_name: env.TRAVIS_JOB_NAME,
668
+ build_number: env.TRAVIS_BUILD_NUMBER
669
+ };
670
+ }
671
+ // Codeship
672
+ if (isTrue(env.CI) && env.CI_NAME === 'codeship') {
673
+ return {
674
+ name: 'Codeship',
675
+ build_url: null,
676
+ job_name: null,
677
+ build_number: null
678
+ };
679
+ }
680
+ // Bitbucket
681
+ if (env.BITBUCKET_BRANCH && env.BITBUCKET_COMMIT) {
682
+ return {
683
+ name: 'Bitbucket',
684
+ build_url: env.BITBUCKET_GIT_HTTP_ORIGIN,
685
+ job_name: null,
686
+ build_number: env.BITBUCKET_BUILD_NUMBER
687
+ };
688
+ }
689
+ // Drone
690
+ if (isTrue(env.CI) && isTrue(env.DRONE)) {
691
+ return {
692
+ name: 'Drone',
693
+ build_url: env.DRONE_BUILD_LINK,
694
+ job_name: null,
695
+ build_number: env.DRONE_BUILD_NUMBER
696
+ };
697
+ }
698
+ // Semaphore
699
+ if (isTrue(env.CI) && isTrue(env.SEMAPHORE)) {
700
+ return {
701
+ name: 'Semaphore',
702
+ build_url: env.SEMAPHORE_ORGANIZATION_URL,
703
+ job_name: env.SEMAPHORE_JOB_NAME,
704
+ build_number: env.SEMAPHORE_JOB_ID
705
+ };
706
+ }
707
+ // GitLab
708
+ if (isTrue(env.CI) && isTrue(env.GITLAB_CI)) {
709
+ return {
710
+ name: 'GitLab',
711
+ build_url: env.CI_JOB_URL,
712
+ job_name: env.CI_JOB_NAME,
713
+ build_number: env.CI_JOB_ID
714
+ };
715
+ }
716
+ // Buildkite
717
+ if (isTrue(env.CI) && isTrue(env.BUILDKITE)) {
718
+ return {
719
+ name: 'Buildkite',
720
+ build_url: env.BUILDKITE_BUILD_URL,
721
+ job_name: env.BUILDKITE_LABEL || env.BUILDKITE_PIPELINE_NAME,
722
+ build_number: env.BUILDKITE_BUILD_NUMBER
723
+ };
724
+ }
725
+ // Visual Studio Team Services
726
+ if (isTrue(env.TF_BUILD) && env.TF_BUILD_BUILDNUMBER) {
727
+ return {
728
+ name: 'Visual Studio Team Services',
729
+ build_url: `${env.SYSTEM_TEAMFOUNDATIONSERVERURI}${env.SYSTEM_TEAMPROJECTID}`,
730
+ job_name: env.SYSTEM_DEFINITIONID,
731
+ build_number: env.BUILD_BUILDID
732
+ };
733
+ }
734
+ // Appveyor
735
+ if (isTrue(env.APPVEYOR)) {
736
+ return {
737
+ name: 'Appveyor',
738
+ build_url: `${env.APPVEYOR_URL}/project/${env.APPVEYOR_ACCOUNT_NAME}/${env.APPVEYOR_PROJECT_SLUG}/builds/${env.APPVEYOR_BUILD_ID}`,
739
+ job_name: env.APPVEYOR_JOB_NAME,
740
+ build_number: env.APPVEYOR_BUILD_NUMBER
741
+ };
742
+ }
743
+ // Azure CI
744
+ if (env.AZURE_HTTP_USER_AGENT && env.TF_BUILD) {
745
+ return {
746
+ name: 'Azure CI',
747
+ build_url: `${env.SYSTEM_TEAMFOUNDATIONSERVERURI}${env.SYSTEM_TEAMPROJECT}/_build/results?buildId=${env.BUILD_BUILDID}`,
748
+ job_name: env.BUILD_BUILDID,
749
+ build_number: env.BUILD_BUILDID
750
+ };
751
+ }
752
+ // AWS CodeBuild
753
+ if (env.CODEBUILD_BUILD_ID || env.CODEBUILD_RESOLVED_SOURCE_VERSION || env.CODEBUILD_SOURCE_VERSION) {
754
+ return {
755
+ name: 'AWS CodeBuild',
756
+ build_url: env.CODEBUILD_PUBLIC_BUILD_URL,
757
+ job_name: env.CODEBUILD_BUILD_ID,
758
+ build_number: env.CODEBUILD_BUILD_ID
759
+ };
760
+ }
761
+ // Bamboo
762
+ if (env.bamboo_buildNumber) {
763
+ return {
764
+ name: 'Bamboo',
765
+ build_url: env.bamboo_buildResultsUrl,
766
+ job_name: env.bamboo_shortJobName,
767
+ build_number: env.bamboo_buildNumber
768
+ };
769
+ }
770
+ // Wercker
771
+ if (env.WERCKER || env.WERCKER_MAIN_PIPELINE_STARTED) {
772
+ return {
773
+ name: 'Wercker',
774
+ build_url: env.WERCKER_BUILD_URL,
775
+ job_name: env.WERCKER_MAIN_PIPELINE_STARTED ? 'Main Pipeline' : null,
776
+ build_number: env.WERCKER_GIT_COMMIT
777
+ };
778
+ }
779
+ // Google Cloud
780
+ if (env.GCP_PROJECT || env.GCLOUD_PROJECT || env.GOOGLE_CLOUD_PROJECT) {
781
+ return {
782
+ name: 'Google Cloud',
783
+ build_url: null,
784
+ job_name: env.PROJECT_ID,
785
+ build_number: env.BUILD_ID,
786
+ };
787
+ }
788
+ // Shippable
789
+ if (env.SHIPPABLE) {
790
+ return {
791
+ name: 'Shippable',
792
+ build_url: env.SHIPPABLE_BUILD_URL,
793
+ job_name: env.SHIPPABLE_JOB_ID ? `Job #${env.SHIPPABLE_JOB_ID}` : null,
794
+ build_number: env.SHIPPABLE_BUILD_NUMBER
795
+ };
796
+ }
797
+ // Netlify
798
+ if (isTrue(env.NETLIFY)) {
799
+ return {
800
+ name: 'Netlify',
801
+ build_url: env.DEPLOY_URL,
802
+ job_name: env.SITE_NAME,
803
+ build_number: env.BUILD_ID
804
+ };
805
+ }
806
+ // Github Actions
807
+ if (isTrue(env.GITHUB_ACTIONS)) {
808
+ return {
809
+ name: 'GitHub Actions',
810
+ build_url: `${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}/actions/runs/${env.GITHUB_RUN_ID}`,
811
+ job_name: env.GITHUB_WORKFLOW,
812
+ build_number: env.GITHUB_RUN_ID,
813
+ };
814
+ }
815
+ // Vercel
816
+ if (isTrue(env.CI) && env.VERCEL === '1') {
817
+ return {
818
+ name: 'Vercel',
819
+ build_url: `http://${env.VERCEL_URL}`,
820
+ job_name: null,
821
+ build_number: null,
822
+ };
823
+ }
824
+ // Teamcity
825
+ if (env.TEAMCITY_VERSION) {
826
+ return {
827
+ name: 'Teamcity',
828
+ build_url: null,
829
+ job_name: null,
830
+ build_number: env.BUILD_NUMBER,
831
+ };
832
+ }
833
+ // Concourse
834
+ if (env.CONCOURSE || env.CONCOURSE_URL || env.CONCOURSE_USERNAME || env.CONCOURSE_TEAM) {
835
+ return {
836
+ name: 'Concourse',
837
+ build_url: null,
838
+ job_name: env.BUILD_JOB_NAME || null,
839
+ build_number: env.BUILD_ID || null,
840
+ };
841
+ }
842
+ // GoCD
843
+ if (env.GO_JOB_NAME) {
844
+ return {
845
+ name: 'GoCD',
846
+ build_url: null,
847
+ job_name: env.GO_JOB_NAME,
848
+ build_number: env.GO_PIPELINE_COUNTER,
849
+ };
850
+ }
851
+ // CodeFresh
852
+ if (env.CF_BUILD_ID) {
853
+ return {
854
+ name: 'CodeFresh',
855
+ build_url: env.CF_BUILD_URL,
856
+ job_name: env.CF_PIPELINE_NAME,
857
+ build_number: env.CF_BUILD_ID,
858
+ };
859
+ }
860
+ // if no matches, return null
861
+ return null;
862
+ }
863
+ export async function getGitMetaData() {
864
+ const info = gitRepoInfo();
865
+ if (!info.commonGitDir) {
866
+ return;
867
+ }
868
+ const { remote } = await pGitconfig(info.commonGitDir);
869
+ const remotes = remote ? Object.keys(remote).map(remoteName => ({ name: remoteName, url: remote[remoteName].url })) : [];
870
+ let gitMetaData = {
871
+ name: 'git',
872
+ sha: info.sha,
873
+ short_sha: info.abbreviatedSha,
874
+ branch: info.branch,
875
+ tag: info.tag,
876
+ committer: info.committer,
877
+ committer_date: info.committerDate,
878
+ author: info.author,
879
+ author_date: info.authorDate,
880
+ commit_message: info.commitMessage,
881
+ root: info.root,
882
+ common_git_dir: info.commonGitDir,
883
+ worktree_git_dir: info.worktreeGitDir,
884
+ last_tag: info.lastTag,
885
+ commits_since_last_tag: info.commitsSinceLastTag,
886
+ remotes: remotes
887
+ };
888
+ gitMetaData = checkAndTruncateVCSInfo(gitMetaData);
889
+ return gitMetaData;
890
+ }
891
+ export function getUniqueIdentifier(test, framework) {
892
+ if (framework === 'jasmine') {
893
+ return test.fullName;
894
+ }
895
+ let parentTitle = test.parent;
896
+ // Sometimes parent will be an object instead of a string
897
+ if (typeof parentTitle === 'object') {
898
+ parentTitle = parentTitle.title;
899
+ }
900
+ return `${parentTitle} - ${test.title}`;
901
+ }
902
+ export function getUniqueIdentifierForCucumber(world) {
903
+ return world.pickle.uri + '_' + world.pickle.astNodeIds.join(',');
904
+ }
905
+ export function getCloudProvider(browser) {
906
+ if (browser && 'instances' in browser) {
907
+ // Loop through all instances
908
+ for (const instanceName of browser.instances) {
909
+ const instance = browser[instanceName];
910
+ if (instance.options && instance.options.hostname && instance.options.hostname.includes('browserstack')) {
911
+ return 'browserstack';
912
+ }
913
+ }
914
+ }
915
+ else if (browser.options && browser.options.hostname && browser.options.hostname.includes('browserstack')) { // Single browser instance
916
+ return 'browserstack';
917
+ }
918
+ return 'unknown_grid';
919
+ }
920
+ export function isBrowserstackSession(browser) {
921
+ return browser && getCloudProvider(browser).toLowerCase() === 'browserstack';
922
+ }
923
+ /**
924
+ * Sets a BrowserStack annotation with the provided message
925
+ * @param browser - The WebDriver browser instance
926
+ * @param message - The annotation message to set
927
+ * @param isAccessibilityEnabled - Whether accessibility is enabled for the session
928
+ */
929
+ export async function setBrowserstackAnnotation(browser, message, isAccessibilityEnabled) {
930
+ if (isAccessibilityEnabled && isBrowserstackSession(browser)) {
931
+ await browser.execute(`browserstack_executor: ${JSON.stringify({
932
+ action: 'annotate',
933
+ arguments: {
934
+ data: message,
935
+ level: 'info'
936
+ }
937
+ })}`);
938
+ }
939
+ }
940
+ export function getScenarioExamples(world) {
941
+ const scenario = world.pickle;
942
+ // no examples present
943
+ if ((scenario.astNodeIds && scenario.astNodeIds.length <= 1) || scenario.astNodeIds === undefined) {
944
+ return;
945
+ }
946
+ const pickleId = scenario.astNodeIds[0];
947
+ const examplesId = scenario.astNodeIds[1];
948
+ const gherkinDocumentChildren = world.gherkinDocument.feature?.children;
949
+ let examples = [];
950
+ gherkinDocumentChildren?.forEach(child => {
951
+ if (child.rule) {
952
+ // handle if rule is present
953
+ child.rule.children.forEach(childLevel2 => {
954
+ if (childLevel2.scenario && childLevel2.scenario.id === pickleId && childLevel2.scenario.examples) {
955
+ const passedExamples = childLevel2.scenario.examples.flatMap((val) => (val.tableBody)).find((item) => item.id === examplesId)?.cells.map((val) => (val.value));
956
+ if (passedExamples) {
957
+ examples = passedExamples;
958
+ }
959
+ }
960
+ });
961
+ }
962
+ else if (child.scenario && child.scenario.id === pickleId && child.scenario.examples) {
963
+ // handle if scenario outside rule
964
+ const passedExamples = child.scenario.examples.flatMap((val) => (val.tableBody)).find((item) => item.id === examplesId)?.cells.map((val) => (val.value));
965
+ if (passedExamples) {
966
+ examples = passedExamples;
967
+ }
968
+ }
969
+ });
970
+ if (examples.length) {
971
+ return examples;
972
+ }
973
+ return;
974
+ }
975
+ export function removeAnsiColors(message) {
976
+ if (!message) {
977
+ return '';
978
+ }
979
+ // https://stackoverflow.com/a/29497680
980
+ // eslint-disable-next-line no-control-regex
981
+ return message.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
982
+ }
983
+ export function getLogTag(eventType) {
984
+ if (eventType === 'TestRunStarted' || eventType === 'TestRunFinished') {
985
+ return 'Test_Upload';
986
+ }
987
+ else if (eventType === 'HookRunStarted' || eventType === 'HookRunFinished') {
988
+ return 'Hook_Upload';
989
+ }
990
+ else if (eventType === 'ScreenshotCreated') {
991
+ return 'Screenshot_Upload';
992
+ }
993
+ else if (eventType === 'LogCreated') {
994
+ return 'Log_Upload';
995
+ }
996
+ return 'undefined';
997
+ }
998
+ // get hierarchy for a particular test (called by reporter for skipped tests)
999
+ export function getHierarchy(fullTitle) {
1000
+ if (!fullTitle) {
1001
+ return [];
1002
+ }
1003
+ return fullTitle.split('.').slice(0, -1);
1004
+ }
1005
+ export function getHookType(hookName) {
1006
+ if (hookName.startsWith('"before each"')) {
1007
+ return 'BEFORE_EACH';
1008
+ }
1009
+ else if (hookName.startsWith('"before all"')) {
1010
+ return 'BEFORE_ALL';
1011
+ }
1012
+ else if (hookName.startsWith('"after each"')) {
1013
+ return 'AFTER_EACH';
1014
+ }
1015
+ else if (hookName.startsWith('"after all"')) {
1016
+ return 'AFTER_ALL';
1017
+ }
1018
+ return 'unknown';
1019
+ }
1020
+ export function isScreenshotCommand(args) {
1021
+ return args.endpoint && args.endpoint.includes('/screenshot');
1022
+ }
1023
+ export function isBStackSession(config) {
1024
+ if (typeof config.user === 'string' && typeof config.key === 'string' && config.key.length === 20) {
1025
+ return true;
1026
+ }
1027
+ return false;
1028
+ }
1029
+ export function isBrowserstackInfra(config, caps) {
1030
+ // a utility function to check if the hostname is browserstack
1031
+ const isBrowserstack = (str) => {
1032
+ return str.includes('browserstack.com');
1033
+ };
1034
+ if ((config.hostname) && !isBrowserstack(config.hostname)) {
1035
+ return false;
1036
+ }
1037
+ if (caps && typeof caps === 'object') {
1038
+ if (Array.isArray(caps)) {
1039
+ for (const capability of caps) {
1040
+ if ((capability.hostname) && !isBrowserstack(capability.hostname)) {
1041
+ return false;
1042
+ }
1043
+ }
1044
+ }
1045
+ else {
1046
+ for (const key in caps) {
1047
+ const capability = caps[key];
1048
+ if ((capability.hostname) && !isBrowserstack(capability.hostname)) {
1049
+ return false;
1050
+ }
1051
+ }
1052
+ }
1053
+ }
1054
+ if (!isBStackSession(config)) {
1055
+ return false;
1056
+ }
1057
+ return true;
1058
+ }
1059
+ export function getBrowserStackUserAndKey(config, options) {
1060
+ // Fallback 1: Env variables
1061
+ // Fallback 2: Service variables in wdio.conf.js (that are received inside options object)
1062
+ const envOrServiceVariables = {
1063
+ user: getBrowserStackUser(options),
1064
+ key: getBrowserStackKey(options)
1065
+ };
1066
+ if (isBStackSession(envOrServiceVariables)) {
1067
+ return envOrServiceVariables;
1068
+ }
1069
+ // Fallback 3: Service variables in testObservabilityOptions object
1070
+ // Fallback 4: Service variables in the top level config object
1071
+ const o11yVariables = {
1072
+ user: getObservabilityUser(options, config),
1073
+ key: getObservabilityKey(options, config)
1074
+ };
1075
+ if (isBStackSession(o11yVariables)) {
1076
+ return o11yVariables;
1077
+ }
1078
+ }
1079
+ export function shouldAddServiceVersion(config, testObservability, caps) {
1080
+ if ((config.services && config.services.toString().includes('chromedriver') && testObservability !== false) || !isBrowserstackInfra(config, caps)) {
1081
+ return false;
1082
+ }
1083
+ return true;
1084
+ }
1085
+ export async function batchAndPostEvents(eventUrl, kind, data) {
1086
+ if (!process.env[TESTOPS_BUILD_COMPLETED_ENV]) {
1087
+ throw new Error('Build not completed yet');
1088
+ }
1089
+ const jwtToken = process.env[BROWSERSTACK_TESTHUB_JWT];
1090
+ if (!jwtToken) {
1091
+ throw new Error('Missing authentication Token');
1092
+ }
1093
+ try {
1094
+ const url = `${APIUtils.DATA_ENDPOINT}/${eventUrl}`;
1095
+ const response = await got.post(url, {
1096
+ agent: DEFAULT_REQUEST_CONFIG.agent,
1097
+ headers: {
1098
+ ...DEFAULT_REQUEST_CONFIG.headers,
1099
+ 'Authorization': `Bearer ${jwtToken}`
1100
+ },
1101
+ json: data,
1102
+ retry: {
1103
+ limit: 3,
1104
+ methods: ['GET', 'POST']
1105
+ }
1106
+ }).json();
1107
+ BStackLogger.debug(`[${kind}] Success response: ${JSON.stringify(response)}`);
1108
+ }
1109
+ catch (error) {
1110
+ BStackLogger.debug(`[${kind}] EXCEPTION IN ${kind} REQUEST TO TEST Reporting and Analytics : ${error}`);
1111
+ throw new Error('Exception in request ' + error);
1112
+ }
1113
+ }
1114
+ export function normalizeTestReportingConfig(_options) {
1115
+ if (!isUndefined(_options.testReporting)) {
1116
+ _options.testObservability = _options.testReporting;
1117
+ }
1118
+ if (!isUndefined(_options.testReportingOptions)) {
1119
+ _options.testObservabilityOptions = _options.testReportingOptions;
1120
+ }
1121
+ }
1122
+ export function normalizeTestReportingEnvVariables() {
1123
+ if (!isUndefined(process.env[BROWSERSTACK_TEST_REPORTING])) {
1124
+ process.env[BROWSERSTACK_OBSERVABILITY] = process.env[BROWSERSTACK_TEST_REPORTING];
1125
+ }
1126
+ if (!isUndefined(process.env[TEST_REPORTING_PROJECT_NAME])) {
1127
+ process.env.TEST_OBSERVABILITY_PROJECT_NAME = process.env[TEST_REPORTING_PROJECT_NAME];
1128
+ }
1129
+ if (!isUndefined(process.env.TEST_REPORTING_BUILD_NAME)) {
1130
+ process.env.TEST_OBSERVABILITY_BUILD_NAME = process.env.TEST_REPORTING_BUILD_NAME;
1131
+ }
1132
+ if (!isUndefined(process.env.TEST_REPORTING_BUILD_TAG)) {
1133
+ process.env.TEST_OBSERVABILITY_BUILD_TAG = process.env.TEST_REPORTING_BUILD_TAG;
1134
+ }
1135
+ }
1136
+ export function getObservabilityUser(options, config) {
1137
+ if (process.env.BROWSERSTACK_USERNAME) {
1138
+ return process.env.BROWSERSTACK_USERNAME;
1139
+ }
1140
+ if (options.testObservabilityOptions && options.testObservabilityOptions.user) {
1141
+ return options.testObservabilityOptions.user;
1142
+ }
1143
+ return config.user;
1144
+ }
1145
+ export function getObservabilityKey(options, config) {
1146
+ if (process.env.BROWSERSTACK_ACCESS_KEY) {
1147
+ return process.env.BROWSERSTACK_ACCESS_KEY;
1148
+ }
1149
+ if (options.testObservabilityOptions && options.testObservabilityOptions.key) {
1150
+ return options.testObservabilityOptions.key;
1151
+ }
1152
+ return config.key;
1153
+ }
1154
+ export function getObservabilityProject(options, bstackProjectName) {
1155
+ if (process.env.TEST_OBSERVABILITY_PROJECT_NAME) {
1156
+ return process.env.TEST_OBSERVABILITY_PROJECT_NAME;
1157
+ }
1158
+ if (options.testObservabilityOptions && options.testObservabilityOptions.projectName) {
1159
+ return options.testObservabilityOptions.projectName;
1160
+ }
1161
+ return bstackProjectName;
1162
+ }
1163
+ export function getObservabilityBuild(options, bstackBuildName) {
1164
+ if (process.env.TEST_OBSERVABILITY_BUILD_NAME) {
1165
+ return process.env.TEST_OBSERVABILITY_BUILD_NAME;
1166
+ }
1167
+ if (options.testObservabilityOptions && options.testObservabilityOptions.buildName) {
1168
+ return options.testObservabilityOptions.buildName;
1169
+ }
1170
+ return bstackBuildName || path.basename(path.resolve(process.cwd()));
1171
+ }
1172
+ export function getObservabilityBuildTags(options, bstackBuildTag) {
1173
+ if (process.env.TEST_OBSERVABILITY_BUILD_TAG) {
1174
+ return process.env.TEST_OBSERVABILITY_BUILD_TAG.split(',');
1175
+ }
1176
+ if (options.testObservabilityOptions && options.testObservabilityOptions.buildTag) {
1177
+ return options.testObservabilityOptions.buildTag;
1178
+ }
1179
+ if (bstackBuildTag) {
1180
+ return [bstackBuildTag];
1181
+ }
1182
+ return [];
1183
+ }
1184
+ export function getTestPlanId(options) {
1185
+ if (process.env[BROWSERSTACK_TEST_PLAN_ID]) {
1186
+ return process.env[BROWSERSTACK_TEST_PLAN_ID];
1187
+ }
1188
+ const CLI_ARG = '--browserstack.testManagementOptions.testPlanId';
1189
+ const argIndex = process.argv.indexOf(CLI_ARG);
1190
+ if (argIndex !== -1 && process.argv[argIndex + 1]) {
1191
+ return process.argv[argIndex + 1];
1192
+ }
1193
+ const argWithEquals = process.argv.find((arg) => arg.startsWith(`${CLI_ARG}=`));
1194
+ if (argWithEquals) {
1195
+ return argWithEquals.split('=')[1];
1196
+ }
1197
+ const testPlanId = options.testManagementOptions?.testPlanId;
1198
+ if (typeof testPlanId === 'string' && testPlanId.trim().length > 0) {
1199
+ return testPlanId.trim();
1200
+ }
1201
+ return undefined;
1202
+ }
1203
+ export function getBrowserStackUser(config) {
1204
+ if (process.env.BROWSERSTACK_USERNAME) {
1205
+ return process.env.BROWSERSTACK_USERNAME;
1206
+ }
1207
+ return config.user;
1208
+ }
1209
+ export function getBrowserStackKey(config) {
1210
+ if (process.env.BROWSERSTACK_ACCESS_KEY) {
1211
+ return process.env.BROWSERSTACK_ACCESS_KEY;
1212
+ }
1213
+ return config.key;
1214
+ }
1215
+ export function isUndefined(value) {
1216
+ let res = (value === undefined || value === null);
1217
+ if (typeof value === 'string') {
1218
+ res = res || value === '';
1219
+ }
1220
+ return res;
1221
+ }
1222
+ export function isTrue(value) {
1223
+ return (value + '').toLowerCase() === 'true';
1224
+ }
1225
+ export function isFalse(value) {
1226
+ return (value + '').toLowerCase() === 'false';
1227
+ }
1228
+ export function frameworkSupportsHook(hook, framework) {
1229
+ if (framework === 'mocha' && (hook === 'before' || hook === 'after' || hook === 'beforeEach' || hook === 'afterEach')) {
1230
+ return true;
1231
+ }
1232
+ if (framework === 'cucumber') {
1233
+ return true;
1234
+ }
1235
+ return false;
1236
+ }
1237
+ export const patchConsoleLogs = o11yErrorHandler(() => {
1238
+ const BSTestOpsPatcher = new logPatcher({});
1239
+ Object.keys(consoleHolder).forEach((method) => {
1240
+ if (!(method in console) || typeof console[method] !== 'function') {
1241
+ BStackLogger.debug(`Skipping method: ${method}, exists: ${method in console}, type: ${typeof console[method]}`);
1242
+ return;
1243
+ }
1244
+ const origMethod = console[method].bind(console);
1245
+ // Make sure we don't override Constructors
1246
+ // Arrow functions are not construable
1247
+ if (typeof console[method] === 'function' && method !== 'Console') {
1248
+ console[method] = (...args) => {
1249
+ try {
1250
+ if (!Object.keys(BSTestOpsPatcher).includes(method)) {
1251
+ origMethod(...args);
1252
+ }
1253
+ else {
1254
+ origMethod(...args);
1255
+ BSTestOpsPatcher[method](...args);
1256
+ }
1257
+ }
1258
+ catch (error) {
1259
+ origMethod(...args);
1260
+ }
1261
+ };
1262
+ }
1263
+ });
1264
+ });
1265
+ export function getFailureObject(error) {
1266
+ const stack = error.stack;
1267
+ const message = typeof error === 'string' ? error : error.message;
1268
+ const backtrace = stack ? removeAnsiColors(stack.toString()) : '';
1269
+ return {
1270
+ failure: [{ backtrace: [backtrace] }],
1271
+ failure_reason: removeAnsiColors(message.toString()),
1272
+ failure_type: message ? (message.toString().match(/AssertionError/) ? 'AssertionError' : 'UnhandledError') : null
1273
+ };
1274
+ }
1275
+ export function truncateString(field, truncateSizeInBytes) {
1276
+ try {
1277
+ const bufferSizeInBytes = Buffer.from(GIT_META_DATA_TRUNCATED).length;
1278
+ const fieldBufferObj = Buffer.from(field);
1279
+ const lenOfFieldBufferObj = fieldBufferObj.length;
1280
+ const finalLen = Math.ceil(lenOfFieldBufferObj - truncateSizeInBytes - bufferSizeInBytes);
1281
+ if (finalLen > 0) {
1282
+ const truncatedString = fieldBufferObj.subarray(0, finalLen).toString() + GIT_META_DATA_TRUNCATED;
1283
+ return truncatedString;
1284
+ }
1285
+ }
1286
+ catch (error) {
1287
+ BStackLogger.debug(`Error while truncating field, nothing was truncated here: ${error}`);
1288
+ }
1289
+ return field;
1290
+ }
1291
+ export function getSizeOfJsonObjectInBytes(jsonData) {
1292
+ try {
1293
+ const buffer = Buffer.from(JSON.stringify(jsonData));
1294
+ return buffer.length;
1295
+ }
1296
+ catch (error) {
1297
+ BStackLogger.debug(`Something went wrong while calculating size of JSON object: ${error}`);
1298
+ }
1299
+ return -1;
1300
+ }
1301
+ export function checkAndTruncateVCSInfo(gitMetaData) {
1302
+ const gitMetaDataSizeInBytes = getSizeOfJsonObjectInBytes(gitMetaData);
1303
+ if (gitMetaDataSizeInBytes && gitMetaDataSizeInBytes > MAX_GIT_META_DATA_SIZE_IN_BYTES) {
1304
+ const truncateSize = gitMetaDataSizeInBytes - MAX_GIT_META_DATA_SIZE_IN_BYTES;
1305
+ const truncatedCommitMessage = truncateString(gitMetaData.commit_message, truncateSize);
1306
+ gitMetaData.commit_message = truncatedCommitMessage;
1307
+ BStackLogger.info(`The commit has been truncated. Size of commit after truncation is ${getSizeOfJsonObjectInBytes(gitMetaData) / 1024} KB`);
1308
+ }
1309
+ return gitMetaData;
1310
+ }
1311
+ export const sleep = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms));
1312
+ export async function uploadLogs(user, key, clientBuildUuid) {
1313
+ try {
1314
+ if (!user || !key) {
1315
+ BStackLogger.debug('Uploading logs failed due to no credentials');
1316
+ return;
1317
+ }
1318
+ const tmpDir = '/tmp';
1319
+ const tarPath = path.join(tmpDir, 'logs.tar');
1320
+ const tarGzPath = path.join(tmpDir, 'logs.tar.gz');
1321
+ const filesToArchive = [
1322
+ BStackLogger.logFilePath,
1323
+ CLI_DEBUG_LOGS_FILE,
1324
+ ].filter(f => fs.existsSync(f));
1325
+ const copiedFileNames = [];
1326
+ for (const f of filesToArchive) {
1327
+ const dest = path.join(tmpDir, path.basename(f));
1328
+ fs.copyFileSync(f, dest);
1329
+ copiedFileNames.push(path.basename(f));
1330
+ }
1331
+ await tar.create({
1332
+ file: tarPath,
1333
+ cwd: tmpDir,
1334
+ portable: true,
1335
+ noDirRecurse: true
1336
+ }, copiedFileNames);
1337
+ await new Promise((resolve, reject) => {
1338
+ const source = fs.createReadStream(tarPath);
1339
+ const dest = fs.createWriteStream(tarGzPath);
1340
+ const gzip = zlib.createGzip({ level: 1 });
1341
+ source.pipe(gzip).pipe(dest);
1342
+ dest.on('finish', resolve);
1343
+ dest.on('error', reject);
1344
+ });
1345
+ const formData = new FormData();
1346
+ const file = await fileFromPath(tarGzPath);
1347
+ formData.append('data', file, 'logs.tar.gz');
1348
+ formData.append('clientBuildUuid', clientBuildUuid);
1349
+ const requestOptions = {
1350
+ body: formData,
1351
+ username: user,
1352
+ password: key
1353
+ };
1354
+ const response = await nodeRequest('POST', UPLOAD_LOGS_ENDPOINT, requestOptions, APIUtils.UPLOAD_LOGS_ADDRESS);
1355
+ fs.unlinkSync(tarPath);
1356
+ fs.unlinkSync(tarGzPath);
1357
+ for (const f of copiedFileNames) {
1358
+ const filePath = path.join(tmpDir, f);
1359
+ if (fs.existsSync(filePath)) {
1360
+ fs.unlinkSync(filePath);
1361
+ }
1362
+ }
1363
+ // Delete the SDK CLI log file after upload
1364
+ if (fs.existsSync(CLI_DEBUG_LOGS_FILE)) {
1365
+ fs.unlinkSync(CLI_DEBUG_LOGS_FILE);
1366
+ }
1367
+ return response;
1368
+ }
1369
+ catch (error) {
1370
+ BStackLogger.error(`Error while uploading logs: ${getErrorString(error)}`);
1371
+ return null;
1372
+ }
1373
+ }
1374
+ export const isObject = (object) => {
1375
+ return object !== null && typeof object === 'object' && !Array.isArray(object);
1376
+ };
1377
+ export const ObjectsAreEqual = (object1, object2) => {
1378
+ const objectKeys1 = Object.keys(object1);
1379
+ const objectKeys2 = Object.keys(object2);
1380
+ if (objectKeys1.length !== objectKeys2.length) {
1381
+ return false;
1382
+ }
1383
+ for (const key of objectKeys1) {
1384
+ const value1 = object1[key];
1385
+ const value2 = object2[key];
1386
+ const isBothAreObjects = isObject(value1) && isObject(value2);
1387
+ if ((isBothAreObjects && !ObjectsAreEqual(value1, value2)) || (!isBothAreObjects && value1 !== value2)) {
1388
+ return false;
1389
+ }
1390
+ }
1391
+ return true;
1392
+ };
1393
+ export const getPlatformVersion = o11yErrorHandler(function getPlatformVersion(caps, userCaps) {
1394
+ if (!caps && !userCaps) {
1395
+ return undefined;
1396
+ }
1397
+ const bstackOptions = (userCaps)?.['bstack:options'];
1398
+ const keys = ['platformVersion', 'platform_version', 'osVersion', 'os_version'];
1399
+ for (const key of keys) {
1400
+ if (caps?.[key]) {
1401
+ BStackLogger.debug(`Got ${key} from driver caps`);
1402
+ return String(caps?.[key]);
1403
+ }
1404
+ else if (bstackOptions && bstackOptions?.[key]) {
1405
+ BStackLogger.debug(`Got ${key} from user bstack options`);
1406
+ return String(bstackOptions?.[key]);
1407
+ }
1408
+ else if (userCaps[key]) {
1409
+ BStackLogger.debug(`Got ${key} from user caps`);
1410
+ return String(userCaps[key]);
1411
+ }
1412
+ }
1413
+ return undefined;
1414
+ });
1415
+ export const isObjectEmpty = (objectName) => {
1416
+ return (objectName &&
1417
+ Object.keys(objectName).length === 0 &&
1418
+ objectName.constructor === Object);
1419
+ };
1420
+ export const getErrorString = (err) => {
1421
+ if (!err) {
1422
+ return undefined;
1423
+ }
1424
+ if (typeof err === 'string') {
1425
+ return err; // works, `e` narrowed to string
1426
+ }
1427
+ else if (err instanceof Error) {
1428
+ return err.message; // works, `e` narrowed to Error
1429
+ }
1430
+ };
1431
+ export function isTurboScale(options) {
1432
+ return Boolean(options?.turboScale);
1433
+ }
1434
+ export function getObservabilityProduct(options, isAppAutomate) {
1435
+ return isAppAutomate
1436
+ ? 'app-automate'
1437
+ : (isTurboScale(options) ? 'turboscale' : 'automate');
1438
+ }
1439
+ export const hasBrowserName = (cap) => {
1440
+ if (!cap || !cap.capabilities) {
1441
+ return false;
1442
+ }
1443
+ const browserStackCapabilities = cap.capabilities;
1444
+ return browserStackCapabilities.browserName !== undefined;
1445
+ };
1446
+ export const isValidCapsForHealing = (caps) => {
1447
+ // Get all capability values
1448
+ const capValues = Object.values(caps);
1449
+ // Check if there are any capabilities and if at least one has a browser name
1450
+ return capValues.length > 0 && capValues.some(hasBrowserName);
1451
+ };
1452
+ export async function pollApi(url, params, headers, upperLimit, startTime = Date.now()) {
1453
+ params.timestamp = Math.round(Date.now() / 1000);
1454
+ BStackLogger.debug(`current timestamp ${params.timestamp}`);
1455
+ try {
1456
+ const response = await got(url, {
1457
+ searchParams: params,
1458
+ headers,
1459
+ });
1460
+ const responseData = JSON.parse(response.body);
1461
+ return {
1462
+ data: responseData,
1463
+ headers: response.headers,
1464
+ message: 'Polling succeeded.',
1465
+ };
1466
+ }
1467
+ catch (error) {
1468
+ if (error.response && error.response.statusCode === 404) {
1469
+ const nextPollTime = parseInt(error.response.headers.next_poll_time, 10) * 1000;
1470
+ BStackLogger.debug(`timeInMillis ${nextPollTime}`);
1471
+ if (isNaN(nextPollTime)) {
1472
+ BStackLogger.warn('Invalid or missing `nextPollTime` header. Stopping polling.');
1473
+ return {
1474
+ data: {},
1475
+ headers: error.response.headers,
1476
+ message: 'Invalid nextPollTime header value. Polling stopped.',
1477
+ };
1478
+ }
1479
+ const elapsedTime = nextPollTime - Date.now();
1480
+ BStackLogger.debug(`elapsedTime ${elapsedTime} timeInMillis ${nextPollTime} upperLimit ${upperLimit}`);
1481
+ // Stop polling if the upper time limit is reached
1482
+ if (nextPollTime > upperLimit) {
1483
+ BStackLogger.warn('Polling stopped due to upper time limit.');
1484
+ return {
1485
+ data: {},
1486
+ headers: error.response.headers,
1487
+ message: 'Polling stopped due to upper time limit.',
1488
+ };
1489
+ }
1490
+ BStackLogger.debug(`Polling again in ${elapsedTime}ms with params:`, params);
1491
+ // Wait for the specified time and poll again
1492
+ await new Promise((resolve) => setTimeout(resolve, elapsedTime));
1493
+ return pollApi(url, params, headers, upperLimit, startTime);
1494
+ }
1495
+ else if (error.response) {
1496
+ throw {
1497
+ data: {},
1498
+ headers: {},
1499
+ message: error.response.body ? JSON.parse(error.response.body).message : 'Unknown error',
1500
+ };
1501
+ }
1502
+ else {
1503
+ BStackLogger.error(`Unexpected error occurred: ${error}`);
1504
+ return { data: {}, headers: {}, message: 'Unexpected error occurred.' };
1505
+ }
1506
+ }
1507
+ }
1508
+ export function getBooleanValueFromString(value) {
1509
+ if (!value) {
1510
+ return false;
1511
+ }
1512
+ return ['true'].includes(value.trim().toLowerCase());
1513
+ }
1514
+ /**
1515
+ * Validate boolean value from mixed types (boolean or string)
1516
+ * Only accepts true boolean or string "true" (case insensitive)
1517
+ */
1518
+ export function isValidEnabledValue(value) {
1519
+ if (value === undefined || value === null) {
1520
+ return false;
1521
+ }
1522
+ if (typeof value === 'boolean') {
1523
+ return value === true;
1524
+ }
1525
+ if (typeof value === 'string') {
1526
+ return getBooleanValueFromString(value);
1527
+ }
1528
+ return false;
1529
+ }
1530
+ export function mergeDeep(target, ...sources) {
1531
+ if (!sources.length) {
1532
+ return target;
1533
+ }
1534
+ const source = sources.shift();
1535
+ if (isObject(target) && isObject(source)) {
1536
+ for (const key in source) {
1537
+ const sourceValue = source[key];
1538
+ const targetValue = target[key];
1539
+ if (isObject(sourceValue)) {
1540
+ if (!targetValue || !isObject(targetValue)) {
1541
+ target[key] = {};
1542
+ }
1543
+ mergeDeep(target[key], sourceValue);
1544
+ }
1545
+ else {
1546
+ target[key] = sourceValue;
1547
+ }
1548
+ }
1549
+ }
1550
+ return mergeDeep(target, ...sources);
1551
+ }
1552
+ export function mergeChromeOptions(base, override) {
1553
+ const merged = { ...base };
1554
+ if (override.args) {
1555
+ merged.args = [...(base.args || []), ...override.args];
1556
+ }
1557
+ if (override.extensions) {
1558
+ merged.extensions = [...(base.extensions || []), ...override.extensions];
1559
+ }
1560
+ if (override.prefs) {
1561
+ merged.prefs = mergeDeep({ ...(base.prefs || {}) }, override.prefs);
1562
+ }
1563
+ return merged;
1564
+ }
1565
+ export function isNullOrEmpty(string) {
1566
+ return !string || string.trim() === '';
1567
+ }
1568
+ export function isHash(entity) {
1569
+ return Boolean(entity && typeof (entity) === 'object' && !Array.isArray(entity));
1570
+ }
1571
+ export function nestedKeyValue(hash, keys) {
1572
+ return keys.reduce((hash, key) => (isHash(hash) ? hash[key] : undefined), hash);
1573
+ }
1574
+ export function removeDir(dir) {
1575
+ const list = fs.readdirSync(dir);
1576
+ for (let i = 0; i < list.length; i++) {
1577
+ const filename = path.join(dir, list[i]);
1578
+ const stat = fs.statSync(filename);
1579
+ if (filename === '.' || filename === '..') {
1580
+ // pass these files
1581
+ }
1582
+ else if (stat.isDirectory()) {
1583
+ // rmdir recursively
1584
+ removeDir(filename);
1585
+ }
1586
+ else {
1587
+ // rm filename
1588
+ fs.unlinkSync(filename);
1589
+ }
1590
+ }
1591
+ fs.rmdirSync(dir);
1592
+ }
1593
+ export function createDir(dir) {
1594
+ if (fs.existsSync(dir)) {
1595
+ removeDir(dir);
1596
+ }
1597
+ fs.mkdirSync(dir, { recursive: true });
1598
+ }
1599
+ export function isWritable(dirPath) {
1600
+ try {
1601
+ fs.accessSync(dirPath, fs.constants.W_OK);
1602
+ return true;
1603
+ }
1604
+ catch {
1605
+ return false;
1606
+ }
1607
+ }
1608
+ export function setReadWriteAccess(dirPath) {
1609
+ try {
1610
+ fs.chmodSync(dirPath, 0o666);
1611
+ BStackLogger.debug(`Directory ${dirPath} is now read/write accessible.`);
1612
+ }
1613
+ catch (err) {
1614
+ BStackLogger.error(`Failed to set directory access: ${err.stack}`);
1615
+ }
1616
+ }
1617
+ export function getMochaTestHierarchy(test) {
1618
+ const value = [];
1619
+ if (test.ctx && test.ctx.test) {
1620
+ // If we already have the parent object, utilize it else get from context
1621
+ let parent = typeof test.parent === 'object' ? test.parent : test.ctx.test.parent;
1622
+ while (parent && parent.title !== '') {
1623
+ value.push(parent.title);
1624
+ parent = parent.parent;
1625
+ }
1626
+ }
1627
+ else if (test.description && test.fullName) {
1628
+ // for Jasmine
1629
+ value.push(test.description);
1630
+ // Strip the trailing " <description>" suffix without building a regex from
1631
+ // a variable (avoids ReDoS and regex-metacharacter mismatches in descriptions).
1632
+ const descSuffix = ` ${test.description}`;
1633
+ value.push(test.fullName.endsWith(descSuffix) ? test.fullName.slice(0, -descSuffix.length) : test.fullName);
1634
+ }
1635
+ return value.reverse();
1636
+ }
1637
+ /**
1638
+ * Checks if the capabilities represent a multiremote configuration
1639
+ * @param capabilities - The capabilities to check
1640
+ * @returns true if capabilities represent any multiremote configuration (regular or parallel)
1641
+ *
1642
+ * @example
1643
+ * Regular multiremote (object):
1644
+ * { browserA: { capabilities: {...} }, browserB: { capabilities: {...} } }
1645
+ *
1646
+ * Parallel multiremote (array with nested structure):
1647
+ * [{ browserA: { capabilities: {...} }, browserB: { capabilities: {...} } }]
1648
+ *
1649
+ * Regular capabilities (array):
1650
+ * [{ browserName: 'chrome', ... }]
1651
+ */
1652
+ export function isMultiRemoteCaps(capabilities) {
1653
+ // Regular multiremote is an object (not array)
1654
+ if (!Array.isArray(capabilities)) {
1655
+ return true;
1656
+ }
1657
+ // Empty array is not multiremote
1658
+ if (capabilities.length === 0) {
1659
+ return false;
1660
+ }
1661
+ // Parallel multiremote is an array with nested capabilities structure
1662
+ return capabilities.every(cap => Object.values(cap).length > 0 &&
1663
+ Object.values(cap).every(c => c !== null && typeof c === 'object' && c.capabilities));
1664
+ }
1665
+ //# sourceMappingURL=util.js.map