diff --git a/.changeset/remove-eol-selfheal.md b/.changeset/remove-eol-selfheal.md new file mode 100644 index 0000000..87437e6 --- /dev/null +++ b/.changeset/remove-eol-selfheal.md @@ -0,0 +1,5 @@ +--- +"@wdio/browserstack-service": major +--- + +Remove the AI self-healing integration (the `selfHeal` option and its `@browserstack/ai-sdk-node` dependency). This integration is end-of-life and the upstream helpers it relied on have been removed, so self-heal is no longer supported by this service. diff --git a/package-lock.json b/package-lock.json index ad1443c..6a496ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,30 +57,6 @@ "node": ">=6.9.0" } }, - "node_modules/@browserstack/ai-sdk-node": { - "version": "1.5.17", - "resolved": "https://registry.npmjs.org/@browserstack/ai-sdk-node/-/ai-sdk-node-1.5.17.tgz", - "integrity": "sha512-odjnFulpBeF64UGHA+bIxkIcALYvEPznTl4U0hRT1AFfn4FqT+4wQdPBYnSnlc2XWTedv4zCDvbp4AFrtKXHEw==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "axios": "^1.7.4", - "uuid": "9.0.1" - } - }, - "node_modules/@browserstack/ai-sdk-node/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@bufbuild/buf": { "version": "1.70.0", "resolved": "https://registry.npmjs.org/@bufbuild/buf/-/buf-1.70.0.tgz", @@ -3770,12 +3746,6 @@ "node": ">= 0.4" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3792,18 +3762,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axios": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", - "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^2.1.0" - } - }, "node_modules/b4a": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", @@ -4130,6 +4088,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4433,18 +4392,6 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "14.0.3", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", @@ -4795,15 +4742,6 @@ "node": ">= 14" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/detect-indent": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", @@ -4945,6 +4883,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -5236,6 +5175,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5245,6 +5185,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5261,6 +5202,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5273,6 +5215,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6596,26 +6539,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -6648,22 +6571,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", - "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fs-extra": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", @@ -6704,6 +6611,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6809,6 +6717,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -6845,6 +6754,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -7029,6 +6939,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7111,6 +7022,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7123,6 +7035,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7138,6 +7051,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -8662,6 +8576,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8691,27 +8606,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -9720,15 +9614,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -12895,10 +12780,9 @@ }, "packages/browserstack-service": { "name": "@wdio/browserstack-service", - "version": "9.29.1", + "version": "9.35.2", "license": "MIT", "dependencies": { - "@browserstack/ai-sdk-node": "1.5.17", "@bufbuild/protobuf": "^2.5.2", "@grpc/grpc-js": "~1.13.5", "@percy/appium-app": "^2.0.9", diff --git a/packages/browserstack-service/package.json b/packages/browserstack-service/package.json index 0fdbc23..567bbba 100644 --- a/packages/browserstack-service/package.json +++ b/packages/browserstack-service/package.json @@ -53,7 +53,6 @@ "prepack": "npm run build" }, "dependencies": { - "@browserstack/ai-sdk-node": "1.5.17", "@bufbuild/protobuf": "^2.5.2", "@grpc/grpc-js": "~1.13.5", "@percy/appium-app": "^2.0.9", diff --git a/packages/browserstack-service/src/ai-handler.ts b/packages/browserstack-service/src/ai-handler.ts deleted file mode 100644 index 2664ed9..0000000 --- a/packages/browserstack-service/src/ai-handler.ts +++ /dev/null @@ -1,239 +0,0 @@ -import path from 'node:path' -import fs from 'node:fs' -import url from 'node:url' -import aiSDK from '@browserstack/ai-sdk-node' -import { BStackLogger } from './bstackLogger.js' -import { TCG_URL, TCG_INFO, SUPPORTED_BROWSERS_FOR_AI, BSTACK_SERVICE_VERSION, BSTACK_TCG_AUTH_RESULT } from './constants.js' -import { handleHealingInstrumentation } from './instrumentation/funnelInstrumentation.js' - -import type { Capabilities } from '@wdio/types' -import type BrowserStackConfig from './config.js' -import type { Options } from '@wdio/types' -import type { BrowserstackHealing } from '@browserstack/ai-sdk-node' -import { getBrowserStackUserAndKey, isBrowserstackInfra } from './util.js' -import type { BrowserstackOptions } from './types.js' -import PerformanceTester from './instrumentation/performance/performance-tester.js' -import * as PERFORMANCE_SDK_EVENTS from './instrumentation/performance/constants.js' - -class AiHandler { - authResult: BrowserstackHealing.InitSuccessResponse | BrowserstackHealing.InitErrorResponse - wdioBstackVersion: string - constructor() { - this.authResult = JSON.parse(process.env[BSTACK_TCG_AUTH_RESULT] || '{}') - this.wdioBstackVersion = BSTACK_SERVICE_VERSION - } - - async authenticateUser(user: string, key: string) { - return await aiSDK.BrowserstackHealing.init(key, user, TCG_URL, this.wdioBstackVersion) - } - - updateCaps( - authResult: BrowserstackHealing.InitSuccessResponse | BrowserstackHealing.InitErrorResponse, - options: BrowserstackOptions, - caps: Array | Capabilities.ResolvedTestrunnerCapabilities - ) { - const installExtCondition = authResult.isAuthenticated === true && (authResult.defaultLogDataEnabled === true || options.selfHeal === true) - if (installExtCondition){ - if (Array.isArray(caps)) { - const newCaps= aiSDK.BrowserstackHealing.initializeCapabilities(caps[0]) - caps[0] = newCaps - } else if (typeof caps === 'object') { - caps = aiSDK.BrowserstackHealing.initializeCapabilities(caps) - } - } else if (options.selfHeal === true) { - const healingWarnMessage = (authResult as aiSDK.BrowserstackHealing.InitErrorResponse).message - BStackLogger.warn(`Healing Auth failed. Disabling healing for this session. Reason: ${healingWarnMessage}`) - } - - return caps - } - - async setToken(sessionId: string, sessionToken: string){ - await aiSDK.BrowserstackHealing.setToken(sessionId, sessionToken, TCG_URL) - } - - async installFirefoxExtension(browser: WebdriverIO.Browser){ - const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) - const extensionPath = path.resolve(__dirname, aiSDK.BrowserstackHealing.getFirefoxAddonPath()) - const extFile = fs.readFileSync(extensionPath) - await browser.installAddOn(extFile.toString('base64'), true) - } - - async handleHealing(orginalFunc: (arg0: string, arg1: string) => { error?: string }, using: string, value: string, browser: WebdriverIO.Browser, options: BrowserstackOptions){ - const sessionId = browser.sessionId - - // a utility function to escape single and double quotes - const escapeString = (str: string) => str.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t') - - const tcgDetails = escapeString(JSON.stringify({ - region: TCG_INFO.tcgRegion, - tcgUrls: { - [TCG_INFO.tcgRegion]: { - endpoint: TCG_INFO.tcgUrl.split('://')[1] - } - } - })) - - const locatorType = escapeString(using) - const locatorValue = escapeString(value) - - this.authResult = this.authResult as BrowserstackHealing.InitSuccessResponse - - try { - const result = await orginalFunc(using, value) - if (!result.error) { - const script = await aiSDK.BrowserstackHealing.logData(locatorType, locatorValue, undefined, undefined, this.authResult.groupId, sessionId, undefined, tcgDetails) - if (script) { - await browser.execute(script) - } - return result - } - if (options.selfHeal === true && this.authResult.isHealingEnabled) { - BStackLogger.info('findElement failed, trying to heal') - PerformanceTester.start(PERFORMANCE_SDK_EVENTS.AI_EVENTS.SELF_HEAL_STEP) - const script = await aiSDK.BrowserstackHealing.healFailure(locatorType, locatorValue, undefined, undefined, this.authResult.userId, this.authResult.groupId, sessionId, undefined, undefined, this.authResult.isGroupAIEnabled, tcgDetails) - if (script) { - await browser.execute(script) - PerformanceTester.start(PERFORMANCE_SDK_EVENTS.AI_EVENTS.SELF_HEAL_GET_RESULT) - const tcgData = await aiSDK.BrowserstackHealing.pollResult(TCG_URL, sessionId, this.authResult.sessionToken) - PerformanceTester.end(PERFORMANCE_SDK_EVENTS.AI_EVENTS.SELF_HEAL_GET_RESULT) - if (tcgData && tcgData.selector && tcgData.value){ - const healedResult = await orginalFunc(tcgData.selector, tcgData.value) - BStackLogger.info('Healing worked, element found: ' + tcgData.selector + ': ' + tcgData.value) - PerformanceTester.end(PERFORMANCE_SDK_EVENTS.AI_EVENTS.SELF_HEAL_STEP) - return healedResult.error ? result : healedResult - } - PerformanceTester.end(PERFORMANCE_SDK_EVENTS.AI_EVENTS.SELF_HEAL_STEP, false, 'No healed result found') - } else { - PerformanceTester.end(PERFORMANCE_SDK_EVENTS.AI_EVENTS.SELF_HEAL_STEP, false, 'No healing script generated') - } - } - } catch (err) { - PerformanceTester.end(PERFORMANCE_SDK_EVENTS.AI_EVENTS.SELF_HEAL_STEP, false, String(err)) - if (options.selfHeal === true) { - BStackLogger.warn('Something went wrong while healing. Disabling healing for this command') - } else { - BStackLogger.warn('Error in findElement: ' + err + 'using: ' + using + 'value: ' + value) - } - } - return await orginalFunc(using, value) - } - - addMultiRemoteCaps ( - authResult: BrowserstackHealing.InitSuccessResponse | BrowserstackHealing.InitErrorResponse, - config: Options.Testrunner, - browserStackConfig: BrowserStackConfig, - options: BrowserstackOptions, - caps: Capabilities.RequestedMultiremoteCapabilities, - browser: string - ) { - if ( caps[browser].capabilities && - !(isBrowserstackInfra(caps[browser])) && - SUPPORTED_BROWSERS_FOR_AI.includes((caps[browser]?.capabilities as WebdriverIO.Capabilities)?.browserName?.toLowerCase() || 'unknown browser') - ) { - const innerConfig = getBrowserStackUserAndKey(config, options) - if (innerConfig?.user && innerConfig.key) { - // @ts-expect-error fix type - handleHealingInstrumentation(authResult, browserStackConfig, options.selfHeal) - caps[browser].capabilities = this.updateCaps(authResult, options, caps[browser].capabilities as WebdriverIO.Capabilities) as WebdriverIO.Capabilities - } - } - } - - handleMultiRemoteSetup( - authResult: BrowserstackHealing.InitSuccessResponse | BrowserstackHealing.InitErrorResponse, - config: Options.Testrunner, - browserStackConfig: BrowserStackConfig, - options: BrowserstackOptions, - caps: Capabilities.RequestedMultiremoteCapabilities, - ) { - const browserNames = Object.keys(caps) - for (let i = 0; i < browserNames.length; i++) { - const browser = browserNames[i] - this.addMultiRemoteCaps(authResult, config, browserStackConfig, options, caps, browser) - } - } - - async setup( - config: Options.Testrunner, - browserStackConfig: BrowserStackConfig, - options: BrowserstackOptions, - caps: WebdriverIO.Capabilities, - isMultiremote: boolean - ) { - try { - const innerConfig = getBrowserStackUserAndKey(config, options) - if (innerConfig?.user && innerConfig.key) { - const authResult = await this.authenticateUser(innerConfig.user, innerConfig.key) - process.env[BSTACK_TCG_AUTH_RESULT] = JSON.stringify(authResult) - if (!isMultiremote && SUPPORTED_BROWSERS_FOR_AI.includes(caps.browserName?.toLowerCase() || 'unknown browser')) { - - // @ts-expect-error fix type - handleHealingInstrumentation(authResult, browserStackConfig, options.selfHeal) - this.updateCaps(authResult, options, caps) - - } else if (isMultiremote) { - this.handleMultiRemoteSetup(authResult, config, browserStackConfig, options, caps as unknown as Capabilities.RequestedMultiremoteCapabilities) - } - } - - } catch (err) { - if (options.selfHeal === true) { - BStackLogger.warn(`Error while initiliazing Browserstack healing Extension ${err}`) - } - } - - return caps - } - - async handleSelfHeal(options: BrowserstackOptions, browser: WebdriverIO.Browser) { - - if (SUPPORTED_BROWSERS_FOR_AI.includes((browser.capabilities as Capabilities.BrowserStackCapabilities)?.browserName?.toLowerCase() as string)) { - const authInfo = this.authResult as BrowserstackHealing.InitSuccessResponse - - if (Object.keys(authInfo).length === 0 && options.selfHeal === true) { - BStackLogger.debug('TCG Auth result is empty') - return - } - - const { isAuthenticated, sessionToken, defaultLogDataEnabled } = authInfo - - if (isAuthenticated && (defaultLogDataEnabled === true || options.selfHeal === true)) { - await this.setToken(browser.sessionId, sessionToken) - - if ((browser.capabilities as Capabilities.BrowserStackCapabilities).browserName === 'firefox') { - await this.installFirefoxExtension(browser) - } - - // @ts-expect-error fix type - browser.overwriteCommand('findElement', async (orginalFunc: unknown, using: string, value: string) => { - // @ts-expect-error fix type - return await this.handleHealing(orginalFunc, using, value, browser, options) - }) - } - } - } - - async selfHeal(options: BrowserstackOptions, caps: Capabilities.ResolvedTestrunnerCapabilities, browser: WebdriverIO.Browser) { - try { - - const multiRemoteBrowsers = Object.keys(caps).filter(e => Object.keys(browser).includes(e)) - if (multiRemoteBrowsers.length > 0) { - for (let i = 0; i < multiRemoteBrowsers.length; i++) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const remoteBrowser = (browser as any)[multiRemoteBrowsers[i]] - await this.handleSelfHeal(options, remoteBrowser) - } - } else { - await this.handleSelfHeal(options, browser) - } - - } catch (err) { - if (options.selfHeal === true) { - BStackLogger.warn(`Error while setting up self-healing: ${err}. Disabling healing for this session.`) - } - } - } -} - -export default new AiHandler() diff --git a/packages/browserstack-service/src/constants.ts b/packages/browserstack-service/src/constants.ts index c64bfa8..19f6495 100644 --- a/packages/browserstack-service/src/constants.ts +++ b/packages/browserstack-service/src/constants.ts @@ -164,15 +164,6 @@ export const FUNNEL_INSTRUMENTATION_URL = 'https://api.browserstack.com/sdk/v1/e export const EDS_URL = 'https://eds.browserstack.com' -export const SUPPORTED_BROWSERS_FOR_AI = ['chrome', 'microsoftedge', 'firefox'] - -export const TCG_URL = 'https://tcg.browserstack.com' - -export const TCG_INFO = { - tcgRegion: 'use', - tcgUrl: TCG_URL, -} - // Smart Selection Mode Constants export const SMART_SELECTION_MODE_RELEVANT_FIRST = 'relevantFirst' export const SMART_SELECTION_MODE_RELEVANT_ONLY = 'relevantOnly' @@ -182,9 +173,6 @@ export const SMART_SELECTION_MODE_RELEVANT_ONLY = 'relevantOnly' // To store the JWT token returned the session launch 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' - // To store the setting of whether to send screenshots or not export const TESTOPS_SCREENSHOT_ENV = 'BS_TESTOPS_ALLOW_SCREENSHOTS' diff --git a/packages/browserstack-service/src/instrumentation/funnelInstrumentation.ts b/packages/browserstack-service/src/instrumentation/funnelInstrumentation.ts index e984c0c..740caed 100644 --- a/packages/browserstack-service/src/instrumentation/funnelInstrumentation.ts +++ b/packages/browserstack-service/src/instrumentation/funnelInstrumentation.ts @@ -9,7 +9,6 @@ import { BSTACK_SERVICE_VERSION, WDIO_NAMING_PREFIX } from '../constants.js' import { getDataFromWorkers } from '../data-store.js' import { getProductMap } from '../testHub/utils.js' import fetchWrap from '../fetchWrapper.js' -import type { BrowserstackHealing } from '@browserstack/ai-sdk-node' import type { FunnelData, EventProperties } from '../types.js' import TestOpsConfig from '../testOps/testOpsConfig.js' import APIUtils from '../cli/apiUtils.js' @@ -199,115 +198,3 @@ function getReferrer(framework?: string) { const fullName = framework ? WDIO_NAMING_PREFIX + framework : 'WebdriverIO' return `${fullName}/${BSTACK_SERVICE_VERSION}` } - -const sendEvent = { - tcgDown: (config: BrowserStackConfig) => fireFunnelTestEvent('SDKTestTcgDownResponse', config), - invalidTcgAuth: (config: BrowserStackConfig) => fireFunnelTestEvent('SDKTestInvalidTcgAuthResponseWithUserImpact', config), - tcgAuthFailure: (config: BrowserStackConfig) => fireFunnelTestEvent('SDKTestTcgAuthFailure', config), - tcgtInitSuccessful: (config: BrowserStackConfig) => fireFunnelTestEvent('SDKTestTcgtInitSuccessful', config), - initFailed: (config: BrowserStackConfig) => fireFunnelTestEvent('SDKTestInitFailedResponse', config), - tcgProxyFailure: (config: BrowserStackConfig) => fireFunnelTestEvent('SDKTestTcgProxyFailure', config), -} - -function isProxyError(authResult: { status?: number }): boolean { - return (authResult as BrowserstackHealing.InitErrorResponse)?.status === 502 -} - -function handleProxyError(config: BrowserStackConfig, isSelfHealEnabled: boolean | undefined) { - sendEvent.tcgProxyFailure(config) - if (isSelfHealEnabled) { - BStackLogger.warn('Proxy Error. Disabling Healing for this session.') - } -} - -function handleUpgradeRequired(isSelfHealEnabled: boolean | undefined) { - if (isSelfHealEnabled) { - BStackLogger.warn('Please upgrade Browserstack Service to the latest version to use the self-healing feature.') - } -} - -function handleAuthenticationFailure(status: number, config: BrowserStackConfig, isSelfHealEnabled: boolean | undefined) { - if (status >= 500) { - if (isSelfHealEnabled) { - BStackLogger.warn('Something went wrong. Disabling healing for this session. Please try again later.') - } - sendEvent.tcgDown(config) - } else { - if (isSelfHealEnabled) { - BStackLogger.warn('Authentication Failed. Disabling Healing for this session.') - } - sendEvent.tcgAuthFailure(config) - } -} - -function handleAuthenticationSuccess( - isHealingEnabledForUser: boolean, - userId: string, - config: BrowserStackConfig, - isSelfHealEnabled: boolean | undefined -) { - if (!isHealingEnabledForUser && isSelfHealEnabled) { - BStackLogger.warn('Healing is not enabled for your group, please contact the admin') - } else if (userId && isHealingEnabledForUser) { - sendEvent.tcgtInitSuccessful(config) - } -} - -function handleInitializationFailure(status: number, config: BrowserStackConfig, isSelfHealEnabled: boolean | undefined) { - if (status >= 400) { - sendEvent.initFailed(config) - } else if (!status && isSelfHealEnabled) { - sendEvent.invalidTcgAuth(config) - } - - if (isSelfHealEnabled) { - BStackLogger.warn('Authentication Failed. Healing will be disabled for this session.') - } -} - -interface AuthResult { - message: string - isAuthenticated: boolean - status: number - userId: string - groupId: string - isHealingEnabled: boolean -} - -export function handleHealingInstrumentation( - authResult: AuthResult, - config: BrowserStackConfig, - isSelfHealEnabled: boolean | undefined, -) { - try { - if (isProxyError(authResult)) { - handleProxyError(config, isSelfHealEnabled) - return - } - - const { message, isAuthenticated, status, userId, groupId, isHealingEnabled: isHealingEnabledForUser } = authResult - - if (message === 'Upgrade required') { - handleUpgradeRequired(isSelfHealEnabled) - return - } - - if (!isAuthenticated) { - handleAuthenticationFailure(status, config, isSelfHealEnabled) - return - } - - if (isAuthenticated && userId && groupId) { - handleAuthenticationSuccess(isHealingEnabledForUser, userId, config, isSelfHealEnabled) - return - } - - if (status >= 400 || !status) { - handleInitializationFailure(status, config, isSelfHealEnabled) - return - } - - } catch (err) { - BStackLogger.debug('Error in handling healing instrumentation: ' + err) - } -} diff --git a/packages/browserstack-service/src/instrumentation/performance/constants.ts b/packages/browserstack-service/src/instrumentation/performance/constants.ts index bf3e6ce..2c40689 100644 --- a/packages/browserstack-service/src/instrumentation/performance/constants.ts +++ b/packages/browserstack-service/src/instrumentation/performance/constants.ts @@ -33,8 +33,6 @@ export const EVENTS = { SDK_AUTOMATION_FRAMEWORK_STOP: 'sdk:automationFrameworkStop', SDK_ACCESSIBILITY_CONFIG: 'sdk:accessibilityConfig', SDK_OBSERVABILITY_CONFIG: 'sdk:observabilityConfig', - SDK_AI_SELF_HEAL_STEP: 'sdk:aiSelfHealStep', - SDK_AI_SELF_HEAL_GET_RESULT: 'sdk:aiSelfHealGetResult', SDK_TEST_FRAMEWORK_EVENT: 'sdk:testFrameworkEvent', SDK_TEST_SESSION_EVENT: 'sdk:testSessionEvent', SDK_CLI_LOG_CREATED_EVENT: 'sdk:cli:logCreatedEvent', @@ -151,14 +149,6 @@ export const CONFIG_EVENTS = { OBSERVABILITY: EVENTS.SDK_OBSERVABILITY_CONFIG } -/** - * AI self-healing events - */ -export const AI_EVENTS = { - SELF_HEAL_STEP: EVENTS.SDK_AI_SELF_HEAL_STEP, - SELF_HEAL_GET_RESULT: EVENTS.SDK_AI_SELF_HEAL_GET_RESULT -} - /** * Event dispatcher events for test framework and session tracking */ diff --git a/packages/browserstack-service/src/launcher.ts b/packages/browserstack-service/src/launcher.ts index 390e4e8..6038dab 100644 --- a/packages/browserstack-service/src/launcher.ts +++ b/packages/browserstack-service/src/launcher.ts @@ -41,7 +41,6 @@ import { getBrowserStackKey, uploadLogs, ObjectsAreEqual, getBasicAuthHeader, - isValidCapsForHealing, getBooleanValueFromString, validateCapsWithNonBstackA11y, mergeChromeOptions, @@ -59,7 +58,6 @@ import type Percy from './Percy/Percy.js' import BrowserStackConfig from './config.js' import { setupExitHandlers } from './exitHandler.js' import { sendFinish, sendStart } from './instrumentation/funnelInstrumentation.js' -import AiHandler from './ai-handler.js' import PerformanceTester from './instrumentation/performance/performance-tester.js' import * as PERFORMANCE_SDK_EVENTS from './instrumentation/performance/constants.js' import { BrowserstackCLI } from './cli/index.js' @@ -355,30 +353,6 @@ export default class BrowserstackLauncherService implements Services.ServiceInst PerformanceTester.end(PERFORMANCE_SDK_EVENTS.FRAMEWORK_EVENTS.INIT) - // Setting up healing for those sessions where we don't add the service version capability as it indicates that the session is not being run on BrowserStack - if (!shouldAddServiceVersion(this._config, this._options.testObservability, capabilities as Capabilities.BrowserStackCapabilities)) { - try { - if ((capabilities as Capabilities.BrowserStackCapabilities).browserName) { - capabilities = await AiHandler.setup(this._config, this.browserStackConfig, this._options, capabilities as WebdriverIO.Capabilities, false) - } else if ( Array.isArray(capabilities)){ - - for (let i = 0; i < capabilities.length; i++) { - if ((capabilities[i] as Capabilities.BrowserStackCapabilities).browserName) { - capabilities[i] = await AiHandler.setup(this._config, this.browserStackConfig, this._options, capabilities[i] as WebdriverIO.Capabilities, false) - } - } - - } else if (isValidCapsForHealing(capabilities)) { - // setting up healing in case capabilities.xyz.capabilities.browserName where xyz can be anything: - capabilities = await AiHandler.setup(this._config, this.browserStackConfig, this._options, capabilities, true) - } - } catch (err) { - if (this._options.selfHeal === true) { - BStackLogger.warn(`Error while setting up Browserstack healing Extension ${err}. Disabling healing for this session.`) - } - } - } - /** * Upload app to BrowserStack if valid file path to app is given. * Update app value of capability directly if app_url, custom_id, shareable_id is given diff --git a/packages/browserstack-service/src/service.ts b/packages/browserstack-service/src/service.ts index eef859c..861b4f1 100644 --- a/packages/browserstack-service/src/service.ts +++ b/packages/browserstack-service/src/service.ts @@ -29,7 +29,6 @@ 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' import PerformanceTester from './instrumentation/performance/performance-tester.js' import * as PERFORMANCE_SDK_EVENTS from './instrumentation/performance/constants.js' import { EVENTS } from './instrumentation/performance/constants.js' @@ -229,17 +228,6 @@ export default class BrowserstackService implements Services.ServiceInstance { this._browser = browser ? browser : globalThis.browser PerformanceTester.browser = this._browser - // Healing Support: - if (!isBrowserstackSession(this._browser)) { - try { - await AiHandler.selfHeal(this._options, caps, this._browser) - } catch (err) { - if (this._options.selfHeal === true) { - BStackLogger.warn(`Error while setting up self-healing: ${err}. Disabling healing for this session.`) - } - } - } - // Ensure capabilities are not null in case of multiremote if (this._isAppAutomate()) { diff --git a/packages/browserstack-service/src/types.ts b/packages/browserstack-service/src/types.ts index 588e5ee..1f552ea 100644 --- a/packages/browserstack-service/src/types.ts +++ b/packages/browserstack-service/src/types.ts @@ -60,9 +60,7 @@ export interface TestOrchestrationOptions { runSmartSelection?: RunSmartSelectionOptions } -export interface BrowserstackOptions extends Options.Testrunner { - selfHeal?: boolean; -} +export type BrowserstackOptions = Options.Testrunner export interface BrowserstackConfig { /** @@ -240,7 +238,6 @@ export interface BrowserstackConfig { * @default false */ ipWhiteListing?: boolean; - selfHeal?: boolean; /** * Set the Test Orchestration related config options under this key. * For e.g. runSmartSelection configurations, etc. diff --git a/packages/browserstack-service/src/util.ts b/packages/browserstack-service/src/util.ts index 8c381e9..da33988 100644 --- a/packages/browserstack-service/src/util.ts +++ b/packages/browserstack-service/src/util.ts @@ -1972,15 +1972,6 @@ export const hasBrowserName = (cap: Capabilities.WebdriverIOConfig): boolean => return browserStackCapabilities.browserName !== undefined } -export const isValidCapsForHealing = (caps: WebdriverIO.Capabilities): boolean => { - - // Get all capability values - const capValues = Object.values(caps) - - // Check if there are any capabilities and if at least one has a browser name - return capValues.length > 0 && capValues.some(hasBrowserName) -} - export function isTurboScale(options: (BrowserstackConfig & BrowserstackOptions) | undefined): boolean { return Boolean(options?.turboScale) } diff --git a/packages/browserstack-service/tests/ai-handler.test.ts b/packages/browserstack-service/tests/ai-handler.test.ts deleted file mode 100644 index 48b2140..0000000 --- a/packages/browserstack-service/tests/ai-handler.test.ts +++ /dev/null @@ -1,750 +0,0 @@ -/// -import path from 'node:path' - -import { describe, expect, it, vi, beforeEach } from 'vitest' -import aiSDK from '@browserstack/ai-sdk-node' - -import AiHandler from '../src/ai-handler.js' -import * as bstackLogger from '../src/bstackLogger.js' -import * as funnelInstrumentation from '../src/instrumentation/funnelInstrumentation.js' -import type { Capabilities } from '@wdio/types' -import { TCG_URL } from '../src/constants.js' - -// Mock only the external dependency -vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))) -vi.mock('@browserstack/ai-sdk-node') -vi.useFakeTimers().setSystemTime(new Date('2020-01-01')) -vi.mock('uuid', () => ({ v4: () => '123456789' })) -vi.mock('node:fs', () => ({ - default: { - readFileSync: vi.fn().mockReturnValue(Buffer.from('extension-content')) - } -})) - -const bstackLoggerSpy = vi.spyOn(bstackLogger.BStackLogger, 'logToFile') -bstackLoggerSpy.mockImplementation(() => {}) - -describe('AiHandler', () => { - let config: any - let browser: any - - beforeEach(() => { - vi.resetModules() - config = { - user: 'foobaruser', - key: '12345678901234567890', - selfHeal: true // Default to true - } - - browser = { - sessionId: 'test-session-id', - execute: vi.fn(), - installAddOn: vi.fn(), - overwriteCommand: vi.fn(), - capabilities: { - browserName: 'chrome' - } - } - }) - - describe('authenticateUser', () => { - it('should authenticate user', async () => { - const authResponse = { - message: 'Authentication successful', - isAuthenticated: true, - defaultLogDataEnabled: true, - isHealingEnabled: true, - sessionToken: 'test-token', - groupId: 123123, - userId: 342423, - isGroupAIEnabled: true, - } - - const initSpy = vi.spyOn(aiSDK.BrowserstackHealing, 'init') - .mockReturnValue(Promise.resolve(authResponse) as any) - - const result = await AiHandler.authenticateUser(config.user, config.key) - - expect(initSpy).toHaveBeenCalledTimes(1) - expect(initSpy).toHaveBeenCalledWith( - config.key, - config.user, - 'https://tcg.browserstack.com', - expect.any(String) - ) - expect(result).toEqual(authResponse) - }) - }) - - describe('updateCaps', () => { - it('should add the AI extension to capabilities', async () => { - const authResult = { - isAuthenticated: true, - defaultLogDataEnabled: true, - } as any - - const caps = { - 'goog:chromeOptions': {} - } - const mockExtension = 'mock-extension' - - vi.spyOn(aiSDK.BrowserstackHealing, 'initializeCapabilities') - .mockReturnValue({ ...caps, 'goog:chromeOptions': { extensions: [mockExtension] } }) - - const updatedCaps = await AiHandler.updateCaps(authResult, config, caps) as any - - expect(updatedCaps['goog:chromeOptions'].extensions).toEqual([mockExtension]) - }) - - it('should not update capabilities if authentication failed', async () => { - const authResult = { - isAuthenticated: false, - message: 'Authentication failed' - } as any - - const caps = { browserName: 'chrome' } - const updatedCaps = await AiHandler.updateCaps(authResult, config, caps) - - expect(updatedCaps).toEqual(caps) - }) - - it('should not update capabilities if selfHeal is false', async () => { - const authResult = { - isAuthenticated: false, - message: 'Authentication failed' - } as any - - config.selfHeal = false - - const caps = { browserName: 'chrome' } - const updatedCaps = await AiHandler.updateCaps(authResult, config, caps) - - expect(updatedCaps).toEqual(caps) - }) - - it('should handle array of capabilities', async () => { - const authResult = { - isAuthenticated: true, - defaultLogDataEnabled: true, - } as any - - const caps = [{ browserName: 'chrome' }] - const mockExtension = 'mock-extension' - - vi.spyOn(aiSDK.BrowserstackHealing, 'initializeCapabilities') - .mockReturnValue({ browserName: 'chrome', 'goog:chromeOptions': { extensions: [mockExtension] } }) - - const updatedCaps = await AiHandler.updateCaps(authResult, config, caps) as any - - expect(updatedCaps[0]['goog:chromeOptions'].extensions).toEqual([mockExtension]) - }) - - it('should handle mixed array and object capabilities', async () => { - const authResult = { - isAuthenticated: true, - defaultLogDataEnabled: true, - isHealingEnabled: true, - } as any - - const capsArray = [{ - browserName: 'chrome', - 'goog:chromeOptions': {} - }] - - const capsObject = { - browserName: 'firefox', - 'moz:firefoxOptions': {} - } - - const mockChromeExtension = 'mock-chrome-extension' - const mockFirefoxExtension = 'mock-firefox-extension' - - const initializeCapabilitiesSpy = vi.spyOn(aiSDK.BrowserstackHealing, 'initializeCapabilities') - .mockReturnValueOnce({ - browserName: 'chrome', - 'goog:chromeOptions': { extensions: [mockChromeExtension] } - }) - .mockReturnValueOnce({ - browserName: 'firefox', - 'moz:firefoxOptions': { extensions: [mockFirefoxExtension] } - }) - - const updatedCapsArray = await AiHandler.updateCaps(authResult, config, capsArray) as Array - const updatedCapsObject = await AiHandler.updateCaps(authResult, config, capsObject) - - expect(initializeCapabilitiesSpy).toHaveBeenCalledTimes(2) - - expect(initializeCapabilitiesSpy).toHaveBeenNthCalledWith(2, capsObject) - - expect(updatedCapsArray).toBeInstanceOf(Array) - expect(updatedCapsArray[0]).toEqual({ - browserName: 'chrome', - 'goog:chromeOptions': { extensions: [mockChromeExtension] } - }) - - expect(updatedCapsObject).toEqual({ - browserName: 'firefox', - 'moz:firefoxOptions': { extensions: [mockFirefoxExtension] } - }) - }) - - it('should update caps if selfHeal is true but defaultLogDataEnabled is false', async () => { - const authResult = { - isAuthenticated: true, - defaultLogDataEnabled: false, - } as any - - const caps = { browserName: 'chrome' } - const updatedCaps = await AiHandler.updateCaps(authResult, config, caps) - - expect(updatedCaps).not.toEqual(caps) - }) - }) - - describe('handleHealing', () => { - it('should attempt healing if findElement fails', async () => { - const originalFunc = vi.fn().mockReturnValueOnce({ error: 'no such element' }) - .mockReturnValueOnce({}) - - const healFailureResponse = { script: 'healing-script' } - const pollResultResponse = { selector: 'css selector', value: '.healed-element' } - - AiHandler['authResult'] = { - isAuthenticated: true, - isHealingEnabled: true, - sessionToken: 'test-session-token', - groupId: 123123, - userId: 342423, - isGroupAIEnabled: true - } as any - - vi.spyOn(aiSDK.BrowserstackHealing, 'healFailure') - .mockResolvedValue(healFailureResponse.script as string) - vi.spyOn(aiSDK.BrowserstackHealing, 'pollResult') - .mockResolvedValue(pollResultResponse as any) - vi.spyOn(aiSDK.BrowserstackHealing, 'logData') - .mockResolvedValue('logging-script' as string) - - const result = await AiHandler.handleHealing(originalFunc, 'id', 'some-id', browser, config) - - expect(aiSDK.BrowserstackHealing.healFailure).toHaveBeenCalledTimes(1) - expect(aiSDK.BrowserstackHealing.pollResult).toHaveBeenCalledTimes(1) - expect(originalFunc).toHaveBeenCalledTimes(2) - expect(browser.execute).toHaveBeenCalledWith('healing-script') - expect(result).toEqual({}) - }) - - it('should attempt logging if findElement successfully runs', async () => { - const originalFunc = vi.fn().mockReturnValueOnce({ element: 'mock-element' }) - .mockReturnValueOnce({}) - - AiHandler['authResult'] = { - isAuthenticated: true, - isHealingEnabled: true, - sessionToken: 'test-session-token', - groupId: 123123, - userId: 342423, - isGroupAIEnabled: true - } as any - - vi.spyOn(aiSDK.BrowserstackHealing, 'logData') - .mockResolvedValue('logging-script' as any) - - const result = await AiHandler.handleHealing(originalFunc, 'id', 'some-id', browser, config) - - expect(originalFunc).toHaveBeenCalledTimes(1) - expect(browser.execute).toHaveBeenCalledWith('logging-script') - expect(result).toEqual({ 'element': 'mock-element' }) - }) - - it('should call originalFunc if there is an error in try block', async () => { - const originalFunc = vi.fn().mockImplementationOnce(() => { - throw new Error('Some error occurred.') - }) - - AiHandler['authResult'] = { - isAuthenticated: true, - isHealingEnabled: true, - sessionToken: 'test-session-token', - groupId: 123123, - userId: 342423, - isGroupAIEnabled: true - } as any - - const result = await AiHandler.handleHealing(originalFunc, 'id', 'some-id', browser, config) - - expect(originalFunc).toHaveBeenCalledTimes(2) - expect(result).toEqual(undefined) - }) - - it('should return original error if selfHeal is false', async () => { - const originalFunc = vi.fn().mockImplementationOnce(() => { - throw new Error('Some error occurred.') - }) - - config.selfHeal = false - - const warnSpy = vi.spyOn(bstackLogger.BStackLogger, 'warn') - - const result = await AiHandler.handleHealing(originalFunc, 'id', 'some-id', browser, config) - - expect(originalFunc).toHaveBeenCalledTimes(2) - expect(warnSpy).toHaveBeenCalledTimes(1) - expect(result).toEqual(undefined) - }) - - it('should return original result error if healed element is also missing', async () => { - - const originalFunc = vi.fn().mockReturnValueOnce({ error: 'no such element' }) - .mockReturnValueOnce({ error: 'no such element' }) - - const healFailureResponse = { script: 'healing-script' } - const pollResultResponse = { selector: 'css selector', value: '.healed-element' } - - AiHandler['authResult'] = { - isAuthenticated: true, - isHealingEnabled: true, - sessionToken: 'test-session-token', - groupId: 123123, - userId: 342423, - isGroupAIEnabled: true - } as any - - vi.spyOn(aiSDK.BrowserstackHealing, 'healFailure') - .mockResolvedValue(healFailureResponse.script as string) - vi.spyOn(aiSDK.BrowserstackHealing, 'pollResult') - .mockResolvedValue(pollResultResponse as any) - vi.spyOn(aiSDK.BrowserstackHealing, 'logData') - .mockResolvedValue('logging-script' as string) - - const result = await AiHandler.handleHealing(originalFunc, 'id', 'some-id', browser, config) - - expect(aiSDK.BrowserstackHealing.healFailure).toHaveBeenCalledTimes(1) - expect(aiSDK.BrowserstackHealing.pollResult).toHaveBeenCalledTimes(1) - expect(originalFunc).toHaveBeenCalledTimes(2) - expect(browser.execute).toHaveBeenCalledWith('healing-script') - expect(result).toEqual({ error: 'no such element' }) - }) - }) - - describe('setup', () => { - it('should authenticate user and update capabilities for supported browser', async () => { - const caps = { browserName: 'chrome' } - const mockAuthResult = { - isAuthenticated: true, - sessionToken: 'mock-session-token', - defaultLogDataEnabled: true, - isHealingEnabled: true, - groupId: 123123, - userId: 342423, - isGroupAIEnabled: true, - } - - const authenticateUserSpy = vi.spyOn(AiHandler, 'authenticateUser') - .mockResolvedValue(mockAuthResult as any) - const handleHealingInstrumentationSpy = vi.spyOn(funnelInstrumentation, 'handleHealingInstrumentation') - const updateCapsSpy = vi.spyOn(AiHandler, 'updateCaps') - .mockResolvedValue({ ...caps, 'goog:chromeOptions': { extensions: ['mock-extension'] } }) - - const mockExtension = 'mock-extension' - vi.spyOn(aiSDK.BrowserstackHealing, 'initializeCapabilities') - .mockReturnValue({ ...caps, 'goog:chromeOptions': { extensions: [mockExtension] } }) - - const emptyObj = {} as any - await AiHandler.setup(config, emptyObj, emptyObj, caps, false) - - expect(authenticateUserSpy).toHaveBeenCalledTimes(1) - expect(handleHealingInstrumentationSpy).toHaveBeenCalledTimes(1) - expect(updateCapsSpy).toHaveBeenCalledTimes(1) - }) - - it('should skip setup if accessKey is not present', async () => { - config.key = '' - const caps = { browserName: 'chrome' } - - const authenticateUserSpy = vi.spyOn(AiHandler, 'authenticateUser') - const handleHealingInstrumentationSpy = vi.spyOn(funnelInstrumentation, 'handleHealingInstrumentation') - const updateCapsSpy = vi.spyOn(AiHandler, 'updateCaps') - - const emptyObj = {} as any - vi.stubEnv('BROWSERSTACK_ACCESS_KEY', '') - const updatedCaps = await AiHandler.setup(config, emptyObj, emptyObj, caps, false) - expect(authenticateUserSpy).not.toHaveBeenCalled() - expect(handleHealingInstrumentationSpy).not.toHaveBeenCalled() - expect(updateCapsSpy).not.toHaveBeenCalled() - expect(updatedCaps).toEqual(caps) // Expect caps to remain unchanged - }) - - it('should skip setup if userName is not present', async () => { - config.user = '' - const caps = { browserName: 'chrome' } - - const authenticateUserSpy = vi.spyOn(AiHandler, 'authenticateUser') - const handleHealingInstrumentationSpy = vi.spyOn(funnelInstrumentation, 'handleHealingInstrumentation') - const updateCapsSpy = vi.spyOn(AiHandler, 'updateCaps') - - const emptyObj = {} as any - vi.stubEnv('BROWSERSTACK_USERNAME', '') - const updatedCaps = await AiHandler.setup(config, emptyObj, emptyObj, caps, false) - expect(authenticateUserSpy).not.toHaveBeenCalled() - expect(handleHealingInstrumentationSpy).not.toHaveBeenCalled() - expect(updateCapsSpy).not.toHaveBeenCalled() - expect(updatedCaps).toEqual(caps) // Expect caps to remain unchanged - }) - - it('should handle errors in setup', async () => { - const caps = { browserName: 'chrome' } - const authenticateUserSpy = vi.spyOn(AiHandler, 'authenticateUser') - .mockRejectedValue(new Error('Authentication failed')) - - const warnSpy = vi.spyOn(bstackLogger.BStackLogger, 'warn') - - const emptyObj = {} as any - const options = { selfHeal: true } as any - const updatedCaps = await AiHandler.setup(config, emptyObj, options, caps, false) - - expect(authenticateUserSpy).toHaveBeenCalledTimes(1) - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Error while initiliazing Browserstack healing Extension')) - expect(updatedCaps).toEqual(caps) - }) - }) - - describe('selfHeal', () => { - it('should set token, install extension for Firefox', async () => { - const caps = { browserName: 'firefox' } as Capabilities.ResolvedTestrunnerCapabilities - AiHandler['authResult'] = { - isAuthenticated: true, - sessionToken: 'mock-session-token', - defaultLogDataEnabled: true, - isHealingEnabled: true - } as any - - const setTokenSpy = vi.spyOn(AiHandler, 'setToken') - const installFirefoxExtensionSpy = vi.spyOn(AiHandler, 'installFirefoxExtension') - - browser.capabilities = caps - await AiHandler.selfHeal(config, caps, browser) - - expect(setTokenSpy).toHaveBeenCalledTimes(1) - expect(setTokenSpy).toHaveBeenCalledWith(browser.sessionId, 'mock-session-token') - expect(installFirefoxExtensionSpy).toHaveBeenCalledTimes(1) - expect(installFirefoxExtensionSpy).toHaveBeenCalledWith(browser) - }) - - it('should set token for Chrome', async () => { - const caps = { browserName: 'chrome' } as Capabilities.ResolvedTestrunnerCapabilities - AiHandler['authResult'] = { - isAuthenticated: true, - sessionToken: 'mock-session-token', - defaultLogDataEnabled: true, - isHealingEnabled: true - } as any - - const setTokenSpy = vi.spyOn(AiHandler, 'setToken') - - browser.capabilities = caps - await AiHandler.selfHeal(config, caps, browser) - - expect(setTokenSpy).toHaveBeenCalledTimes(1) - expect(setTokenSpy).toHaveBeenCalledWith(browser.sessionId, 'mock-session-token') - }) - - it('should skip self-healing if authResult is empty', async () => { - const caps = { browserName: 'chrome' } as Capabilities.ResolvedTestrunnerCapabilities - AiHandler['authResult'] = {} as any - - const setTokenSpy = vi.spyOn(AiHandler, 'setToken') - - await AiHandler.selfHeal(config, caps, browser) - - expect(setTokenSpy).not.toHaveBeenCalled() - }) - - it('should call overwriteCommand for Chrome', async () => { - const caps = { browserName: 'chrome' } as Capabilities.ResolvedTestrunnerCapabilities - AiHandler['authResult'] = { - isAuthenticated: true, - sessionToken: 'mock-session-token', - defaultLogDataEnabled: true, - isHealingEnabled: true - } as any - - const setTokenSpy = vi.spyOn(AiHandler, 'setToken') - const overwriteCommandSpy = vi.spyOn(browser, 'overwriteCommand') - - vi.spyOn(aiSDK.BrowserstackHealing, 'logData') - .mockResolvedValue('logging-script') - - browser.capabilities = caps - await AiHandler.selfHeal(config, caps, browser) - - expect(setTokenSpy).toHaveBeenCalledTimes(1) - expect(setTokenSpy).toHaveBeenCalledWith(browser.sessionId, 'mock-session-token') - expect(overwriteCommandSpy).toHaveBeenCalledTimes(1) - expect(overwriteCommandSpy).toHaveBeenCalledWith('findElement', expect.any(Function)) - }) - - it('should call overwriteCommand for Edge', async () => { - const caps = { browserName: 'microsoftedge' } as Capabilities.ResolvedTestrunnerCapabilities - AiHandler['authResult'] = { - isAuthenticated: true, - sessionToken: 'mock-session-token', - defaultLogDataEnabled: true, - isHealingEnabled: true - } as any - - const setTokenSpy = vi.spyOn(AiHandler, 'setToken') - const overwriteCommandSpy = vi.spyOn(browser, 'overwriteCommand') - - vi.spyOn(aiSDK.BrowserstackHealing, 'logData') - .mockResolvedValue('logging-script') - - browser.capabilities = caps - await AiHandler.selfHeal(config, caps, browser) - - expect(setTokenSpy).toHaveBeenCalledTimes(1) - expect(setTokenSpy).toHaveBeenCalledWith(browser.sessionId, 'mock-session-token') - expect(overwriteCommandSpy).toHaveBeenCalledTimes(1) - expect(overwriteCommandSpy).toHaveBeenCalledWith('findElement', expect.any(Function)) - }) - - it('should skip selfHeal for unsupported browser', async () => { - const caps = { browserName: 'safari' } as Capabilities.ResolvedTestrunnerCapabilities - - const setTokenSpy = vi.spyOn(AiHandler, 'setToken') - const installFirefoxExtensionSpy = vi.spyOn(AiHandler, 'installFirefoxExtension') - const overwriteCommandSpy = vi.spyOn(browser, 'overwriteCommand') - - browser.capabilities = caps - await AiHandler.selfHeal(config, caps, browser) - - expect(setTokenSpy).not.toHaveBeenCalled() - expect(installFirefoxExtensionSpy).not.toHaveBeenCalled() - expect(overwriteCommandSpy).not.toHaveBeenCalled() - }) - - it('should handle error in selfHeal function', async () => { - const caps = { browserName: 'chrome' } as Capabilities.ResolvedTestrunnerCapabilities - AiHandler['authResult'] = { - isAuthenticated: true, - sessionToken: 'mock-session-token', - defaultLogDataEnabled: true, - isHealingEnabled: true - } as any - - const setTokenSpy = vi.spyOn(AiHandler, 'setToken').mockImplementationOnce(() => { - throw new Error('Some error occurred in setToken') - }) - - const warnSpy = vi.spyOn(bstackLogger.BStackLogger, 'warn') - - config.selfHeal = true - await AiHandler.selfHeal(config, caps, browser) - - expect(setTokenSpy).toHaveBeenCalledTimes(1) - expect(warnSpy).toHaveBeenCalledWith('Error while setting up self-healing: Error: Some error occurred in setToken. Disabling healing for this session.') - }) - - it('should not set token if isAuthenticated is false', async () => { - const caps = { browserName: 'chrome' } as Capabilities.ResolvedTestrunnerCapabilities - AiHandler['authResult'] = { - isAuthenticated: false, - sessionToken: 'mock-session-token', - } as any - - const setTokenSpy = vi.spyOn(AiHandler, 'setToken') - - browser.capabilities = caps - await AiHandler.selfHeal(config, caps, browser) - - expect(setTokenSpy).not.toHaveBeenCalled() - }) - - it('should not overwrite findElement command if defaultLogDataEnabled and selfHeal are false', async () => { - const caps = { browserName: 'chrome' } as Capabilities.ResolvedTestrunnerCapabilities - config.selfHeal = false - AiHandler['authResult'] = { - isAuthenticated: true, - sessionToken: 'mock-session-token', - defaultLogDataEnabled: false, - } as any - - const overwriteCommandSpy = vi.spyOn(browser, 'overwriteCommand') - - browser.capabilities = caps - await AiHandler.selfHeal(config, caps, browser) - - expect(overwriteCommandSpy).not.toHaveBeenCalled() - }) - }) - - describe('setToken', () => { - it('should call setToken with correct parameters', async () => { - const setTokenSpy = vi.spyOn(aiSDK.BrowserstackHealing, 'setToken') - .mockResolvedValue(undefined) - - await AiHandler.setToken('test-session-id', 'test-token') - - expect(setTokenSpy).toHaveBeenCalledWith('test-session-id', 'test-token', TCG_URL) - }) - }) - - describe('handle multi remote browser session', () => { - let config: any - let browserMock: any - let caps: any - const mockAuthResult = { - isAuthenticated: true, - sessionToken: 'mock-session-token', - defaultLogDataEnabled: true, - isHealingEnabled: true, - groupId: 123123, - userId: 342423, - isGroupAIEnabled: true, - } - beforeEach(() => { - config = { - capabilities: { - myChromeBrowser: { - hostname: 'localhost', - port: 4444, - protocol: 'http', - capabilities: { - browserName: 'chrome', - 'goog:chromeOptions': {} - } - }, - myFirefoxBrowser: { - hostname: 'localhost', - port: 4444, - protocol: 'http', - capabilities: { - browserName: 'firefox', - 'moz:firefoxOptions': {} - } - } - }, - maxInstances: 15, - user: 'foobaruser', - key: '12345678901234567890', - selfHeal: true - } - - caps = { - myChromeBrowser: { - hostname: 'localhost', - port: 4444, - protocol: 'http', - capabilities: { - browserName: 'chrome', - 'goog:chromeOptions': {} - } - }, - myFirefoxBrowser: { - hostname: 'localhost', - port: 4444, - protocol: 'http', - capabilities: { - browserName: 'firefox', - 'moz:firefoxOptions': {} - } - } - } - - browserMock = { - myChromeBrowser: { - sessionId: 'chrome-session-id', - capabilities: { browserName: 'chrome' }, - overwriteCommand: vi.fn(), - }, - myFirefoxBrowser: { - sessionId: 'firefox-session-id', - capabilities: { browserName: 'firefox' }, - overwriteCommand: vi.fn(), - installAddOn: vi.fn() - } - } as any - }) - - it('should setup capabilities for multiremote session not running on BrowserStack', async () => { - const mockExtension = 'mock-extension' - - const handleHealingInstrumentationSpy = vi.spyOn(funnelInstrumentation, 'handleHealingInstrumentation') - const updateCapsSpy = vi.spyOn(AiHandler, 'updateCaps') - .mockResolvedValue({ 'goog:chromeOptions': { extensions: [mockExtension] } }) - - vi.spyOn(aiSDK.BrowserstackHealing, 'initializeCapabilities') - .mockReturnValue({ 'goog:chromeOptions': { extensions: [mockExtension] } }) - - const emptyObj = {} as any - AiHandler.handleMultiRemoteSetup(mockAuthResult, config, emptyObj, emptyObj, config.capabilities) as any - expect(handleHealingInstrumentationSpy).toHaveBeenCalledTimes(2) - expect(updateCapsSpy).toHaveBeenCalledTimes(2) - }) - - it('should call handleSelfHeal for each browser in multiremote setup', async () => { - AiHandler['authResult'] = { - isAuthenticated: true, - sessionToken: 'mock-session-token', - defaultLogDataEnabled: true, - isHealingEnabled: true - } as any - - const setTokenSpy = vi.spyOn(AiHandler, 'setToken') - const installFirefoxExtensionSpy = vi.spyOn(AiHandler, 'installFirefoxExtension') - const handleSelfHealSpy = vi.spyOn(AiHandler, 'handleSelfHeal') - - await AiHandler.selfHeal(config, caps, browserMock) - - expect(handleSelfHealSpy).toHaveBeenCalledTimes(2) - expect(handleSelfHealSpy).toHaveBeenCalledWith(config, browserMock.myChromeBrowser) - expect(handleSelfHealSpy).toHaveBeenCalledWith(config, browserMock.myFirefoxBrowser) - expect(setTokenSpy).toHaveBeenCalledWith('chrome-session-id', 'mock-session-token') - expect(setTokenSpy).toHaveBeenCalledWith('firefox-session-id', 'mock-session-token') - expect(installFirefoxExtensionSpy).toHaveBeenCalledTimes(1) - expect(installFirefoxExtensionSpy).toHaveBeenCalledWith(browserMock.myFirefoxBrowser) - }) - - it('should skip setup for multiremote session if accessKey is not present', async () => { - config.key = '' - const authenticateUserSpy = vi.spyOn(AiHandler, 'authenticateUser') - .mockResolvedValue(mockAuthResult as any) - const handleHealingInstrumentationSpy = vi.spyOn(funnelInstrumentation, 'handleHealingInstrumentation') - const updateCapsSpy = vi.spyOn(AiHandler, 'updateCaps') - - const emptyObj = {} as any - vi.stubEnv('BROWSERSTACK_ACCESS_KEY', '') - const authResult = { - isAuthenticated: true, - sessionToken: 'mock-session-token', - defaultLogDataEnabled: true, - isHealingEnabled: true - } as any - AiHandler.handleMultiRemoteSetup(authResult, config, emptyObj, emptyObj, config.capabilities) as any - expect(authenticateUserSpy).not.toHaveBeenCalled() - expect(handleHealingInstrumentationSpy).not.toHaveBeenCalled() - expect(updateCapsSpy).not.toHaveBeenCalled() - }) - - it('should handle multiremote setup', async () => { - const caps = { - browserA: { - capabilities: { browserName: 'chrome' } - }, - browserB: { - capabilities: { browserName: 'firefox' } - } - } as any - - const handleMultiRemoteSetupSpy = vi.spyOn(AiHandler, 'handleMultiRemoteSetup') - .mockResolvedValue(caps) - - const emptyObj = {} as any - const updatedCaps = await AiHandler.setup(config, emptyObj, emptyObj, caps, true) - - expect(handleMultiRemoteSetupSpy).toHaveBeenCalledTimes(1) - expect(updatedCaps).toEqual(caps) - - }) - }) -}) diff --git a/packages/browserstack-service/tests/cli/frameworks/index.test.ts b/packages/browserstack-service/tests/cli/frameworks/index.test.ts index 6d223ad..fa8e4f9 100644 --- a/packages/browserstack-service/tests/cli/frameworks/index.test.ts +++ b/packages/browserstack-service/tests/cli/frameworks/index.test.ts @@ -141,8 +141,7 @@ describe('BrowserstackCLI', () => { // Mock wdio config for testing const mockWdioConfig = { - capabilities: [], - selfHeal: false + capabilities: [] } beforeEach(() => { diff --git a/packages/browserstack-service/tests/instrumentation/funnelInstrumentation.test.ts b/packages/browserstack-service/tests/instrumentation/funnelInstrumentation.test.ts index 150830e..5e143d8 100644 --- a/packages/browserstack-service/tests/instrumentation/funnelInstrumentation.test.ts +++ b/packages/browserstack-service/tests/instrumentation/funnelInstrumentation.test.ts @@ -4,7 +4,6 @@ import { BStackLogger } from '../../src/bstackLogger.js' import fs from 'node:fs' import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest' import { FUNNEL_INSTRUMENTATION_URL } from '../../src/constants.js' -import type { BrowserstackHealing } from '@browserstack/ai-sdk-node' vi.mock('fetch') const mockedFetch = vi.mocked(fetch) @@ -22,33 +21,6 @@ const config = { appAutomate: false, } -const expectedEventData = { - userName: '[REDACTED]', - accessKey: '[REDACTED]', - event_type: 'SDKTestAttempted', - detectedFramework: 'WebdriverIO-framework', - event_properties: { - language_framework: 'WebdriverIO_framework', - referrer: expect.stringContaining('WebdriverIO-'), - language: 'WebdriverIO', - languageVersion: process.version, - buildName: config.buildName, - buildIdentifier: config.buildIdentifier, - os: expect.any(String), - hostname: expect.any(String), - productMap: { - 'observability': true, - 'accessibility': true, - 'percy': true, - 'automate': true, - 'app_automate': false - }, - product: expect.arrayContaining(['observability', 'automate', 'percy', 'accessibility']), - framework: 'framework', - isCLIEnabled: false - } -} - describe('funnelInstrumentation', () => { let originalCwd: { (): string; (): string } @@ -152,175 +124,4 @@ describe('funnelInstrumentation', () => { body: JSON.stringify(data) })) }) - - // NOT WORKING CODE: - - describe('Healing instrumentation', () => { - - it('should not send instrumentation event in case user receives an upgrade required warning', async () => { - const authResult = { message: 'Upgrade required' } as BrowserstackHealing.InitErrorResponse - FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true) - expect(fetch).not.toHaveBeenCalled() - }) - - it('should send server error event when isAuthenticated is false and status is 5xx', async () => { - const authResult = { isAuthenticated: false, status: 500 } as BrowserstackHealing.InitErrorResponse - - FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true) - - expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({ - method: 'POST', - headers: { - 'Authorization': `Basic ${Buffer.from(`${config.userName}:${config.accessKey}`).toString('base64')}`, - 'content-type': 'application/json', - }, - - body: expect.stringContaining('SDKTestTcgDownResponse') - })) - const [[, { body }]] = (fetch as jest.Mock).mock.calls - const parsedBody = JSON.parse(body) - - expectedEventData.event_type = 'SDKTestTcgDownResponse' - expect(parsedBody).toEqual(expect.objectContaining(expectedEventData)) - }) - - it('should send initialization success event when userId is present', async () => { - const authResult = { - isAuthenticated: true, - userId: 123456, - groupId: 234567, - sessionToken: 'session-token', - isGroupAIEnabled: true, - defaultLogDataEnabled: true, - isHealingEnabled: true - } as BrowserstackHealing.InitSuccessResponse - - FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true) - - expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({ - method: 'POST', - headers: { - 'Authorization': `Basic ${Buffer.from(`${config.userName}:${config.accessKey}`).toString('base64')}`, - 'content-type': 'application/json', - }, - - body: expect.stringContaining('SDKTestTcgtInitSuccessful') - })) - const [[, { body }]] = (fetch as jest.Mock).mock.calls - const parsedBody = JSON.parse(body) - - expectedEventData.event_type = 'SDKTestTcgtInitSuccessful' - expect(parsedBody).toEqual(expect.objectContaining(expectedEventData)) - }) - - it('should send initialization failed event if status is 4xx', async () => { - const authResult = { - isAuthenticated: true, - status: 400, - message: 'Request failed with status code 400' - } as any - - FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true) - - expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({ - method: 'POST', - headers: { - 'Authorization': `Basic ${Buffer.from(`${config.userName}:${config.accessKey}`).toString('base64')}`, - 'content-type': 'application/json', - }, - - body: expect.stringContaining('SDKTestInitFailedResponse') - })) - const [[, { body }]] = (fetch as jest.Mock).mock.calls - const parsedBody = JSON.parse(body) - - expectedEventData.event_type = 'SDKTestInitFailedResponse' - expect(parsedBody).toEqual(expect.objectContaining(expectedEventData)) - }) - - it('should send server error event when isAuthenticated is false and status is 5xx', async () => { - const authResult = { isAuthenticated: true } as any - - FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true) - - expect(fetch).toHaveBeenCalledWith(FUNNEL_INSTRUMENTATION_URL, expect.objectContaining({ - method: 'POST', - headers: { - 'Authorization': `Basic ${Buffer.from(`${config.userName}:${config.accessKey}`).toString('base64')}`, - 'content-type': 'application/json', - }, - body: expect.stringContaining('SDKTestInvalidTcgAuthResponseWithUserImpact') - })) - const [[, { body }]] = (fetch as jest.Mock).mock.calls - const parsedBody = JSON.parse(body) - - expectedEventData.event_type = 'SDKTestInvalidTcgAuthResponseWithUserImpact' - expect(parsedBody).toEqual(expect.objectContaining(expectedEventData)) - }) - - it('should not send user impact event if selfHeal is not enabled by user', async () => { - const authResult = { - isAuthenticated: true - } as any - - FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, false) - - expect(fetch).not.toHaveBeenCalled() - }) - - it('should handle exceptions during healing instrumentation', async () => { - // Create an authResult that throws an error when 'userId' is accessed - const authResult = { - isAuthenticated: true, - get userId() { - throw new Error('Property access error') - } - } as any - - const debugSpy = vi.spyOn(BStackLogger, 'debug') - - FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, true) - - expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('Error in handling healing instrumentation:')) - expect(fetch).not.toHaveBeenCalled() // Ensure no event is sent - }) - - it('should handle healing not enabled for user', async () => { - const authResult = { - isAuthenticated: true, - userId: 123456, - groupId: 234567, - sessionToken: 'session-token', - isGroupAIEnabled: true, - defaultLogDataEnabled: true, - isHealingEnabled: false - } as BrowserstackHealing.InitSuccessResponse - - FunnelTestEvent.handleHealingInstrumentation(authResult, config as any, false) - - expect(fetch).not.toHaveBeenCalled() - }) - - it('should handle healing disabled for the group', async () => { - const authResult = { - isAuthenticated: true, - userId: 123456, - groupId: 234567, - sessionToken: 'session-token', - isGroupAIEnabled: true, - defaultLogDataEnabled: true, - isHealingEnabled: false - } as BrowserstackHealing.InitSuccessResponse - - const bstackConfig = { - ...config, - selfHeal: true - } - - FunnelTestEvent.handleHealingInstrumentation(authResult, bstackConfig as any, true) - - expect(fetch).not.toHaveBeenCalled() - expect(BStackLogger.warn).toHaveBeenCalledWith('Healing is not enabled for your group, please contact the admin') - }) - }) })