diff --git a/packages/wdio-browserstack-service/src/Percy/Percy.ts b/packages/wdio-browserstack-service/src/Percy/Percy.ts index 2f643b052a6..fc22f53f1ba 100644 --- a/packages/wdio-browserstack-service/src/Percy/Percy.ts +++ b/packages/wdio-browserstack-service/src/Percy/Percy.ts @@ -11,6 +11,7 @@ import PercyBinary from './PercyBinary.js' import type { BrowserstackConfig, UserConfig } from '../types.js' import type { Options } from '@wdio/types' +import { BROWSERSTACK_TESTHUB_UUID } from '../constants.js' const logDir = 'logs' @@ -78,7 +79,7 @@ class Percy { this.#proc = spawn( binaryPath, commandArgs, - { env: { ...process.env, PERCY_TOKEN: token } } + { env: { ...process.env, PERCY_TOKEN: token, TH_BUILD_UUID: process.env[BROWSERSTACK_TESTHUB_UUID] } } ) this.#proc.stdout.pipe(logStream) diff --git a/packages/wdio-browserstack-service/src/Percy/PercySDK.ts b/packages/wdio-browserstack-service/src/Percy/PercySDK.ts index 0904e5d59de..a1df136d1b0 100644 --- a/packages/wdio-browserstack-service/src/Percy/PercySDK.ts +++ b/packages/wdio-browserstack-service/src/Percy/PercySDK.ts @@ -1,4 +1,7 @@ +import InsightsHandler from '../insights-handler.js' +import TestReporter from '../reporter.js' import { PercyLogger } from './PercyLogger.js' +import { isUndefined } from '../util.js' const tryRequire = async function (pkg: string, fallback: any) { try { @@ -17,20 +20,64 @@ let snapshotHandler = (...args: any[]) => { PercyLogger.error('Unsupported driver for percy') } if (percySnapshot) { - snapshotHandler = (browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, name: string) => { + snapshotHandler = (browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, snapshotName: string, options?: { [key: string]: any }) => { if (process.env.PERCY_SNAPSHOT === 'true') { - return percySnapshot(browser, name) + let { name, uuid } = InsightsHandler.currentTest + if (isUndefined(name)) { + ({ name, uuid } = TestReporter.currentTest) + } + options ||= {} + options = { + ...options, + testCase: name || '', + thTestCaseExecutionId: uuid || '', + } + return percySnapshot(browser, snapshotName, options) } } } export const snapshot = snapshotHandler +/* +This is a helper method which appends some internal fields +to the options object being sent to Percy methods +*/ +const screenshotHelper = (type: string, driverOrName: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser | string, nameOrOptions?: string | { [key: string]: any }, options?: { [key: string]: any }) => { + let { name, uuid } = InsightsHandler.currentTest + if (isUndefined(name)) { + ({ name, uuid } = TestReporter.currentTest) + } + if (!driverOrName || typeof driverOrName === 'string') { + nameOrOptions ||= {} + if (typeof nameOrOptions === 'object') { + nameOrOptions = { + ...nameOrOptions, + testCase: name || '', + thTestCaseExecutionId: uuid || '', + } + } + } else { + options ||= {} + options = { + ...options, + testCase: name || '', + thTestCaseExecutionId: uuid || '', + } + } + if (type === 'app') { + return percyAppScreenshot(driverOrName, nameOrOptions, options) + } + return percySnapshot.percyScreenshot(driverOrName, nameOrOptions, options) +} + /* eslint-disable @typescript-eslint/no-unused-vars */ let screenshotHandler = async (...args: any[]) => { PercyLogger.error('Unsupported driver for percy') } if (percySnapshot && percySnapshot.percyScreenshot) { - screenshotHandler = percySnapshot.percyScreenshot + screenshotHandler = (browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser | string, screenshotName?: string | { [key: string]: any }, options?: { [key: string]: any }) => { + return screenshotHelper('web', browser, screenshotName, options) + } } export const screenshot = screenshotHandler @@ -39,6 +86,8 @@ let screenshotAppHandler = async (...args: any[]) => { PercyLogger.error('Unsupported driver for percy') } if (percyAppScreenshot) { - screenshotAppHandler = percyAppScreenshot + screenshotAppHandler = (driverOrName: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser | string, nameOrOptions?: string | { [key: string]: any }, options?: { [key: string]: any }) => { + return screenshotHelper('app', driverOrName, nameOrOptions, options) + } } export const screenshotApp = screenshotAppHandler diff --git a/packages/wdio-browserstack-service/src/accessibility-handler.ts b/packages/wdio-browserstack-service/src/accessibility-handler.ts index 906c542395b..ebb36457004 100644 --- a/packages/wdio-browserstack-service/src/accessibility-handler.ts +++ b/packages/wdio-browserstack-service/src/accessibility-handler.ts @@ -4,6 +4,8 @@ import type { Capabilities, Frameworks } from '@wdio/types' import type { ITestCaseHookParameter } from './cucumber-types.js' +import Listener from './testOps/listener.js' + import { getA11yResultsSummary, getA11yResults, @@ -30,6 +32,7 @@ class _AccessibilityHandler { private _testMetadata: { [key: string]: any; } = {} private static _a11yScanSessionMap: { [key: string]: any; } = {} private _sessionId: string | null = null + private listener = Listener.getInstance() constructor ( private _browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, @@ -130,6 +133,8 @@ class _AccessibilityHandler { this._framework !== 'mocha' || !this.shouldRunTestHooks(this._browser, this._accessibility) ) { + /* This is to be used when test events are sent */ + Listener.setTestRunAccessibilityVar(false) return } @@ -142,6 +147,9 @@ class _AccessibilityHandler { AccessibilityHandler._a11yScanSessionMap[this._sessionId] = shouldScanTest } + /* This is to be used when test events are sent */ + Listener.setTestRunAccessibilityVar(this._accessibility && shouldScanTest) + if (!isPageOpened) { return } @@ -176,22 +184,15 @@ class _AccessibilityHandler { if (shouldScanTestForAccessibility) { BStackLogger.info('Automate test case execution has ended. Processing for accessibility testing is underway. ') - } - const dataForExtension = { - saveResults: shouldScanTestForAccessibility, - testDetails: { - 'name': test.title, - 'testRunId': process.env.BS_A11Y_TEST_RUN_ID, - 'filePath': this._suiteFile, - 'scopeList': [suiteTitle, test.title] - }, - platform: this._platformA11yMeta - } + const dataForExtension = { + 'thTestRunUuid': process.env.TEST_ANALYTICS_ID, + 'thBuildUuid': process.env.BROWSERSTACK_TESTHUB_UUID, + 'thJwtToken': process.env.BROWSERSTACK_TESTHUB_JWT + } - await this.sendTestStopEvent((this._browser as WebdriverIO.Browser), dataForExtension) + await this.sendTestStopEvent((this._browser as WebdriverIO.Browser), dataForExtension) - if (shouldScanTestForAccessibility) { BStackLogger.info('Accessibility testing for this test case has ended.') } } catch (error) { @@ -208,6 +209,8 @@ class _AccessibilityHandler { const featureData = gherkinDocument.feature const uniqueId = getUniqueIdentifierForCucumber(world) if (!this.shouldRunTestHooks(this._browser, this._accessibility)) { + /* This is to be used when test events are sent */ + Listener.setTestRunAccessibilityVar(false) return } @@ -220,6 +223,9 @@ class _AccessibilityHandler { AccessibilityHandler._a11yScanSessionMap[this._sessionId] = shouldScanScenario } + /* This is to be used when test events are sent */ + Listener.setTestRunAccessibilityVar(this._accessibility && shouldScanScenario) + if (!isPageOpened) { return } @@ -242,8 +248,6 @@ class _AccessibilityHandler { const pickleData = world.pickle try { - const gherkinDocument = world.gherkinDocument - const featureData = gherkinDocument.feature const uniqueId = getUniqueIdentifierForCucumber(world) const accessibilityScanStarted = this._testMetadata[uniqueId]?.accessibilityScanStarted const shouldScanTestForAccessibility = this._testMetadata[uniqueId]?.scanTestForAccessibility @@ -254,22 +258,15 @@ class _AccessibilityHandler { if (shouldScanTestForAccessibility) { BStackLogger.info('Automate test case execution has ended. Processing for accessibility testing is underway. ') - } - const dataForExtension = { - saveResults: shouldScanTestForAccessibility, - testDetails: { - 'name': pickleData.name, - 'testRunId': process.env.BS_A11Y_TEST_RUN_ID, - 'filePath': gherkinDocument.uri, - 'scopeList': [featureData?.name, pickleData.name] - }, - platform: this._platformA11yMeta - } + const dataForExtension = { + 'thTestRunUuid': process.env.TEST_ANALYTICS_ID, + 'thBuildUuid': process.env.BROWSERSTACK_TESTHUB_UUID, + 'thJwtToken': process.env.BROWSERSTACK_TESTHUB_JWT + } - await this.sendTestStopEvent(( this._browser as WebdriverIO.Browser), dataForExtension) + await this.sendTestStopEvent(( this._browser as WebdriverIO.Browser), dataForExtension) - if (shouldScanTestForAccessibility) { BStackLogger.info('Accessibility testing for this test case has ended.') } } catch (error) { diff --git a/packages/wdio-browserstack-service/src/cleanup.ts b/packages/wdio-browserstack-service/src/cleanup.ts index 01db3a1bc3c..283f0f0cff3 100644 --- a/packages/wdio-browserstack-service/src/cleanup.ts +++ b/packages/wdio-browserstack-service/src/cleanup.ts @@ -2,7 +2,7 @@ import { getErrorString, stopBuildUpstream } from './util.js' import { BStackLogger } from './bstackLogger.js' import fs from 'node:fs' import { fireFunnelRequest } from './instrumentation/funnelInstrumentation.js' -import { TESTOPS_BUILD_ID_ENV, TESTOPS_JWT_ENV } from './constants.js' +import { BROWSERSTACK_TESTHUB_UUID, BROWSERSTACK_TESTHUB_JWT, BROWSERSTACK_OBSERVABILITY } from './constants.js' export default class BStackCleanup { static async startCleanup() { @@ -29,15 +29,15 @@ export default class BStackCleanup { } } static async executeObservabilityCleanup(funnelData: any) { - if (!process.env[TESTOPS_JWT_ENV]) { + if (!process.env[BROWSERSTACK_TESTHUB_JWT]) { return } BStackLogger.debug('Executing observability cleanup') try { const killSignal = funnelData?.event_properties?.finishedMetadata?.signal const result = await stopBuildUpstream(killSignal) - if (process.env[TESTOPS_BUILD_ID_ENV]) { - BStackLogger.info(`\nVisit https://observability.browserstack.com/builds/${process.env[TESTOPS_BUILD_ID_ENV]} to view build report, insights, and many more debugging information all at one place!\n`) + if (process.env[BROWSERSTACK_OBSERVABILITY] && process.env[BROWSERSTACK_TESTHUB_UUID]) { + BStackLogger.info(`\nVisit https://observability.browserstack.com/builds/${process.env[BROWSERSTACK_TESTHUB_UUID]} to view build report, insights, and many more debugging information all at one place!\n`) } const status = (result && result.status) || 'failed' const message = (result && result.message) diff --git a/packages/wdio-browserstack-service/src/constants.ts b/packages/wdio-browserstack-service/src/constants.ts index ae280a6b6f3..29687fc5a09 100644 --- a/packages/wdio-browserstack-service/src/constants.ts +++ b/packages/wdio-browserstack-service/src/constants.ts @@ -40,7 +40,6 @@ export const DEFAULT_WAIT_TIMEOUT_FOR_PENDING_UPLOADS = 5000 // 5s export const DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS = 100 // 100ms export const BSTACK_SERVICE_VERSION = bstackServiceVersion -export const ACCESSIBILITY_API_URL = 'https://accessibility.browserstack.com/api' export const NOT_ALLOWED_KEYS_IN_CAPS = ['includeTagsInTestingScope', 'excludeTagsInTestingScope'] export const LOGS_FILE = 'logs/bstack-wdio-service.log' @@ -82,7 +81,7 @@ export const TCG_INFO = { // Env variables - Define all the env variable constants over here // To store the JWT token returned the session launch -export const TESTOPS_JWT_ENV = 'BS_TESTOPS_JWT' +export const BROWSERSTACK_TESTHUB_JWT = 'BROWSERSTACK_TESTHUB_JWT' // To store tcg auth result for selfHealing feature: export const BSTACK_TCG_AUTH_RESULT = 'BSTACK_TCG_AUTH_RESULT' @@ -91,7 +90,10 @@ export const BSTACK_TCG_AUTH_RESULT = 'BSTACK_TCG_AUTH_RESULT' export const TESTOPS_SCREENSHOT_ENV = 'BS_TESTOPS_ALLOW_SCREENSHOTS' // To store build hashed id -export const TESTOPS_BUILD_ID_ENV = 'BS_TESTOPS_BUILD_HASHED_ID' +export const BROWSERSTACK_TESTHUB_UUID = 'BROWSERSTACK_TESTHUB_UUID' + +// To store test run uuid +export const TEST_ANALYTICS_ID = 'TEST_ANALYTICS_ID' // Whether to collect performance instrumentation or not export const PERF_MEASUREMENT_ENV = 'BROWSERSTACK_O11Y_PERF_MEASUREMENT' @@ -105,6 +107,15 @@ export const RERUN_ENV = 'BROWSERSTACK_RERUN' // To store whether the build launch has completed or not export const TESTOPS_BUILD_COMPLETED_ENV = 'BS_TESTOPS_BUILD_COMPLETED' +// Whether percy has started successfully or not +export const BROWSERSTACK_PERCY = 'BROWSERSTACK_PERCY' + +// Whether session is a accessibility session +export const BROWSERSTACK_ACCESSIBILITY = 'BROWSERSTACK_ACCESSIBILITY' + +// Whether session is a observability session +export const BROWSERSTACK_OBSERVABILITY = 'BROWSERSTACK_OBSERVABILITY' + // Maximum size of VCS info which is allowed export const MAX_GIT_META_DATA_SIZE_IN_BYTES = 64 * 1024 diff --git a/packages/wdio-browserstack-service/src/crash-reporter.ts b/packages/wdio-browserstack-service/src/crash-reporter.ts index 0a486a3b3a3..5c68606f051 100644 --- a/packages/wdio-browserstack-service/src/crash-reporter.ts +++ b/packages/wdio-browserstack-service/src/crash-reporter.ts @@ -1,7 +1,7 @@ import type { Capabilities, Options } from '@wdio/types' import got from 'got' -import { BSTACK_SERVICE_VERSION, DATA_ENDPOINT, TESTOPS_BUILD_ID_ENV } from './constants.js' +import { BSTACK_SERVICE_VERSION, DATA_ENDPOINT, BROWSERSTACK_TESTHUB_UUID } from './constants.js' import type { BrowserstackConfig, CredentialsForCrashReportUpload, UserConfigforReporting } from './types.js' import { DEFAULT_REQUEST_CONFIG, getObservabilityKey, getObservabilityUser } from './util.js' import { BStackLogger } from './bstackLogger.js' @@ -61,7 +61,7 @@ export default class CrashReporter { } const data = { - hashed_id: process.env[TESTOPS_BUILD_ID_ENV], + hashed_id: process.env[BROWSERSTACK_TESTHUB_UUID], observability_version: { frameworkName: 'WebdriverIO-' + (this.userConfigForReporting.framework || 'null'), sdkVersion: BSTACK_SERVICE_VERSION diff --git a/packages/wdio-browserstack-service/src/exitHandler.ts b/packages/wdio-browserstack-service/src/exitHandler.ts index 05d0851611a..9f175d97a0b 100644 --- a/packages/wdio-browserstack-service/src/exitHandler.ts +++ b/packages/wdio-browserstack-service/src/exitHandler.ts @@ -3,7 +3,7 @@ import path from 'node:path' import BrowserStackConfig from './config.js' import { saveFunnelData } from './instrumentation/funnelInstrumentation.js' import { fileURLToPath } from 'node:url' -import { TESTOPS_JWT_ENV } from './constants.js' +import { BROWSERSTACK_TESTHUB_JWT } from './constants.js' import { BStackLogger } from './bstackLogger.js' const __filename = fileURLToPath(import.meta.url) @@ -46,7 +46,7 @@ export function setupExitHandlers() { export function shouldCallCleanup(config: BrowserStackConfig): string[] { const args: string[] = [] - if (!!process.env[TESTOPS_JWT_ENV] && !config.testObservability.buildStopped) { + if (!!process.env[BROWSERSTACK_TESTHUB_JWT] && !config.testObservability.buildStopped) { args.push('--observability') } diff --git a/packages/wdio-browserstack-service/src/insights-handler.ts b/packages/wdio-browserstack-service/src/insights-handler.ts index 0821c9a4679..b5e956caf13 100644 --- a/packages/wdio-browserstack-service/src/insights-handler.ts +++ b/packages/wdio-browserstack-service/src/insights-handler.ts @@ -44,7 +44,7 @@ class _InsightsHandler { private _commands: Record = {} private _gitConfigPath?: string private _suiteFile?: string - private _currentTest: CurrentRunInfo = {} + public static currentTest: CurrentRunInfo = {} private _currentHook: CurrentRunInfo = {} private _cucumberData: CucumberStore = { stepsStarted: false, @@ -53,8 +53,8 @@ class _InsightsHandler { } private _userCaps?: Capabilities.RemoteCapability = {} private listener = Listener.getInstance() - private _currentTestId: string | undefined - private _cbtQueue: Array = [] + public currentTestId: string | undefined + public cbtQueue: Array = [] constructor (private _browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, private _framework?: string, _userCaps?: Capabilities.RemoteCapability, _options?: BrowserstackConfig & Options.Testrunner) { const caps = (this._browser as WebdriverIO.Browser).capabilities as WebdriverIO.Capabilities @@ -207,7 +207,7 @@ class _InsightsHandler { const hookMetaData = { uuid: hookUUID, startedAt: (new Date()).toISOString(), - testRunId: this._currentTest.uuid, + testRunId: InsightsHandler.currentTest.uuid, hookType: hookType } @@ -368,7 +368,7 @@ class _InsightsHandler { async beforeTest (test: Frameworks.Test) { const uuid = uuidv4() - this._currentTest = { + InsightsHandler.currentTest = { test, uuid } if (this._framework !== 'mocha') { @@ -408,7 +408,7 @@ class _InsightsHandler { async beforeScenario (world: ITestCaseHookParameter) { const uuid = uuidv4() - this._currentTest = { + InsightsHandler.currentTest = { uuid } this._cucumberData.scenario = world.pickle @@ -503,8 +503,8 @@ class _InsightsHandler { try { if (this._currentHook.uuid && !this._currentHook.finished && (this._framework === 'mocha' || this._framework === 'cucumber')) { stdLog.hook_run_uuid = this._currentHook.uuid - } else if (this._currentTest.uuid && (this._framework === 'mocha' || this._framework === 'cucumber')) { - stdLog.test_run_uuid = this._currentTest.uuid + } else if (InsightsHandler.currentTest.uuid && (this._framework === 'mocha' || this._framework === 'cucumber')) { + stdLog.test_run_uuid = InsightsHandler.currentTest.uuid } if (stdLog.hook_run_uuid || stdLog.test_run_uuid) { this.listener.logCreated([stdLog]) @@ -628,7 +628,11 @@ class _InsightsHandler { const testMetaData = this._tests[fullTitle] const filename = test.file || this._suiteFile - this._currentTestId = testMetaData.uuid + this.currentTestId = testMetaData.uuid + + if (eventType === 'TestRunStarted') { + InsightsHandler.currentTest.name = test.title || test.description + } const testData: TestData = { uuid: testMetaData.uuid, @@ -743,7 +747,11 @@ class _InsightsHandler { fullNameWithExamples = scenario?.name || '' } - this._currentTestId = uuid + this.currentTestId = uuid + + if (eventType === 'TestRunStarted') { + InsightsHandler.currentTest.name = fullNameWithExamples + } const testData: TestData = { uuid: uuid, @@ -816,13 +824,13 @@ class _InsightsHandler { return testData } - private async flushCBTDataQueue() { - if (isUndefined(this._currentTestId)) {return} - this._cbtQueue.forEach(cbtData => { - cbtData.uuid = this._currentTestId! + public async flushCBTDataQueue() { + if (isUndefined(this.currentTestId)) {return} + this.cbtQueue.forEach(cbtData => { + cbtData.uuid = this.currentTestId! this.listener.cbtSessionCreated(cbtData) }) - this._currentTestId = undefined // set undefined for next test + this.currentTestId = undefined // set undefined for next test } async sendCBTInfo() { @@ -838,11 +846,11 @@ class _InsightsHandler { integrations: integrationsData } - if (this._currentTestId !== undefined) { - cbtData.uuid = this._currentTestId + if (this.currentTestId !== undefined) { + cbtData.uuid = this.currentTestId this.listener.cbtSessionCreated(cbtData) } else { - this._cbtQueue.push(cbtData) + this.cbtQueue.push(cbtData) } } diff --git a/packages/wdio-browserstack-service/src/instrumentation/funnelInstrumentation.ts b/packages/wdio-browserstack-service/src/instrumentation/funnelInstrumentation.ts index 47f2a125041..ef11b9f22ea 100644 --- a/packages/wdio-browserstack-service/src/instrumentation/funnelInstrumentation.ts +++ b/packages/wdio-browserstack-service/src/instrumentation/funnelInstrumentation.ts @@ -9,6 +9,7 @@ import { BStackLogger } from '../bstackLogger.js' import type BrowserStackConfig from '../config.js' import { BSTACK_SERVICE_VERSION, FUNNEL_INSTRUMENTATION_URL } from '../constants.js' import { getDataFromWorkers, removeWorkersDataDir } from '../data-store.js' +import { getProductMap } from '../testHub/utils.js' import type { BrowserstackHealing } from '@browserstack/ai-sdk-node' async function fireFunnelTestEvent(eventType: string, config: BrowserStackConfig) { @@ -48,13 +49,27 @@ export function saveFunnelData(eventType: string, config: BrowserStackConfig): s return filePath } +function redactCredentialsFromFunnelData(data: any) { + if (data) { + if (data.userName) { + data.userName = '[REDACTED]' + } + if (data.accessKey) { + data.accessKey = '[REDACTED]' + } + } + return data +} + // Called from two different process export async function fireFunnelRequest(data: any): Promise { + const { userName, accessKey } = data + redactCredentialsFromFunnelData(data) BStackLogger.debug('Sending SDK event with data ' + util.inspect(data, { depth: 6 })) await got.post(FUNNEL_INSTRUMENTATION_URL, { headers: { 'content-type': 'application/json' - }, username: data.userName, password: data.accessKey, json: data + }, username: userName, password: accessKey, json: data }) } @@ -82,16 +97,6 @@ function getProductList(config: BrowserStackConfig) { return products } -function getProductMap(config: BrowserStackConfig): any { - return { - 'observability': config.testObservability.enabled, - 'accessibility': config.accessibility, - 'percy': config.percy, - 'automate': config.automate, - 'app_automate': config.appAutomate - } -} - function buildEventData(eventType: string, config: BrowserStackConfig): any { const eventProperties: any = { // Framework Details diff --git a/packages/wdio-browserstack-service/src/launcher.ts b/packages/wdio-browserstack-service/src/launcher.ts index 03dba07733b..d7e69b9f626 100644 --- a/packages/wdio-browserstack-service/src/launcher.ts +++ b/packages/wdio-browserstack-service/src/launcher.ts @@ -20,19 +20,19 @@ import type { BrowserstackConfig, App, AppConfig, AppUploadResponse, UserConfig, import { BSTACK_SERVICE_VERSION, NOT_ALLOWED_KEYS_IN_CAPS, PERF_MEASUREMENT_ENV, RERUN_ENV, RERUN_TESTS_ENV, - TESTOPS_BUILD_ID_ENV, - VALID_APP_EXTENSION + BROWSERSTACK_TESTHUB_UUID, + VALID_APP_EXTENSION, + BROWSERSTACK_PERCY, + BROWSERSTACK_OBSERVABILITY } from './constants.js' import { launchTestSession, - createAccessibilityTestRun, shouldAddServiceVersion, stopBuildUpstream, getCiInfo, isBStackSession, isUndefined, isAccessibilityAutomationSession, - stopAccessibilityTestRun, isTrue, getBrowserStackUser, getBrowserStackKey, @@ -271,38 +271,8 @@ export default class BrowserstackLauncherService implements Services.ServiceInst // remove accessibilityOptions from the capabilities if present this._updateObjectTypeCaps(capabilities, 'accessibilityOptions') - if (this._accessibilityAutomation) { - const scannerVersion = await createAccessibilityTestRun(this._options, this._config, { - projectName: this._projectName, - buildName: this._buildName, - buildTag: this._buildTag, - bstackServiceVersion: BSTACK_SERVICE_VERSION, - buildIdentifier: this._buildIdentifier, - accessibilityOptions: this._options.accessibilityOptions - }) - - if (scannerVersion) { - process.env.BSTACK_A11Y_SCANNER_VERSION = scannerVersion - } - BStackLogger.debug(`Accessibility scannerVersion ${scannerVersion}`) - } - - if (this._options.accessibilityOptions) { - const filteredOpts = Object.keys(this._options.accessibilityOptions) - .filter(key => !NOT_ALLOWED_KEYS_IN_CAPS.includes(key)) - .reduce((opts, key) => { - return { - ...opts, - [key]: this._options.accessibilityOptions?.[key] - } - }, {}) - - this._updateObjectTypeCaps(capabilities, 'accessibilityOptions', filteredOpts) - } else if (isAccessibilityAutomationSession(this._accessibilityAutomation)) { - this._updateObjectTypeCaps(capabilities, 'accessibilityOptions', {}) - } - - if (this._options.testObservability) { + const shouldSetupPercy = this._options.percy || (isUndefined(this._options.percy) && this._options.app) + if (this._options.testObservability || this._accessibilityAutomation || shouldSetupPercy) { BStackLogger.debug('Sending launch start event') await launchTestSession(this._options, this._config, { @@ -311,19 +281,35 @@ export default class BrowserstackLauncherService implements Services.ServiceInst buildTag: this._buildTag, bstackServiceVersion: BSTACK_SERVICE_VERSION, buildIdentifier: this._buildIdentifier - }) - } - const shouldSetupPercy = this._options.percy || (isUndefined(this._options.percy) && this._options.app) - if (shouldSetupPercy) { - try { - const bestPlatformPercyCaps = getBestPlatformForPercySnapshot(capabilities) - this._percyBestPlatformCaps = bestPlatformPercyCaps - await this.setupPercy(this._options, this._config, { - projectName: this._projectName - }) - this._updateBrowserStackPercyConfig() - } catch (err: unknown) { - PercyLogger.error(`Error while setting up Percy ${err}`) + }, this.browserStackConfig) + + if (this._accessibilityAutomation && this._options.accessibilityOptions) { + const filteredOpts = Object.keys(this._options.accessibilityOptions) + .filter(key => !NOT_ALLOWED_KEYS_IN_CAPS.includes(key)) + .reduce((opts, key) => { + return { + ...opts, + [key]: this._options.accessibilityOptions?.[key] + } + }, {}) + + this._updateObjectTypeCaps(capabilities, 'accessibilityOptions', filteredOpts) + } else if (isAccessibilityAutomationSession(this._accessibilityAutomation)) { + this._updateObjectTypeCaps(capabilities, 'accessibilityOptions', {}) + } + + if (shouldSetupPercy) { + try { + const bestPlatformPercyCaps = getBestPlatformForPercySnapshot(capabilities) + this._percyBestPlatformCaps = bestPlatformPercyCaps + process.env[BROWSERSTACK_PERCY] = 'false' + await this.setupPercy(this._options, this._config, { + projectName: this._projectName + }) + this._updateBrowserStackPercyConfig() + } catch (err: unknown) { + PercyLogger.error(`Error while setting up Percy ${err}`) + } } } @@ -376,30 +362,23 @@ export default class BrowserstackLauncherService implements Services.ServiceInst async onComplete () { BStackLogger.debug('Inside OnComplete hook..') - if (isAccessibilityAutomationSession(this._accessibilityAutomation)) { - await stopAccessibilityTestRun().catch((error: any) => { - BStackLogger.error(`Exception in stop accessibility test run: ${error}`) - }) - } - if (this._options.testObservability) { - BStackLogger.debug('Sending stop launch event') - await stopBuildUpstream() - if (process.env[TESTOPS_BUILD_ID_ENV]) { - console.log(`\nVisit https://observability.browserstack.com/builds/${process.env[TESTOPS_BUILD_ID_ENV]} to view build report, insights, and many more debugging information all at one place!\n`) - } - this.browserStackConfig.testObservability.buildStopped = true + BStackLogger.debug('Sending stop launch event') + await stopBuildUpstream() + if (process.env[BROWSERSTACK_OBSERVABILITY] && process.env[BROWSERSTACK_TESTHUB_UUID]) { + console.log(`\nVisit https://observability.browserstack.com/builds/${process.env[BROWSERSTACK_TESTHUB_UUID]} to view build report, insights, and many more debugging information all at one place!\n`) + } + this.browserStackConfig.testObservability.buildStopped = true - if (process.env[PERF_MEASUREMENT_ENV]) { - await PerformanceTester.stopAndGenerate('performance-launcher.html') - PerformanceTester.calculateTimes(['launchTestSession', 'stopBuildUpstream']) + if (process.env[PERF_MEASUREMENT_ENV]) { + await PerformanceTester.stopAndGenerate('performance-launcher.html') + PerformanceTester.calculateTimes(['launchTestSession', 'stopBuildUpstream']) - if (!process.env.START_TIME) { - return - } - const duration = (new Date()).getTime() - (new Date(process.env.START_TIME)).getTime() - BStackLogger.info(`Total duration is ${duration / 1000 } s`) + if (!process.env.START_TIME) { + return } + const duration = (new Date()).getTime() - (new Date(process.env.START_TIME)).getTime() + BStackLogger.info(`Total duration is ${duration / 1000} s`) } await sendFinish(this.browserStackConfig) @@ -451,8 +430,8 @@ export default class BrowserstackLauncherService implements Services.ServiceInst } async setupPercy(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig) { - if (this._percy?.isRunning()) { + process.env[BROWSERSTACK_PERCY] = 'true' return } try { @@ -461,6 +440,7 @@ export default class BrowserstackLauncherService implements Services.ServiceInst throw new Error('Could not start percy, check percy logs for info.') } PercyLogger.info('Percy started successfully') + process.env[BROWSERSTACK_PERCY] = 'true' let signal = 0 const handler = async () => { signal++ @@ -471,6 +451,7 @@ export default class BrowserstackLauncherService implements Services.ServiceInst process.on('SIGTERM', handler) } catch (err: unknown) { PercyLogger.debug(`Error in percy setup ${err}`) + process.env[BROWSERSTACK_PERCY] = 'false' } } @@ -817,8 +798,8 @@ export default class BrowserstackLauncherService implements Services.ServiceInst } _getClientBuildUuid() { - if (process.env[TESTOPS_BUILD_ID_ENV]) { - return process.env[TESTOPS_BUILD_ID_ENV] + if (process.env[BROWSERSTACK_TESTHUB_UUID]) { + return process.env[BROWSERSTACK_TESTHUB_UUID] } const uuid = uuidv4() BStackLogger.logToFile(`If facing any issues, please contact BrowserStack support with the Build Run Id - ${uuid}`, 'info') diff --git a/packages/wdio-browserstack-service/src/reporter.ts b/packages/wdio-browserstack-service/src/reporter.ts index 29ad67c3b67..47a2768b97a 100644 --- a/packages/wdio-browserstack-service/src/reporter.ts +++ b/packages/wdio-browserstack-service/src/reporter.ts @@ -32,7 +32,7 @@ class _TestReporter extends WDIOReporter { private _gitConfigPath?: string private _gitConfigured: boolean = false private _currentHook: CurrentRunInfo = {} - private _currentTest: CurrentRunInfo = {} + public static currentTest: CurrentRunInfo = {} private _userCaps?: Capabilities.RemoteCapability = {} private listener = Listener.getInstance() @@ -63,8 +63,8 @@ class _TestReporter extends WDIOReporter { public async appendTestItemLog(stdLog: StdLog) { if (this._currentHook.uuid && !this._currentHook.finished) { stdLog.hook_run_uuid = this._currentHook.uuid - } else if (this._currentTest.uuid) { - stdLog.test_run_uuid = this._currentTest.uuid + } else if (_TestReporter.currentTest.uuid) { + stdLog.test_run_uuid = _TestReporter.currentTest.uuid } if (stdLog.hook_run_uuid || stdLog.test_run_uuid) { this.listener.logCreated([stdLog]) @@ -158,7 +158,7 @@ class _TestReporter extends WDIOReporter { return } const uuid = uuidv4() - this._currentTest.uuid = uuid + _TestReporter.currentTest.uuid = uuid _TestReporter._tests[testStats.fullTitle] = { uuid: uuid, @@ -226,6 +226,10 @@ class _TestReporter extends WDIOReporter { // If no describe block present, onSuiteStart doesn't get called. Use specs list for filename const suiteFileName = this._suiteName || (this.specs?.length > 0 ? this.specs[this.specs.length - 1]?.replace('file:', '') : undefined) + if (eventType === 'TestRunStarted') { + _TestReporter.currentTest.name = testStats.title + } + await this.configureGit() const testData: TestData = { uuid: testMetaData ? testMetaData.uuid : uuidv4(), diff --git a/packages/wdio-browserstack-service/src/scripts/accessibility-scripts.ts b/packages/wdio-browserstack-service/src/scripts/accessibility-scripts.ts index e3b68a26afc..081c6249908 100644 --- a/packages/wdio-browserstack-service/src/scripts/accessibility-scripts.ts +++ b/packages/wdio-browserstack-service/src/scripts/accessibility-scripts.ts @@ -38,7 +38,7 @@ class AccessibilityScripts { } } - public update(data: { commands: [any], scripts: { scan: null; getResults: null; getResultsSummary: null; saveResults: null; }; }) { + public update(data: { commands: any[], scripts: Record }) { if (data.scripts) { this.performScan = data.scripts.scan this.getResults = data.scripts.getResults diff --git a/packages/wdio-browserstack-service/src/service.ts b/packages/wdio-browserstack-service/src/service.ts index f95159e778e..ea895bfa006 100644 --- a/packages/wdio-browserstack-service/src/service.ts +++ b/packages/wdio-browserstack-service/src/service.ts @@ -10,7 +10,8 @@ import { getParentSuiteName, isBrowserstackSession, patchConsoleLogs, - shouldAddServiceVersion + shouldAddServiceVersion, + isTrue } from './util.js' import type { BrowserstackConfig, BrowserstackOptions, MultiRemoteAction, SessionResponse, TurboScaleSessionResponse } from './types.js' import type { Pickle, Feature, ITestCaseHookParameter, CucumberHook } from './cucumber-types.js' @@ -24,6 +25,7 @@ import PercyHandler from './Percy/Percy-Handler.js' import Listener from './testOps/listener.js' import { saveWorkerData } from './data-store.js' import UsageStats from './testOps/usageStats.js' +import { shouldProcessEventForTesthub } from './testHub/utils.js' import AiHandler from './ai-handler.js' export default class BrowserstackService implements Services.ServiceInstance { @@ -57,11 +59,11 @@ export default class BrowserstackService implements Services.ServiceInstance { this._config || (this._config = this._options) this._observability = this._options.testObservability this._accessibility = this._options.accessibility - this._percy = process.env.BROWSERSTACK_PERCY === 'true' + this._percy = isTrue(process.env.BROWSERSTACK_PERCY) this._percyCaptureMode = process.env.BROWSERSTACK_PERCY_CAPTURE_MODE this._turboScale = this._options.turboScale - if (this._observability) { + if (shouldProcessEventForTesthub('')) { this._config.reporters?.push(TestReporter) if (process.env[PERF_MEASUREMENT_ENV]) { PerformanceTester.startMonitoring('performance-report-service.csv') @@ -139,30 +141,8 @@ export default class BrowserstackService implements Services.ServiceInstance { this._scenariosThatRan = [] if (this._browser) { - if (this._percy) { - this._percyHandler = new PercyHandler( - this._percyCaptureMode, - this._browser, - this._caps, - this._isAppAutomate(), - this._config.framework - ) - this._percyHandler.before() - } try { const sessionId = this._browser.sessionId - if (this._observability) { - patchConsoleLogs() - - this._insightsHandler = new InsightsHandler( - this._browser, - this._config.framework, - this._caps, - this._options - ) - await this._insightsHandler.before() - } - if (isBrowserstackSession(this._browser)) { try { this._accessibilityHandler = new AccessibilityHandler( @@ -174,16 +154,29 @@ export default class BrowserstackService implements Services.ServiceInstance { this._options.accessibilityOptions ) await this._accessibilityHandler.before(sessionId) + Listener.setAccessibilityOptions(this._options.accessibilityOptions) } catch (err) { BStackLogger.error(`[Accessibility Test Run] Error in service class before function: ${err}`) } } + if (shouldProcessEventForTesthub('')) { + patchConsoleLogs() + + this._insightsHandler = new InsightsHandler( + this._browser, + this._config.framework, + this._caps, + this._options + ) + await this._insightsHandler.before() + } + /** * register command event */ this._browser.on('command', async (command) => { - if (this._observability) { + if (shouldProcessEventForTesthub('')) { this._insightsHandler?.browserCommand( 'client:beforeCommand', Object.assign(command, { sessionId }), @@ -199,7 +192,7 @@ export default class BrowserstackService implements Services.ServiceInstance { * register result event */ this._browser.on('result', (result) => { - if (this._observability) { + if (shouldProcessEventForTesthub('')) { this._insightsHandler?.browserCommand( 'client:afterCommand', Object.assign(result, { sessionId }), @@ -212,10 +205,21 @@ export default class BrowserstackService implements Services.ServiceInstance { }) } catch (err) { BStackLogger.error(`Error in service class before function: ${err}`) - if (this._observability) { + if (shouldProcessEventForTesthub('')) { CrashReporter.uploadCrashReport(`Error in service class before function: ${err}`, err && (err as any).stack) } } + + if (this._percy) { + this._percyHandler = new PercyHandler( + this._percyCaptureMode, + this._browser, + this._caps, + this._isAppAutomate(), + this._config.framework + ) + this._percyHandler.before() + } } return await this._printSessionURL() @@ -266,8 +270,8 @@ export default class BrowserstackService implements Services.ServiceInstance { await this._setSessionName(suiteTitle, test) await this._setAnnotation(`Test: ${test.fullName ?? test.title}`) - await this._insightsHandler?.beforeTest(test) await this._accessibilityHandler?.beforeTest(suiteTitle, test) + await this._insightsHandler?.beforeTest(test) } async afterTest(test: Frameworks.Test, context: never, results: Frameworks.TestResult) { @@ -276,9 +280,9 @@ export default class BrowserstackService implements Services.ServiceInstance { if (!passed) { this._failReasons.push((error && error.message) || 'Unknown Error') } + await this._accessibilityHandler?.afterTest(this._suiteTitle, test) await this._insightsHandler?.afterTest(test, results) await this._percyHandler?.afterTest() - await this._accessibilityHandler?.afterTest(this._suiteTitle, test) } async after (result: number) { @@ -331,8 +335,8 @@ export default class BrowserstackService implements Services.ServiceInstance { */ async beforeScenario (world: ITestCaseHookParameter) { this._currentTest = world - await this._insightsHandler?.beforeScenario(world) await this._accessibilityHandler?.beforeScenario(world) + await this._insightsHandler?.beforeScenario(world) const scenarioName = world.pickle.name || 'unknown scenario' await this._setAnnotation(`Scenario: ${scenarioName}`) } @@ -356,9 +360,9 @@ export default class BrowserstackService implements Services.ServiceInstance { this._failReasons.push(exception) } + await this._accessibilityHandler?.afterScenario(world) await this._insightsHandler?.afterScenario(world) await this._percyHandler?.afterScenario() - await this._accessibilityHandler?.afterScenario(world) } async beforeStep (step: Frameworks.PickleStep, scenario: Pickle) { diff --git a/packages/wdio-browserstack-service/src/testHub/utils.ts b/packages/wdio-browserstack-service/src/testHub/utils.ts new file mode 100644 index 00000000000..1e5d3041495 --- /dev/null +++ b/packages/wdio-browserstack-service/src/testHub/utils.ts @@ -0,0 +1,65 @@ + +import { BROWSERSTACK_PERCY, BROWSERSTACK_OBSERVABILITY, BROWSERSTACK_ACCESSIBILITY } from '../constants.js' +import type BrowserStackConfig from '../config.js' +import { BStackLogger } from '../bstackLogger.js' +import { isTrue } from '../util.js' + +export const getProductMap = (config: BrowserStackConfig): any => { + return { + 'observability': config.testObservability.enabled, + 'accessibility': config.accessibility, + 'percy': config.percy, + 'automate': config.automate, + 'app_automate': config.appAutomate + } +} + +export const shouldProcessEventForTesthub = (eventType: string): boolean => { + if (isTrue(process.env[BROWSERSTACK_OBSERVABILITY])) { + return true + } + if (isTrue(process.env[BROWSERSTACK_ACCESSIBILITY])) { + return !(['HookRunStarted', 'HookRunFinished', 'LogCreated'].includes(eventType)) + } + if (isTrue(process.env[BROWSERSTACK_PERCY]) && eventType) { + return false + } + return Boolean(process.env[BROWSERSTACK_ACCESSIBILITY] || process.env[BROWSERSTACK_OBSERVABILITY] || process.env[BROWSERSTACK_PERCY])! +} + +export const handleErrorForObservability = (error: any): void => { + process.env[BROWSERSTACK_OBSERVABILITY] = 'false' + logBuildError(error, 'observability') +} + +export const handleErrorForAccessibility = (error: any): void => { + process.env[BROWSERSTACK_ACCESSIBILITY] = 'false' + logBuildError(error, 'accessibility') +} + +export const logBuildError = (error: any, product: string = ''): void => { + if (!error || !error.errors) { + BStackLogger.error(`${product.toUpperCase()} Build creation failed ${error!}`) + return + } + + for (const errorJson of error.errors) { + const errorType = errorJson.key + const errorMessage = errorJson.message + if (errorMessage) { + switch (errorType) { + case 'ERROR_INVALID_CREDENTIALS': + BStackLogger.error(errorMessage) + break + case 'ERROR_ACCESS_DENIED': + BStackLogger.info(errorMessage) + break + case 'ERROR_SDK_DEPRECATED': + BStackLogger.error(errorMessage) + break + default: + BStackLogger.error(errorMessage) + } + } + } +} diff --git a/packages/wdio-browserstack-service/src/testOps/listener.ts b/packages/wdio-browserstack-service/src/testOps/listener.ts index 9ec1cfcb648..d010b3b5e87 100644 --- a/packages/wdio-browserstack-service/src/testOps/listener.ts +++ b/packages/wdio-browserstack-service/src/testOps/listener.ts @@ -7,10 +7,12 @@ import { DATA_BATCH_ENDPOINT, DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS, DEFAULT_WAIT_TIMEOUT_FOR_PENDING_UPLOADS, - LOG_KIND_USAGE_MAP, TESTOPS_BUILD_COMPLETED_ENV + LOG_KIND_USAGE_MAP, TESTOPS_BUILD_COMPLETED_ENV, + TEST_ANALYTICS_ID } from '../constants.js' import { sendScreenshots } from './requestUtils.js' import { BStackLogger } from '../bstackLogger.js' +import { shouldProcessEventForTesthub } from '../testHub/utils.js' class Listener { private static instance: Listener @@ -23,6 +25,8 @@ class Listener { private readonly logEvents: FeatureStats = this.usageStats.logStats private requestBatcher?: RequestQueueHandler private pendingUploads = 0 + private static _accessibilityOptions?: { [key: string]: any; } + private static _testRunAccessibilityVar?: boolean = false // Making the constructor private to use singleton pattern private constructor() { @@ -35,6 +39,14 @@ class Listener { return Listener.instance } + public static setAccessibilityOptions(options: { [key: string]: any; } | undefined) { + Listener._accessibilityOptions = options + } + + public static setTestRunAccessibilityVar(accessibility: boolean | undefined) { + Listener._testRunAccessibilityVar = accessibility + } + public async onWorkerEnd() { try { await this.uploadPending() @@ -62,6 +74,9 @@ class Listener { public hookStarted(hookData: TestData): void { try { + if (!shouldProcessEventForTesthub('HookRunStarted')) { + return + } this.hookStartedStats.triggered() this.sendBatchEvents(this.getEventForHook('HookRunStarted', hookData)) } catch (e) { @@ -72,6 +87,9 @@ class Listener { public hookFinished(hookData: TestData): void { try { + if (!shouldProcessEventForTesthub('HookRunFinished')) { + return + } this.hookFinishedStats.triggered(hookData.result) this.sendBatchEvents(this.getEventForHook('HookRunFinished', hookData)) } catch (e) { @@ -82,7 +100,16 @@ class Listener { public testStarted(testData: TestData): void { try { + if (!shouldProcessEventForTesthub('TestRunStarted')) { + return + } + process.env[TEST_ANALYTICS_ID] = testData.uuid this.testStartedStats.triggered() + + testData.product_map = { + accessibility: Listener._testRunAccessibilityVar + } + this.sendBatchEvents(this.getEventForHook('TestRunStarted', testData)) } catch (e) { this.testStartedStats.failed() @@ -92,6 +119,14 @@ class Listener { public testFinished(testData: TestData): void { try { + if (!shouldProcessEventForTesthub('TestRunFinished')) { + return + } + + testData.product_map = { + accessibility: Listener._testRunAccessibilityVar + } + this.testFinishedStats.triggered(testData.result) this.sendBatchEvents(this.getEventForHook('TestRunFinished', testData)) } catch (e) { @@ -102,6 +137,9 @@ class Listener { public logCreated(logs: LogData[]): void { try { + if (!shouldProcessEventForTesthub('LogCreated')) { + return + } this.markLogs('triggered', logs) this.sendBatchEvents({ event_type: 'LogCreated', logs: logs @@ -117,6 +155,9 @@ class Listener { return } try { + if (!shouldProcessEventForTesthub('LogCreated')) { + return + } this.markLogs('triggered', jsonArray) this.pendingUploads += 1 await sendScreenshots([{ @@ -133,6 +174,9 @@ class Listener { public cbtSessionCreated(data: CBTData): void { try { + if (!shouldProcessEventForTesthub('CBTSessionCreated')) { + return + } this.cbtSessionStats.triggered() this.sendBatchEvents({ event_type: 'CBTSessionCreated', test_run: data }) } catch (e) { diff --git a/packages/wdio-browserstack-service/src/testOps/requestUtils.ts b/packages/wdio-browserstack-service/src/testOps/requestUtils.ts index 2cbb6cabc08..6db8379fe9e 100644 --- a/packages/wdio-browserstack-service/src/testOps/requestUtils.ts +++ b/packages/wdio-browserstack-service/src/testOps/requestUtils.ts @@ -3,7 +3,7 @@ import { DATA_ENDPOINT, DATA_EVENT_ENDPOINT, DATA_SCREENSHOT_ENDPOINT, - TESTOPS_BUILD_COMPLETED_ENV, TESTOPS_JWT_ENV + TESTOPS_BUILD_COMPLETED_ENV, BROWSERSTACK_TESTHUB_JWT } from '../constants.js' import { BStackLogger } from '../bstackLogger.js' import { DEFAULT_REQUEST_CONFIG, getLogTag } from '../util.js' @@ -23,7 +23,7 @@ export async function uploadEventData (eventData: UploadType | Array throw new Error('Build start not completed yet') } - if (!process.env[TESTOPS_JWT_ENV]) { + if (!process.env[BROWSERSTACK_TESTHUB_JWT]) { BStackLogger.debug(`[${logTag}] Missing Authentication Token/ Build ID`) throw new Error('Token/buildID is undefined, build creation might have failed') } @@ -34,7 +34,7 @@ export async function uploadEventData (eventData: UploadType | Array agent: DEFAULT_REQUEST_CONFIG.agent, headers: { ...DEFAULT_REQUEST_CONFIG.headers, - 'Authorization': `Bearer ${process.env[TESTOPS_JWT_ENV]}` + 'Authorization': `Bearer ${process.env[BROWSERSTACK_TESTHUB_JWT]}` }, json: eventData }).json() diff --git a/packages/wdio-browserstack-service/src/types.ts b/packages/wdio-browserstack-service/src/types.ts index 6bf4396ebc6..2e44298713e 100644 --- a/packages/wdio-browserstack-service/src/types.ts +++ b/packages/wdio-browserstack-service/src/types.ts @@ -196,6 +196,7 @@ export interface TestMeta { export interface CurrentRunInfo { uuid?: string, + name?: string, test?: Frameworks.Test, finished?: boolean } @@ -225,7 +226,8 @@ export interface TestData { hooks?: string[], meta?: TestMeta, tags?: string[], - test_run_id?: string + test_run_id?: string, + product_map?: {} } export interface UserConfig { @@ -265,7 +267,38 @@ export interface ScreenshotLog extends LogData { export interface LaunchResponse { jwt: string, build_hashed_id: string, - allow_screenshots?: boolean + observability?: { + success: boolean; + options: { + allow_screenshots?: boolean; + }, + errors?: { + key: string; + message: string; + }[]; + }, + accessibility?: { + success: boolean; + errors?: { + key: string; + message: string; + }[]; + options: { + status: string; + commandsToWrap: { + scriptsToRun: string[]; + commands: any[]; + }; + scripts: { + name: string; + command: string; + }[]; + capabilities: { + name: string, + value: any + }[]; + } + }; } export interface UserConfigforReporting { @@ -341,4 +374,4 @@ export interface TOUsageStats { export interface TOStopData { finished_at: string, finished_metadata: Array, -} \ No newline at end of file +} diff --git a/packages/wdio-browserstack-service/src/util.ts b/packages/wdio-browserstack-service/src/util.ts index 8a01a78b417..1fc279afef7 100644 --- a/packages/wdio-browserstack-service/src/util.ts +++ b/packages/wdio-browserstack-service/src/util.ts @@ -19,22 +19,25 @@ import type { ColorName } from 'chalk' import { FormData } from 'formdata-node' import logPatcher from './logPatcher.js' import PerformanceTester from './performance-tester.js' +import { getProductMap, logBuildError, handleErrorForObservability, handleErrorForAccessibility } from './testHub/utils.js' +import type BrowserStackConfig from './config.js' import type { UserConfig, UploadType, LaunchResponse, BrowserstackConfig, TOStopData } from './types.js' import type { ITestCaseHookParameter } from './cucumber-types.js' import { - ACCESSIBILITY_API_URL, BROWSER_DESCRIPTION, DATA_ENDPOINT, UPLOAD_LOGS_ADDRESS, UPLOAD_LOGS_ENDPOINT, consoleHolder, TESTOPS_SCREENSHOT_ENV, - TESTOPS_BUILD_ID_ENV, + BROWSERSTACK_TESTHUB_UUID, PERF_MEASUREMENT_ENV, RERUN_ENV, TESTOPS_BUILD_COMPLETED_ENV, - TESTOPS_JWT_ENV, + BROWSERSTACK_TESTHUB_JWT, + BROWSERSTACK_OBSERVABILITY, + BROWSERSTACK_ACCESSIBILITY, MAX_GIT_META_DATA_SIZE_IN_BYTES, GIT_META_DATA_TRUNCATED } from './constants.js' @@ -268,15 +271,87 @@ export function o11yClassErrorHandler(errorClass: T): T { return errorClass } -export const launchTestSession = o11yErrorHandler(async function launchTestSession(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig) { +export const processTestObservabilityResponse = (response: LaunchResponse) => { + if (!response.observability) { + handleErrorForObservability(null) + return + } + if (!response.observability.success) { + handleErrorForObservability(response.observability) + return + } + process.env[BROWSERSTACK_OBSERVABILITY] = 'true' + if (response.observability.options.allow_screenshots) { + process.env[TESTOPS_SCREENSHOT_ENV] = response.observability.options.allow_screenshots.toString() + } +} + +interface DataElement { + [key: string]: any +} + +export const jsonifyAccessibilityArray = ( + dataArray: DataElement[], + keyName: keyof DataElement, + valueName: keyof DataElement +): Record => { + const result: Record = {} + dataArray.forEach((element: DataElement) => { + result[element[keyName]] = element[valueName] + }) + return result +} + +export const processAccessibilityResponse = (response: LaunchResponse) => { + if (!response.accessibility) { + handleErrorForAccessibility(null) + return + } + if (!response.accessibility.success) { + handleErrorForAccessibility(response.accessibility) + return + } + + if (response.accessibility.options) { + const { accessibilityToken, scannerVersion } = jsonifyAccessibilityArray(response.accessibility.options.capabilities, 'name', 'value') + const scriptsJson = { + 'scripts': jsonifyAccessibilityArray(response.accessibility.options.scripts, 'name', 'command'), + 'commands': response.accessibility.options.commandsToWrap.commands + } + if (scannerVersion) { + process.env.BSTACK_A11Y_SCANNER_VERSION = scannerVersion + } + BStackLogger.debug(`Accessibility scannerVersion ${scannerVersion}`) + if (accessibilityToken) { + process.env.BSTACK_A11Y_JWT = accessibilityToken + process.env[BROWSERSTACK_ACCESSIBILITY] = 'true' + } + if (scriptsJson) { + AccessibilityScripts.update(scriptsJson) + AccessibilityScripts.store() + } + } +} + +export const processLaunchBuildResponse = (response: LaunchResponse, options: BrowserstackConfig & Options.Testrunner) => { + if (options.testObservability) { + processTestObservabilityResponse(response) + } + if (options.accessibility) { + processAccessibilityResponse(response) + } +} + +export const launchTestSession = o11yErrorHandler(async function launchTestSession(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig, bStackConfig: BrowserStackConfig) { const launchBuildUsage = UsageStats.getInstance().launchBuildUsage launchBuildUsage.triggered() + const data = { format: 'json', project_name: getObservabilityProject(options, bsConfig.projectName), name: getObservabilityBuild(options, bsConfig.buildName), build_identifier: bsConfig.buildIdentifier, - start_time: (new Date()).toISOString(), + started_at: (new Date()).toISOString(), tags: getObservabilityBuildTags(options, bsConfig.buildTag), host_info: { hostname: hostname(), @@ -289,10 +364,21 @@ export const launchTestSession = o11yErrorHandler(async function launchTestSessi build_run_identifier: process.env.BROWSERSTACK_BUILD_RUN_IDENTIFIER, failed_tests_rerun: process.env[RERUN_ENV] || false, version_control: await getGitMetaData(), - observability_version: { + accessibility: { + settings: options.accessibilityOptions + }, + browserstackAutomation: shouldAddServiceVersion(config, options.testObservability), + framework_details: { frameworkName: 'WebdriverIO-' + config.framework, - sdkVersion: bsConfig.bstackServiceVersion + frameworkVersion: bsConfig.bstackServiceVersion, + sdkVersion: bsConfig.bstackServiceVersion, + language: 'ECMAScript', + testFramework: { + name: 'WebdriverIO', + version: bsConfig.bstackServiceVersion + } }, + product_map: getProductMap(bStackConfig), config: {} } @@ -306,7 +392,7 @@ export const launchTestSession = o11yErrorHandler(async function launchTestSessi data.config = CrashReporter.userConfigForReporting try { - const url = `${DATA_ENDPOINT}/api/v1/builds` + const url = `${DATA_ENDPOINT}/api/v2/builds` const response: LaunchResponse = await got.post(url, { ...DEFAULT_REQUEST_CONFIG, username: getObservabilityUser(options, config), @@ -316,41 +402,25 @@ export const launchTestSession = o11yErrorHandler(async function launchTestSessi BStackLogger.debug(`[Start_Build] Success response: ${JSON.stringify(response)}`) process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true' if (response.jwt) { - launchBuildUsage.success() - process.env[TESTOPS_JWT_ENV] = response.jwt + process.env[BROWSERSTACK_TESTHUB_JWT] = response.jwt } if (response.build_hashed_id) { - process.env[TESTOPS_BUILD_ID_ENV] = response.build_hashed_id + process.env[BROWSERSTACK_TESTHUB_UUID] = response.build_hashed_id TestOpsConfig.getInstance().buildHashedId = response.build_hashed_id } - if (response.allow_screenshots) { - process.env[TESTOPS_SCREENSHOT_ENV] = response.allow_screenshots.toString() - } - } catch (error) { - launchBuildUsage.failed(error) - if (error instanceof HTTPError && error.response) { - const errorMessageJson = error.response.body ? JSON.parse(error.response.body.toString()) : null - const errorMessage = errorMessageJson ? errorMessageJson.message : null, errorType = errorMessageJson ? errorMessageJson.errorType : null - switch (errorType) { - case 'ERROR_INVALID_CREDENTIALS': - BStackLogger.error(errorMessage) - break - case 'ERROR_ACCESS_DENIED': - BStackLogger.info(errorMessage) - break - case 'ERROR_SDK_DEPRECATED': - BStackLogger.error(errorMessage) - break - default: - BStackLogger.error(errorMessage) - } - } else { - BStackLogger.error(`Data upload to BrowserStack Test Observability failed due to ${error}`) + processLaunchBuildResponse(response, options) + launchBuildUsage.success() + } catch (error: any) { + if (!error.success) { + launchBuildUsage.failed(error) + logBuildError(error) + return } } }) export const validateCapsWithA11y = (deviceName?: any, platformMeta?: { [key: string]: any; }, chromeOptions?: any) => { + /* Check if the current driver platform is eligible for Accessibility scan */ try { if (deviceName) { BStackLogger.warn('Accessibility Automation will run only on Desktop browsers.') @@ -413,99 +483,6 @@ export const isAccessibilityAutomationSession = (accessibilityFlag?: boolean | s return false } -export const createAccessibilityTestRun = errorHandler(async function createAccessibilityTestRun(options: BrowserstackConfig & Options.Testrunner, config: Options.Testrunner, bsConfig: UserConfig) { - const userName = getBrowserStackUser(config) - const accessKey = getBrowserStackKey(config) - - if (isUndefined(userName) || isUndefined(accessKey)) { - BStackLogger.error('Exception while creating test run for BrowserStack Accessibility Automation: Missing BrowserStack credentials') - return null - } - - const data = { - 'projectName': bsConfig.projectName, - 'buildName': bsConfig.buildName || - path.basename(path.resolve(process.cwd())), - 'startTime': (new Date()).toISOString(), - 'description': '', - 'source': { - frameworkName: 'WebdriverIO-' + config.framework, - frameworkVersion: bsConfig.bstackServiceVersion, - sdkVersion: bsConfig.bstackServiceVersion, - language: 'ECMAScript', - testFramework: 'webdriverIO', - testFrameworkVersion: bsConfig.bstackServiceVersion - }, - 'settings': bsConfig.accessibilityOptions || {}, - 'versionControl': await getGitMetaData(), - 'ciInfo': getCiInfo(), - 'hostInfo': { - hostname: hostname(), - platform: platform(), - type: type(), - version: version(), - arch: arch() - }, - 'browserstackAutomation': true, - } - - const requestOptions = { - json: data, - username: getBrowserStackUser(config), - password: getBrowserStackKey(config), - } - - try { - const response: any = await nodeRequest( - 'POST', 'v2/test_runs', requestOptions, ACCESSIBILITY_API_URL - ) - - BStackLogger.debug(`[Create Accessibility Test Run] Success response: ${JSON.stringify(response)}`) - - if (response.data.accessibilityToken) { - process.env.BSTACK_A11Y_JWT = response.data.accessibilityToken - } - if (response.data.id) { - process.env.BS_A11Y_TEST_RUN_ID = response.data.id - } - BStackLogger.debug(`BrowserStack Accessibility Automation Test Run ID: ${response.data.id}`) - - if (response.data) { - AccessibilityScripts.update(response.data) - AccessibilityScripts.store() - } - - return response.data.scannerVersion - } catch (error : any) { - if (error.response) { - BStackLogger.error( - `Exception while creating test run for BrowserStack Accessibility Automation: ${ - error.response.status - } ${error.response.statusText} ${JSON.stringify(error.response.data)}` - ) - } else { - const errorMessage = error.message - if (errorMessage === 'Invalid configuration passed.') { - BStackLogger.error( - `Exception while creating test run for BrowserStack Accessibility Automation: ${ - errorMessage || error.stack - }` - ) - for (const errorkey of error.errors){ - BStackLogger.error(errorkey.message) - } - } else { - BStackLogger.error( - `Exception while creating test run for BrowserStack Accessibility Automation: ${ - errorMessage || error.stack - }` - ) - } - } - return null - } -}) - export const performA11yScan = async (browser: WebdriverIO.Browser | WebdriverIO.MultiRemoteBrowser, isBrowserStackSession?: boolean, isAccessibility?: boolean | string, commandName?: string) : Promise<{ [key: string]: any; } | undefined> => { if (!isBrowserStackSession) { BStackLogger.warn('Not a BrowserStack Automate session, cannot perform Accessibility scan.') @@ -570,53 +547,6 @@ export const getA11yResultsSummary = async (browser: WebdriverIO.Browser, isBrow } } -export const stopAccessibilityTestRun = errorHandler(async function stopAccessibilityTestRun() { - 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' - if (!hasA11yJwtToken) { - return { - status: 'error', - message: 'Build creation had failed.' - } - } - - const data = { - 'endTime': (new Date()).toISOString(), - } - - const requestOptions = { ...{ - json: data, - headers: { - 'Authorization': `Bearer ${process.env.BSTACK_A11Y_JWT}`, - } - } } - - try { - const response: any = await nodeRequest( - 'PUT', 'test_runs/stop', requestOptions, ACCESSIBILITY_API_URL - ) - - if (response.data && response.data.error) { - throw new Error('Invalid request: ' + response.data.error) - } else if (response.error) { - throw new Error('Invalid request: ' + response.error) - } else { - BStackLogger.info(`BrowserStack Accessibility Automation Test Run marked as completed at ${new Date().toISOString()}`) - return { status: 'success', message: '' } - } - } catch (error : any) { - if (error.response && error.response.status && error.response.statusText && error.response.data) { - BStackLogger.error(`Exception while marking completion of BrowserStack Accessibility Automation Test Run: ${error.response.status} ${error.response.statusText} ${JSON.stringify(error.response.data)}`) - } else { - BStackLogger.error(`Exception while marking completion of BrowserStack Accessibility Automation Test Run: ${error.message || util.format(error)}`) - } - return { - status: 'error', - message: error.message || - (error.response ? `${error.response.status}:${error.response.statusText}` : error) - } - } -}) - export const stopBuildUpstream = o11yErrorHandler(async function stopBuildUpstream(killSignal: string|null = null) { const stopBuildUsage = UsageStats.getInstance().stopBuildUsage stopBuildUsage.triggered() @@ -628,7 +558,7 @@ export const stopBuildUpstream = o11yErrorHandler(async function stopBuildUpstre } } - const jwtToken = process.env[TESTOPS_JWT_ENV] + const jwtToken = process.env[BROWSERSTACK_TESTHUB_JWT] if (!jwtToken) { stopBuildUsage.failed('Token/buildID is undefined, build creation might have failed') BStackLogger.debug('[STOP_BUILD] Missing Authentication Token/ Build ID') @@ -649,7 +579,7 @@ export const stopBuildUpstream = o11yErrorHandler(async function stopBuildUpstre } try { - const url = `${DATA_ENDPOINT}/api/v1/builds/${process.env[TESTOPS_BUILD_ID_ENV]}/stop` + const url = `${DATA_ENDPOINT}/api/v1/builds/${process.env[BROWSERSTACK_TESTHUB_UUID]}/stop` const response = await got.put(url, { agent: DEFAULT_REQUEST_CONFIG.agent, headers: { @@ -1123,7 +1053,7 @@ export async function batchAndPostEvents (eventUrl: string, kind: string, data: throw new Error('Build not completed yet') } - const jwtToken = process.env[TESTOPS_JWT_ENV] + const jwtToken = process.env[BROWSERSTACK_TESTHUB_JWT] if (!jwtToken) { throw new Error('Missing authentication Token') } @@ -1217,7 +1147,11 @@ export function getBrowserStackKey(config: Options.Testrunner) { } export function isUndefined(value: any) { - return value === undefined || value === null + let res = (value === undefined || value === null) + if (typeof value === 'string') { + res = res || value === '' + } + return res } export function isTrue(value?: any) { diff --git a/packages/wdio-browserstack-service/tests/cleanup.test.ts b/packages/wdio-browserstack-service/tests/cleanup.test.ts index 3874ed8fc50..e8a9d24bf49 100644 --- a/packages/wdio-browserstack-service/tests/cleanup.test.ts +++ b/packages/wdio-browserstack-service/tests/cleanup.test.ts @@ -4,7 +4,7 @@ import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import fs from 'node:fs' import * as FunnelTestEvent from '../src/instrumentation/funnelInstrumentation.js' import * as bstackLogger from '../src/bstackLogger.js' -import { TESTOPS_JWT_ENV } from '../src/constants.js' +import { BROWSERSTACK_TESTHUB_JWT } from '../src/constants.js' const bstackLoggerSpy = vi.spyOn(bstackLogger.BStackLogger, 'logToFile') bstackLoggerSpy.mockImplementation(() => {}) @@ -31,7 +31,7 @@ describe('BStackCleanup', () => { it('executes observability cleanup if --observability is present in argv', async () => { vi.spyOn(utils, 'stopBuildUpstream') process.argv.push('--observability') - process.env[TESTOPS_JWT_ENV] = 'some jwt' + process.env[BROWSERSTACK_TESTHUB_JWT] = 'some jwt' await BStackCleanup.startCleanup() @@ -61,7 +61,7 @@ describe('BStackCleanup', () => { }) it('invoke stop call for observability when jwt is set', async () => { - process.env[TESTOPS_JWT_ENV] = 'jwtToken' + process.env[BROWSERSTACK_TESTHUB_JWT] = 'jwtToken' await BStackCleanup.executeObservabilityCleanup({}) expect(stopBuildUpstreamSpy).toBeCalledTimes(1) }) diff --git a/packages/wdio-browserstack-service/tests/insights-handler.test.ts b/packages/wdio-browserstack-service/tests/insights-handler.test.ts index a3613182170..0bc8e470cf1 100644 --- a/packages/wdio-browserstack-service/tests/insights-handler.test.ts +++ b/packages/wdio-browserstack-service/tests/insights-handler.test.ts @@ -786,7 +786,7 @@ describe('appendTestItemLog', function () { }) it('should upload with current test uuid for log', function () { - insightsHandler['_currentTest'] = { uuid: 'some_uuid' } + InsightsHandler['currentTest'] = { uuid: 'some_uuid' } insightsHandler['appendTestItemLog'](testLogObj) expect(testLogObj.test_run_uuid).toBe('some_uuid') expect(sendDataSpy).toBeCalledTimes(1) @@ -800,6 +800,7 @@ describe('appendTestItemLog', function () { }) it('should not upload log if hook is finished', function () { + InsightsHandler['currentTest'] = {} insightsHandler['_currentHook'] = { uuid: 'some_uuid', finished: true } insightsHandler['appendTestItemLog'](testLogObj) expect(testLogObj.hook_run_uuid).toBe(undefined) @@ -827,7 +828,7 @@ describe('processCucumberHook', function () { it ('should send data for before event', function () { cucumberHookTypeSpy.mockReturnValue('BEFORE_ALL') - insightsHandler['_currentTest'].uuid = 'test_uuid' + InsightsHandler['currentTest'].uuid = 'test_uuid' insightsHandler['processCucumberHook'](undefined, { event: 'before', hookUUID: 'hook_uuid' }) expect(getHookRunDataForCucumberSpy).toBeCalledWith(expect.objectContaining({ uuid: 'hook_uuid', @@ -846,3 +847,39 @@ describe('processCucumberHook', function () { expect(getHookRunDataForCucumberSpy).toBeCalledWith(hookObj, 'HookRunFinished', resultObj) }) }) + +describe('sendCBTInfo', () => { + beforeAll(() => { + insightsHandler = new InsightsHandler(browser, 'framework') + }) + it('should not call cbtSessionCreated', () => { + const cbtSessionCreatedSpy = vi.spyOn(Listener.getInstance(), 'cbtSessionCreated').mockImplementation(() => { return [] as any }) + insightsHandler.sendCBTInfo() + expect(cbtSessionCreatedSpy).toBeCalledTimes(0) + }) + it('should call cbtSessionCreated', () => { + insightsHandler.currentTestId = 'abc' + const cbtSessionCreatedSpy = vi.spyOn(Listener.getInstance(), 'cbtSessionCreated').mockImplementation(() => { return [] as any }) + insightsHandler.sendCBTInfo() + expect(cbtSessionCreatedSpy).toBeCalled() + }) +}) + +describe('flushCBTDataQueue', () => { + beforeAll(() => { + insightsHandler = new InsightsHandler(browser, 'framework') + }) + it('flushCBTDataQueue should not call cbtSessionCreated', () => { + insightsHandler.cbtQueue = [{ uuid: 'abc', integrations: {} }] + const cbtSessionCreatedSpy = vi.spyOn(Listener.getInstance(), 'cbtSessionCreated').mockImplementation(() => { return [] as any }) + insightsHandler.flushCBTDataQueue() + expect(cbtSessionCreatedSpy).toBeCalledTimes(0) + }) + it('flushCBTDataQueue should call cbtSessionCreated', () => { + insightsHandler.currentTestId = 'abc' + insightsHandler.cbtQueue = [{ uuid: 'abc', integrations: {} }] + const cbtSessionCreatedSpy = vi.spyOn(Listener.getInstance(), 'cbtSessionCreated').mockImplementation(() => { return [] as any }) + insightsHandler.flushCBTDataQueue() + expect(cbtSessionCreatedSpy).toBeCalled() + }) +}) diff --git a/packages/wdio-browserstack-service/tests/instrumentation/funnelInstrumentation.test.ts b/packages/wdio-browserstack-service/tests/instrumentation/funnelInstrumentation.test.ts index 5e8cc09a812..9b004e97487 100644 --- a/packages/wdio-browserstack-service/tests/instrumentation/funnelInstrumentation.test.ts +++ b/packages/wdio-browserstack-service/tests/instrumentation/funnelInstrumentation.test.ts @@ -27,8 +27,8 @@ const config = { } const expectedEventData = { - userName: config.userName, - accessKey: config.accessKey, + userName: '[REDACTED]', + accessKey: '[REDACTED]', event_type: 'SDKTestAttempted', detectedFramework: 'WebdriverIO-framework', event_properties: { @@ -130,7 +130,7 @@ describe('funnelInstrumentation', () => { }) it('fireFunnelRequest sends request with correct data', async () => { - const data = { key: 'value', userName: 'some_name', accessKey: 'some_key' } + const data = { key: 'value', userName: '[REDACTED]', accessKey: '[REDACTED]' } await FunnelTestEvent.fireFunnelRequest(data) expect(got.post).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({ headers: expect.any(Object), @@ -157,8 +157,8 @@ describe('funnelInstrumentation', () => { username: config.userName, password: config.accessKey, json: { - userName: config.userName, - accessKey: config.accessKey, + userName: '[REDACTED]', + accessKey: '[REDACTED]', event_type: 'SDKTestTcgDownResponse', detectedFramework: 'WebdriverIO-framework', event_properties: { @@ -192,8 +192,8 @@ describe('funnelInstrumentation', () => { username: config.userName, password: config.accessKey, json: { - userName: config.userName, - accessKey: config.accessKey, + userName: '[REDACTED]', + accessKey: '[REDACTED]', event_type: 'SDKTestTcgAuthFailure', detectedFramework: 'WebdriverIO-framework', event_properties: { @@ -236,8 +236,8 @@ describe('funnelInstrumentation', () => { username: config.userName, password: config.accessKey, json: { - userName: config.userName, - accessKey: config.accessKey, + userName: '[REDACTED]', + accessKey: '[REDACTED]', event_type: 'SDKTestTcgtInitSuccessful', detectedFramework: 'WebdriverIO-framework', event_properties: { @@ -276,8 +276,8 @@ describe('funnelInstrumentation', () => { username: config.userName, password: config.accessKey, json: { - userName: config.userName, - accessKey: config.accessKey, + userName: '[REDACTED]', + accessKey: '[REDACTED]', event_type: 'SDKTestInitFailedResponse', detectedFramework: 'WebdriverIO-framework', event_properties: { @@ -319,8 +319,8 @@ describe('funnelInstrumentation', () => { username: config.userName, password: config.accessKey, json: { - userName: config.userName, - accessKey: config.accessKey, + userName: '[REDACTED]', + accessKey: '[REDACTED]', event_type: 'SDKTestInvalidTcgAuthResponseWithUserImpact', detectedFramework: 'WebdriverIO-framework', event_properties: { diff --git a/packages/wdio-browserstack-service/tests/launcher.test.ts b/packages/wdio-browserstack-service/tests/launcher.test.ts index 8666d194c25..7ae0625a069 100644 --- a/packages/wdio-browserstack-service/tests/launcher.test.ts +++ b/packages/wdio-browserstack-service/tests/launcher.test.ts @@ -13,7 +13,7 @@ import BrowserstackLauncher from '../src/launcher.js' import type { BrowserstackConfig } from '../src/types.js' import * as utils from '../src/util.js' import * as bstackLogger from '../src/bstackLogger.js' -import { RERUN_ENV, RERUN_TESTS_ENV, TESTOPS_BUILD_ID_ENV } from '../src/constants.js' +import { RERUN_ENV, RERUN_TESTS_ENV, BROWSERSTACK_TESTHUB_UUID } from '../src/constants.js' import * as FunnelInstrumentation from '../src/instrumentation/funnelInstrumentation.js' vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) @@ -566,18 +566,27 @@ describe('onPrepare', () => { vi.clearAllMocks() }) - it('should create accessibility test run if accessibility flag is true', async () => { - const createAccessibilityTestRunSpy = vi.spyOn(utils, 'createAccessibilityTestRun').mockReturnValue('0.0.6.0') - const serviceOptions = { accessibility: true } + it('should launch testhub build when accessibility is true', async () => { + const launchTestSessionSpy = vi.spyOn(utils, 'launchTestSession') + const serviceOptions = { accessibility: true, testObservability: false } const service = new BrowserstackLauncher(serviceOptions as any, caps, config) vi.spyOn(service, '_updateObjectTypeCaps').mockImplementation(() => {}) await service.onPrepare(config, caps) - expect(createAccessibilityTestRunSpy).toHaveBeenCalledTimes(1) - vi.clearAllMocks() + expect(launchTestSessionSpy).toHaveBeenCalledOnce() + }) + + it('should launch testhub build when observability is true', async () => { + const launchTestSessionSpy = vi.spyOn(utils, 'launchTestSession') + const serviceOptions = { accessibility: false, testObservability: true } + const service = new BrowserstackLauncher(serviceOptions as any, caps, config) + vi.spyOn(service, '_updateObjectTypeCaps').mockImplementation(() => {}) + await service.onPrepare(config, caps) + expect(launchTestSessionSpy).toHaveBeenCalledOnce() + }) it('should add accessibility options after filtering not allowed caps', async () => { - const createAccessibilityTestRunSpy = vi.spyOn(utils, 'createAccessibilityTestRun').mockReturnValue('0.0.6.0') + const launchTestSessionSpy = vi.spyOn(utils, 'launchTestSession') const caps: any = [{ 'bstack:options': { buildName: 'browserstack wdio build' } }, @@ -588,7 +597,7 @@ describe('onPrepare', () => { const service = new BrowserstackLauncher(serviceOptions as any, caps, config) const capabilities = [{ 'bstack:options': { buildName: 'browserstack wdio build' } }, { 'bstack:options': { buildName: 'browserstack wdio build' } }] await service.onPrepare(config, capabilities) - expect(createAccessibilityTestRunSpy).toHaveBeenCalledTimes(1) + expect(launchTestSessionSpy).toHaveBeenCalledOnce() expect(capabilities[0]['bstack:options']).toEqual({ buildName: 'browserstack wdio build', accessibilityOptions: { wcagVersion: 'wcag2aa' } }) vi.clearAllMocks() }) @@ -632,12 +641,12 @@ describe('onComplete', () => { }) it('should stop accessibility test run on complete', () => { - const createAccessibilityTestRunSpy = vi.spyOn(utils, 'stopAccessibilityTestRun') + const stopBuildUpstreamSpy = vi.spyOn(utils, 'stopBuildUpstream') vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true) const service = new BrowserstackLauncher({} as any, [{}] as any, {} as any) service.onComplete() - expect(createAccessibilityTestRunSpy).toHaveBeenCalledTimes(1) + expect(stopBuildUpstreamSpy).toHaveBeenCalledTimes(1) }) }) @@ -1283,9 +1292,9 @@ describe('_uploadServiceLogs', () => { }] it('get observability build id', async() => { - process.env[TESTOPS_BUILD_ID_ENV] = 'obs123' + process.env[BROWSERSTACK_TESTHUB_UUID] = 'obs123' expect(service._getClientBuildUuid()).toEqual('obs123') - delete process.env[TESTOPS_BUILD_ID_ENV] + delete process.env[BROWSERSTACK_TESTHUB_UUID] }) const service = new BrowserstackLauncher(options as any, caps, config) @@ -1308,9 +1317,9 @@ describe('_getClientBuildUuid', () => { const service = new BrowserstackLauncher(options as any, caps, config) it('get observability build id', async() => { - process.env[TESTOPS_BUILD_ID_ENV] = 'obs123' + process.env[BROWSERSTACK_TESTHUB_UUID] = 'obs123' expect(service._getClientBuildUuid()).toEqual('obs123') - delete process.env[TESTOPS_BUILD_ID_ENV] + delete process.env[BROWSERSTACK_TESTHUB_UUID] }) it('get randomly generated id if both the conditions fail', async() => { diff --git a/packages/wdio-browserstack-service/tests/reporter.test.ts b/packages/wdio-browserstack-service/tests/reporter.test.ts index 9e9d62902f4..77c321d6e07 100644 --- a/packages/wdio-browserstack-service/tests/reporter.test.ts +++ b/packages/wdio-browserstack-service/tests/reporter.test.ts @@ -330,7 +330,7 @@ describe('test-reporter', () => { }) it('should upload with current test uuid for log', function () { - reporter['_currentTest'] = { uuid: 'some_uuid' } + TestReporter['currentTest'] = { uuid: 'some_uuid' } reporter['appendTestItemLog'](testLogObj) expect(testLogObj.test_run_uuid).toBe('some_uuid') expect(sendDataSpy).toBeCalledTimes(1) @@ -344,6 +344,7 @@ describe('test-reporter', () => { }) it('should not upload log if hook is finished', function () { + TestReporter['currentTest'] = {} reporter['_currentHook'] = { uuid: 'some_uuid', finished: true } reporter['appendTestItemLog'](testLogObj) expect(testLogObj.hook_run_uuid).toBe(undefined) diff --git a/packages/wdio-browserstack-service/tests/service.test.ts b/packages/wdio-browserstack-service/tests/service.test.ts index 01d8037c491..2d93c8ce238 100644 --- a/packages/wdio-browserstack-service/tests/service.test.ts +++ b/packages/wdio-browserstack-service/tests/service.test.ts @@ -550,12 +550,12 @@ describe('afterStep', () => { describe('beforeScenario', () => { const service = new BrowserstackService({}, [] as any, { user: 'foo', key: 'bar' } as any) - it('call insightsHandler.beforeScenario', () => { + it('call insightsHandler.beforeScenario', async () => { service['_insightsHandler'] = new InsightsHandler(browser) + service['_accessibilityHandler'] = undefined vi.spyOn(utils, 'getUniqueIdentifierForCucumber').mockReturnValue('test title') const methodSpy = vi.spyOn(service['_insightsHandler'], 'beforeScenario') - service.beforeScenario({ pickle: { name: '', tags: [] }, gherkinDocument: { uri: '', feature: { name: '', description: '' } } } as any) - + await service.beforeScenario({ pickle: { name: '', tags: [] }, gherkinDocument: { uri: '', feature: { name: '', description: '' } } } as any) expect(methodSpy).toBeCalled() }) }) diff --git a/packages/wdio-browserstack-service/tests/testHub/utils.test.ts b/packages/wdio-browserstack-service/tests/testHub/utils.test.ts new file mode 100644 index 00000000000..4910e0912bd --- /dev/null +++ b/packages/wdio-browserstack-service/tests/testHub/utils.test.ts @@ -0,0 +1,169 @@ +import { describe, expect, it, vi, beforeEach } from 'vitest' +import path from 'node:path' +import logger from '@wdio/logger' +import * as utils from '../../src/testHub/utils.js' +import * as bstackLogger from '../../src/bstackLogger.js' +import { BROWSERSTACK_OBSERVABILITY, BROWSERSTACK_ACCESSIBILITY } from '../../src/constants.js' + +describe('getProductMap', () => { + let config = {} + + beforeEach(() => { + config = { + testObservability: { + enabled : true + }, + accessibility: false, + percy: false, + automate: true, + appAutomate: false + } + }) + + it('should create a valid product map', () => { + const productMap = utils.getProductMap(config as any) + const expectedProductMap = { + 'observability': true, + 'accessibility': false, + 'percy': false, + 'automate': true, + 'app_automate': false + } + expect(productMap).toEqual(expectedProductMap) + }) +}) + +describe('shouldProcessEventForTesthub', () => { + beforeEach(() => { + delete process.env['BROWSERSTACK_OBSERVABILITY'] + delete process.env['BROWSERSTACK_ACCESSIBILITY'] + delete process.env['BROWSERSTACK_PERCY'] + }) + + it('should return true when only observability is true', () => { + process.env['BROWSERSTACK_OBSERVABILITY'] = 'true' + expect(utils.shouldProcessEventForTesthub('')).to.equal(true) + }) + + it('should return true when only accessibility is true', () => { + process.env['BROWSERSTACK_ACCESSIBILITY'] = 'true' + expect(utils.shouldProcessEventForTesthub('')).to.equal(true) + }) + + it('should return true when only percy is true', () => { + process.env['BROWSERSTACK_PERCY'] = 'true' + expect(utils.shouldProcessEventForTesthub('')).to.equal(true) + }) + + it('should be false for Hook event when accessibility is only true', () => { + process.env['BROWSERSTACK_ACCESSIBILITY'] = 'true' + expect(utils.shouldProcessEventForTesthub('HookRunFinished')).to.equal(false) + }) + + it('should be false for Log event when only percy is true', () => { + process.env['BROWSERSTACK_PERCY'] = 'true' + expect(utils.shouldProcessEventForTesthub('CBTSessionCreated')).to.equal(false) + }) +}) + +describe('logBuildError', () => { + const log = logger('test') + vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) + const bstackLoggerSpy = vi.spyOn(bstackLogger.BStackLogger, 'logToFile') + bstackLoggerSpy.mockImplementation(() => {}) + + it('should log error for ERROR_INVALID_CREDENTIALS', () => { + vi.mocked(log.error).mockClear() + const logErrorMock = vi.spyOn(log, 'error') + const errorJson = { + errors: [ + { + key: 'ERROR_INVALID_CREDENTIALS', + message: 'Access to BrowserStack Test Observability denied due to incorrect credentials.' + } + ], + } + utils.logBuildError(errorJson as any, 'observability') + expect(logErrorMock.mock.calls[0][0]).toContain('Access to BrowserStack Test Observability denied due to incorrect credentials.') + }) + + it('should log error for ERROR_ACCESS_DENIED', () => { + vi.mocked(log.error).mockClear() + const logErrorMock = vi.spyOn(log, 'info') + const errorJson = { + errors: [ + { + key: 'ERROR_ACCESS_DENIED', + message: 'Access to BrowserStack Test Observability denied.' + } + ], + } + utils.logBuildError(errorJson as any, 'observability') + expect(logErrorMock.mock.calls[0][0]).toContain('Access to BrowserStack Test Observability denied.') + }) + + it('should log error for ERROR_SDK_DEPRECATED', () => { + vi.mocked(log.error).mockClear() + vi.mocked(log.info).mockClear() + const logErrorMock = vi.spyOn(log, 'error') + const errorJson = { + errors: [ + { + key: 'ERROR_SDK_DEPRECATED', + message: 'Access to BrowserStack Test Observability denied due to SDK deprecation.' + } + ], + } + utils.logBuildError(errorJson as any, 'observability') + expect(logErrorMock.mock.calls[0][0]).toContain('Access to BrowserStack Test Observability denied due to SDK deprecation.') + }) + + it('should log error for RANDOM_ERROR_TYPE', () => { + vi.mocked(log.error).mockClear() + vi.mocked(log.info).mockClear() + const logErrorMock = vi.spyOn(log, 'error') + const errorJson = { + errors: [ + { + key: 'RANDOM_ERROR_TYPE', + message: 'Random error message.' + } + ], + } + utils.logBuildError(errorJson as any, 'observability') + expect(logErrorMock.mock.calls[0][0]).toContain('Random error message.') + }) + + it('should log error if error is null', () => { + vi.mocked(log.error).mockClear() + const logErrorMock = vi.spyOn(log, 'error') + utils.logBuildError(null, 'product_name') + expect(logErrorMock.mock.calls[0][0]).toContain('PRODUCT_NAME Build creation failed ') + }) + + it('handleErrorForObservability', () => { + const errorJson = { + errors: [ + { + key: 'RANDOM_ERROR_TYPE', + message: 'Random error message.' + } + ], + } + utils.handleErrorForObservability(errorJson) + expect(process.env[BROWSERSTACK_OBSERVABILITY]).toEqual('false') + }) + + it('handleErrorForAccessibility', () => { + const errorJson = { + errors: [ + { + key: 'RANDOM_ERROR_TYPE', + message: 'Random error message.' + } + ], + } + utils.handleErrorForAccessibility(errorJson) + expect(process.env[BROWSERSTACK_ACCESSIBILITY]).toEqual('false') + }) +}) diff --git a/packages/wdio-browserstack-service/tests/testOps/requestUtils.test.ts b/packages/wdio-browserstack-service/tests/testOps/requestUtils.test.ts index ad88cbc2bc7..21e2a93cea2 100644 --- a/packages/wdio-browserstack-service/tests/testOps/requestUtils.test.ts +++ b/packages/wdio-browserstack-service/tests/testOps/requestUtils.test.ts @@ -1,14 +1,14 @@ import { uploadEventData } from '../../src/testOps/requestUtils.js' import { describe, expect, it, vi } from 'vitest' import got from 'got' -import { TESTOPS_BUILD_COMPLETED_ENV, TESTOPS_JWT_ENV } from '../../src/constants.js' +import { TESTOPS_BUILD_COMPLETED_ENV, BROWSERSTACK_TESTHUB_JWT } from '../../src/constants.js' describe('uploadEventData', () => { const mockedGot = vi.mocked(got) it('got.post called', async () => { process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true' - process.env[TESTOPS_JWT_ENV] = 'jwt' + process.env[BROWSERSTACK_TESTHUB_JWT] = 'jwt' mockedGot.post = vi.fn().mockReturnValue({ json: () => Promise.resolve({ }), } as any) @@ -19,7 +19,7 @@ describe('uploadEventData', () => { it('got.post failed', async () => { process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true' - process.env[TESTOPS_JWT_ENV] = 'jwt' + process.env[BROWSERSTACK_TESTHUB_JWT] = 'jwt' mockedGot.post = vi.fn().mockReturnValue({ json: () => Promise.reject({ }), } as any) @@ -30,7 +30,7 @@ describe('uploadEventData', () => { it('got.post not called', async () => { process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true' - delete process.env[TESTOPS_JWT_ENV] + delete process.env[BROWSERSTACK_TESTHUB_JWT] mockedGot.post = vi.fn().mockReturnValue({ json: () => Promise.resolve({ }), } as any) diff --git a/packages/wdio-browserstack-service/tests/util.test.ts b/packages/wdio-browserstack-service/tests/util.test.ts index 1c9bfcb0fd1..89a71f86d77 100644 --- a/packages/wdio-browserstack-service/tests/util.test.ts +++ b/packages/wdio-browserstack-service/tests/util.test.ts @@ -1,11 +1,14 @@ import path from 'node:path' +import type { LaunchResponse } from '../src/types.js' + import { describe, expect, it, vi, beforeEach, afterEach, beforeAll } from 'vitest' import got from 'got' import gitRepoInfo from 'git-repo-info' import CrashReporter from '../src/crash-reporter.js' import logger from '@wdio/logger' import * as utils from '../src/util.js' +import logPatcher from '../src/logPatcher.js' import { getBrowserDescription, getBrowserCapabilities, @@ -35,12 +38,18 @@ import { validateCapsWithA11y, shouldScanTestForAccessibility, isAccessibilityAutomationSession, - createAccessibilityTestRun, isTrue, - uploadLogs + uploadLogs, + getObservabilityProduct, + isUndefined, + processTestObservabilityResponse, + processAccessibilityResponse, + processLaunchBuildResponse, + jsonifyAccessibilityArray, } from '../src/util.js' import * as bstackLogger from '../src/bstackLogger.js' -import { TESTOPS_BUILD_COMPLETED_ENV, TESTOPS_JWT_ENV } from '../src/constants.js' +import { BROWSERSTACK_OBSERVABILITY, TESTOPS_BUILD_COMPLETED_ENV, BROWSERSTACK_TESTHUB_JWT, BROWSERSTACK_ACCESSIBILITY } from '../src/constants.js' +import * as testHubUtils from '../src/testHub/utils.js' const log = logger('test') @@ -651,7 +660,7 @@ describe('stopBuildUpstream', () => { it('return error if completed but jwt token not present', async () => { process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true' - delete process.env[TESTOPS_JWT_ENV] + delete process.env[BROWSERSTACK_TESTHUB_JWT] const result: any = await stopBuildUpstream() @@ -662,7 +671,7 @@ describe('stopBuildUpstream', () => { it('return success if completed', async () => { process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true' - process.env[TESTOPS_JWT_ENV] = 'jwt' + process.env[BROWSERSTACK_TESTHUB_JWT] = 'jwt' mockedGot.put = vi.fn().mockReturnValue({ json: () => Promise.resolve({}), @@ -675,7 +684,7 @@ describe('stopBuildUpstream', () => { it('return error if failed', async () => { process.env[TESTOPS_BUILD_COMPLETED_ENV] = 'true' - process.env[TESTOPS_JWT_ENV] = 'jwt' + process.env[BROWSERSTACK_TESTHUB_JWT] = 'jwt' mockedGot.put = vi.fn().mockReturnValue({ json: () => Promise.reject({}), @@ -694,13 +703,14 @@ describe('stopBuildUpstream', () => { describe('launchTestSession', () => { const mockedGot = vi.mocked(got) vi.mocked(gitRepoInfo).mockReturnValue({} as any) + vi.spyOn(testHubUtils, 'getProductMap').mockReturnValue({} as any) it('return undefined if completed', async () => { mockedGot.post = vi.fn().mockReturnValue({ json: () => Promise.resolve({ build_hashed_id: 'build_id', jwt: 'jwt' }), } as any) - const result: any = await launchTestSession( { framework: 'framework' } as any, { }, {}) + const result: any = await launchTestSession( { framework: 'framework' } as any, { }, {}, {}) expect(got.post).toBeCalledTimes(1) expect(result).toEqual(undefined) }) @@ -1030,82 +1040,6 @@ describe('isAccessibilityAutomationSession', () => { }) }) -describe('createAccessibilityTestRun', () => { - const logInfoMock = vi.spyOn(log, 'error') - - beforeEach (() => { - vi.mocked(gitRepoInfo).mockReturnValue({} as any) - }) - - it('return null if BrowserStack credentials arre undefined', async () => { - const result: any = await createAccessibilityTestRun( { framework: 'framework' } as any, {}) - expect(result).toEqual(null) - expect(logInfoMock.mock.calls[2][0]) - .toContain('Exception while creating test run for BrowserStack Accessibility Automation: Missing BrowserStack credentials') - }) - - it('return undefined if completed', async () => { - vi.spyOn(utils, 'getGitMetaData').mockReturnValue({} as any) - vi.mocked(got).mockReturnValue({ - json: () => Promise.resolve({ data: { accessibilityToken: 'someToken', id: 'id', scannerVersion: '0.0.6.0' } }), - } as any) - - const result: any = await createAccessibilityTestRun( { framework: 'framework' } as any, { user: 'user', key: 'key' }, { bstackServiceVersion: '1.2.3' }) - expect(got).toBeCalledTimes(1) - expect(result).toEqual('0.0.6.0') - }) - - it('return undefined if completed', async () => { - vi.spyOn(utils, 'getGitMetaData').mockReturnValue({} as any) - vi.mocked(got).mockReturnValue({ - json: () => Promise.resolve({ accessibilityToken: 'someToken', id: 'id', scannerVersion: '0.0.6.0' }), - } as any) - - const result: any = await createAccessibilityTestRun( { framework: 'framework' } as any, { user: 'user', key: 'key' }, {}) - expect(got).toBeCalledTimes(1) - expect(result).toEqual(null) - expect(logInfoMock.mock.calls[3][0]).contains('Exception while creating test run for BrowserStack Accessibility Automation') - }) - - afterEach(() => { - (got as vi.Mock).mockClear() - }) -}) - -describe('stopAccessibilityTestRun', () => { - beforeEach (() => { - vi.mocked(gitRepoInfo).mockReturnValue({} as any) - }) - - it('return error object if ally token not defined', async () => { - process.env.BSTACK_A11Y_JWT = undefined - const result: any = await utils.stopAccessibilityTestRun() - expect(result).toEqual({ 'message': 'Build creation had failed.', 'status': 'error' }) - }) - - it('return success object if ally token defined and no error in response data', async () => { - process.env.BSTACK_A11Y_JWT = 'someToken' - vi.mocked(got).mockReturnValue({ - json: () => Promise.resolve({ data: {} }), - } as any) - const result: any = await utils.stopAccessibilityTestRun() - expect(result).toEqual({ 'message': '', 'status': 'success' }) - }) - - it('return error object if ally token defined and no error in response data', async () => { - process.env.BSTACK_A11Y_JWT = 'someToken' - vi.mocked(got).mockReturnValue({ - json: () => Promise.resolve({ data: { error: 'Some Error occurred' } }), - } as any) - const result: any = await utils.stopAccessibilityTestRun() - expect(result).toEqual({ 'message': 'Invalid request: Some Error occurred', 'status': 'error' }) - }) - - afterEach(() => { - (got as vi.Mock).mockClear() - }) -}) - describe('getA11yResults', () => { const browser = { sessionId: 'session123', @@ -1146,8 +1080,10 @@ describe('getA11yResults', () => { }) it('return results object if bstack as well as accessibility session', async () => { + process.env.BSTACK_A11Y_JWT = 'abc' vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true) await utils.getA11yResults((browser as WebdriverIO.Browser), true, true) + delete process.env.BSTACK_A11Y_JWT expect(browser.executeAsync).toBeCalledTimes(2) }) }) @@ -1192,8 +1128,10 @@ describe('getA11yResultsSummary', () => { }) it('return results object if bstack as well as accessibility session', async () => { + process.env.BSTACK_A11Y_JWT = 'abc' vi.spyOn(utils, 'isAccessibilityAutomationSession').mockReturnValue(true) await utils.getA11yResultsSummary((browser as WebdriverIO.Browser), true, true) + delete process.env.BSTACK_A11Y_JWT expect(browser.executeAsync).toBeCalledTimes(2) }) }) @@ -1318,3 +1256,205 @@ describe('getFailureObject', function () { }) }) }) + +describe('getObservabilityProduct', () => { + it ('should return app automate', function () { + expect(getObservabilityProduct(undefined, true)).toEqual('app-automate') + }) +}) + +describe('isUndefined', () => { + it ('should return true for empty string', function () { + expect(isUndefined('')).toEqual(true) + }) +}) + +describe('processTestObservabilityResponse', () => { + let response: LaunchResponse, handleErrorForObservabilitySpy + beforeAll(() => { + response = { + jwt: 'abc', + build_hashed_id: 'abc', + observability: { + success: true, + options: {}, + errors: undefined + }, + accessibility: { + success: true, + options: { + status: 'true', + commandsToWrap: { + scriptsToRun: [], + commands: [] + }, + scripts: [{ + name: 'abc', + command: 'abc' + }], + capabilities: [{ + name: 'abc', + value: 'abc' + }] + }, + errors: undefined + } + } + }) + it ('processTestObservabilityResponse should not log an error', function () { + processTestObservabilityResponse(response) + expect(process.env[BROWSERSTACK_OBSERVABILITY]).toEqual('true') + }) + it ('processTestObservabilityResponse should log error if observability success is false', function () { + handleErrorForObservabilitySpy = vi.spyOn(testHubUtils, 'handleErrorForObservability').mockReturnValue({} as any) + const res = response + res.observability!.success = false + processTestObservabilityResponse(res) + expect(handleErrorForObservabilitySpy).toBeCalled() + }) + it ('processTestObservabilityResponse should log error if observability field not found', function () { + handleErrorForObservabilitySpy = vi.spyOn(testHubUtils, 'handleErrorForObservability').mockReturnValue({} as any) + const res = response + res.observability = undefined + processTestObservabilityResponse(res) + expect(handleErrorForObservabilitySpy).toBeCalled() + }) + afterEach(() => { + handleErrorForObservabilitySpy?.mockClear() + }) +}) + +describe('processAccessibilityResponse', () => { + let response: LaunchResponse, handleErrorForAccessibilitySpy + beforeAll(() => { + response = { + jwt: 'abc', + build_hashed_id: 'abc', + observability: { + success: true, + options: {}, + errors: undefined + }, + accessibility: { + success: true, + options: { + status: 'true', + commandsToWrap: { + scriptsToRun: [], + commands: [] + }, + scripts: [{ + name: 'abc', + command: 'abc' + }], + capabilities: [ + { + name: 'accessibilityToken', + value: 'abc' + }, + { + name: 'scannerVersion', + value: 'abc' + } + ] + }, + errors: undefined + } + } + }) + it ('processAccessibilityResponse should not log an error', function () { + processAccessibilityResponse(response) + expect(process.env[BROWSERSTACK_ACCESSIBILITY]).toEqual('true') + }) + it ('processAccessibilityResponse should log error if accessibility success is false', function () { + handleErrorForAccessibilitySpy = vi.spyOn(testHubUtils, 'handleErrorForAccessibility').mockReturnValue({} as any) + const res = response + res.accessibility!.success = false + processAccessibilityResponse(res) + expect(handleErrorForAccessibilitySpy).toBeCalled() + }) + it ('processAccessibilityResponse should log error if accessibility field not found', function () { + handleErrorForAccessibilitySpy = vi.spyOn(testHubUtils, 'handleErrorForAccessibility').mockReturnValue({} as any) + const res = response + res.accessibility = undefined + processAccessibilityResponse(res) + expect(handleErrorForAccessibilitySpy).toBeCalled() + }) + afterEach(() => { + handleErrorForAccessibilitySpy?.mockClear() + }) +}) + +describe('processLaunchBuildResponse', () => { + let response: LaunchResponse, observabilitySpy, accessibilitySpy + beforeAll(() => { + response = { + jwt: 'abc', + build_hashed_id: 'abc', + observability: { + success: true, + options: {}, + errors: undefined + }, + accessibility: { + success: true, + options: { + status: 'true', + commandsToWrap: { + scriptsToRun: [], + commands: [] + }, + scripts: [{ + name: 'abc', + command: 'abc' + }], + capabilities: [{ + name: 'accessibilityToken', + value: 'abc' + }] + }, + errors: undefined + } + } + }) + beforeEach(() => { + observabilitySpy = vi.spyOn(utils, 'processTestObservabilityResponse').mockImplementation(() => {}) + accessibilitySpy = vi.spyOn(utils, 'processAccessibilityResponse').mockImplementation(() => {}) + }) + it ('processTestObservabilityResponse should be called', function () { + processLaunchBuildResponse(response, { testObservability: true, accessibility: true, capabilities: {} }) + expect(process.env[BROWSERSTACK_OBSERVABILITY]).toEqual('true') + }) + it ('processAccessibilityResponse should be called', function () { + processLaunchBuildResponse(response, { testObservability: true, accessibility: true, capabilities: {} }) + expect(process.env[BROWSERSTACK_ACCESSIBILITY]).toEqual('true') + }) + afterEach(() => { + observabilitySpy?.mockClear() + accessibilitySpy?.mockClear() + }) +}) + +describe('jsonifyAccessibilityArray', () => { + const array = [{ + name: 'accessibilityToken', + value: 'abc' + }] + it('jsonifyAccessibilityArray', () => { + expect(jsonifyAccessibilityArray(array, 'name', 'value')).toEqual({ 'accessibilityToken': 'abc' }) + }) +}) + +describe('logPatcher', () => { + const BSTestOpsPatcher = new logPatcher({}) + const emitSpy = vi.spyOn(process, 'emit') + it('logPatcher methods should emit data', () => { + BSTestOpsPatcher.info('abc') + BSTestOpsPatcher.error('abc') + BSTestOpsPatcher.warn('abc') + BSTestOpsPatcher.trace('abc') + BSTestOpsPatcher.debug('abc') + BSTestOpsPatcher.log('abc') + expect(emitSpy).toBeCalledTimes(6) + }) +})