diff --git a/bin/accessibility-automation/cypress/index.js b/bin/accessibility-automation/cypress/index.js index 6bc8aa3b..0d863fa5 100644 --- a/bin/accessibility-automation/cypress/index.js +++ b/bin/accessibility-automation/cypress/index.js @@ -5,6 +5,10 @@ const browserStackLog = (message) => { cy.task('browserstack_log', message); } +const sendToReporter = (data) => { + cy.task('test_accessibility_data', data); +} + const commandsToWrap = ['visit', 'click', 'type', 'request', 'dblclick', 'rightclick', 'clear', 'check', 'uncheck', 'select', 'trigger', 'selectFile', 'scrollIntoView', 'scroll', 'scrollTo', 'blur', 'focus', 'go', 'reload', 'submit', 'viewport', 'origin']; const performScan = (win, payloadToSend) => @@ -250,8 +254,11 @@ const shouldScanForAccessibility = (attributes) => { if (Cypress.env("EXCLUDE_TAGS_FOR_ACCESSIBILITY")) { excludeTagArray = Cypress.env("EXCLUDE_TAGS_FOR_ACCESSIBILITY").split(";") } + browserStackLog("EXCLUDE_TAGS_FOR_ACCESSIBILITY = " + excludeTagArray) + browserStackLog("INCLUDE_TAGS_FOR_ACCESSIBILITY = " + includeTagArray) const fullTestName = attributes.title; + browserStackLog("fullTestName = " + fullTestName) const excluded = excludeTagArray.some((exclude) => fullTestName.includes(exclude)); const included = includeTagArray.length === 0 || includeTags.some((include) => fullTestName.includes(include)); shouldScanTestForAccessibility = !excluded && included; @@ -274,6 +281,7 @@ Cypress.on('command:start', async (command) => { const attributes = Cypress.mocha.getRunner().suite.ctx.currentTest || Cypress.mocha.getRunner().suite.ctx._runnable; let shouldScanTestForAccessibility = shouldScanForAccessibility(attributes); + sendToReporter({[attributes.title]: shouldScanTestForAccessibility}) if (!shouldScanTestForAccessibility) return; cy.window().then((win) => { @@ -284,47 +292,48 @@ Cypress.on('command:start', async (command) => { afterEach(() => { const attributes = Cypress.mocha.getRunner().suite.ctx.currentTest; - cy.window().then(async (win) => { - let shouldScanTestForAccessibility = shouldScanForAccessibility(attributes); - if (!shouldScanTestForAccessibility) return cy.wrap({}); - - cy.wrap(performScan(win), {timeout: 30000}).then(() => { - try { - let os_data; - if (Cypress.env("OS")) { - os_data = Cypress.env("OS"); - } else { - os_data = Cypress.platform === 'linux' ? 'mac' : "win" - } - let filePath = ''; - if (attributes.invocationDetails !== undefined && attributes.invocationDetails.relativeFile !== undefined) { - filePath = attributes.invocationDetails.relativeFile; - } - const payloadToSend = { - "saveResults": shouldScanTestForAccessibility, - "testDetails": { - "name": attributes.title, - "testRunId": '5058', // variable not consumed, shouldn't matter what we send - "filePath": filePath, - "scopeList": [ - filePath, - attributes.title - ] - }, - "platform": { - "os_name": os_data, - "os_version": Cypress.env("OS_VERSION"), - "browser_name": Cypress.browser.name, - "browser_version": Cypress.browser.version - } - }; - browserStackLog(`Saving accessibility test results`); - cy.wrap(saveTestResults(win, payloadToSend), {timeout: 30000}).then(() => { - browserStackLog(`Saved accessibility test results`); - }) + cy.task('readFileMaybe', 'testDetails.json').then(data => { + browserStackLog('FILE CONTENT ::::: ' + data) + if (data === null) return; - } catch (er) { - } + let testDetails = {} + try { + testDetails = JSON.parse(data); + } catch (err) { + browserStackLog('Error while parsing json for testDetails:' + err) + } + const testId = testDetails[attributes.fullTitle()] + browserStackLog('TestId : ' + testId) + cy.window().then(async (win) => { + let shouldScanTestForAccessibility = shouldScanForAccessibility(attributes); + sendToReporter({[attributes.title]: shouldScanTestForAccessibility}) + if (!shouldScanTestForAccessibility) return cy.wrap({}); + + cy.wrap(performScan(win), {timeout: 30000}).then(() => { + try { + let os_data; + if (Cypress.env("OS")) { + os_data = Cypress.env("OS"); + } else { + os_data = Cypress.platform === 'linux' ? 'mac' : "win" + } + let filePath = ''; + if (attributes.invocationDetails !== undefined && attributes.invocationDetails.relativeFile !== undefined) { + filePath = attributes.invocationDetails.relativeFile; + } + const payloadToSend = { + 'thTestRunUuid': testId, + 'thBuildUuid': Cypress.env("BROWSERSTACK_TESTHUB_UUID"), + 'thJwtToken': Cypress.env('BROWSERSTACK_TESTHUB_JWT') + }; + browserStackLog(`Saving accessibility test results`); + cy.wrap(saveTestResults(win, payloadToSend), {timeout: 30000}).then(() => { + browserStackLog(`Saved accessibility test results`); + }) + + } catch (er) { + } + }) }) }); }) diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js index dd47f208..60f75219 100644 --- a/bin/accessibility-automation/plugin/index.js +++ b/bin/accessibility-automation/plugin/index.js @@ -1,7 +1,13 @@ const path = require("node:path"); +const fs = require('node:fs'); +const ipc = require('node-ipc'); +const { connectIPCClient } = require('../../testObservability/plugin/ipcClient'); +const { IPC_EVENTS } = require('../../testObservability/helper/constants'); const browserstackAccessibility = (on, config) => { + connectIPCClient(config); + let browser_validation = true; if (process.env.BROWSERSTACK_ACCESSIBILITY_DEBUG === 'true') { config.env.BROWSERSTACK_LOGS = 'true'; @@ -13,6 +19,19 @@ const browserstackAccessibility = (on, config) => { return null }, + + test_accessibility_data(data) { + ipc.of.browserstackTestObservability.emit(IPC_EVENTS.ACCESSIBILITY_DATA, data); + return null; + }, + + readFileMaybe(filename) { + if (fs.existsSync(filename)) { + return fs.readFileSync(filename, 'utf8') + } + + return null + } }) on('before:browser:launch', (browser = {}, launchOptions) => { try { @@ -41,6 +60,8 @@ const browserstackAccessibility = (on, config) => { config.env.ACCESSIBILITY_EXTENSION_PATH = process.env.ACCESSIBILITY_EXTENSION_PATH config.env.OS_VERSION = process.env.OS_VERSION config.env.OS = process.env.OS + config.env.BROWSERSTACK_TESTHUB_UUID = process.env.BROWSERSTACK_TESTHUB_UUID + config.env.BROWSERSTACK_TESTHUB_JWT = process.env.BROWSERSTACK_TESTHUB_JWT config.env.IS_ACCESSIBILITY_EXTENSION_LOADED = browser_validation.toString() diff --git a/bin/commands/runs.js b/bin/commands/runs.js index d2c4aa5c..f2024830 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -39,6 +39,8 @@ const { supportFileCleanup } = require('../accessibility-automation/helper'); const { isTurboScaleSession, getTurboScaleGridDetails, patchCypressConfigFileContent, atsFileCleanup } = require('../helpers/atsHelper'); +const TestHubHandler = require('../testhub/testhubHandler'); +const { shouldProcessEventForTesthub, checkAndSetAccessibility } = require('../testhub/utils'); module.exports = function run(args, rawArgs) { @@ -75,6 +77,7 @@ module.exports = function run(args, rawArgs) { const turboScaleSession = isTurboScaleSession(bsConfig); Constants.turboScaleObj.enabled = turboScaleSession; + checkAndSetAccessibility(bsConfig); utils.setUsageReportingFlag(bsConfig, args.disableUsageReporting); utils.setDefaults(bsConfig, args); @@ -114,10 +117,11 @@ module.exports = function run(args, rawArgs) { utils.setBuildTags(bsConfig, args); /* - Send build start to Observability + Send build start to testHub */ - if(isTestObservabilitySession) { - await launchTestSession(bsConfig, bsConfigPath); + if(shouldProcessEventForTesthub()) { + await TestHubHandler.launchBuild(bsConfig, bsConfigPath); + // await launchTestSession(bsConfig, bsConfigPath); utils.setO11yProcessHooks(null, bsConfig, args, null, buildReportData); } @@ -149,9 +153,6 @@ module.exports = function run(args, rawArgs) { // add cypress dependency if missing utils.setCypressNpmDependency(bsConfig); - if (isAccessibilitySession && isBrowserstackInfra) { - await createAccessibilityTestRun(bsConfig); - } if (turboScaleSession) { // Local is only required in case user is running on trial grid and wants to access private website. diff --git a/bin/helpers/helper.js b/bin/helpers/helper.js index 8a985829..5299dd05 100644 --- a/bin/helpers/helper.js +++ b/bin/helpers/helper.js @@ -181,6 +181,17 @@ exports.getGitMetaData = () => { } }) } + +exports.getHostInfo = () => { + return { + hostname: os.hostname(), + platform: os.platform(), + type: os.type(), + version: os.version(), + arch: os.arch() + } +} + exports.getCiInfo = () => { var env = process.env; // Jenkins @@ -318,7 +329,7 @@ exports.setBrowserstackCypressCliDependency = (bsConfig) => { typeof runSettings.npm_dependencies === 'object') { if (!("browserstack-cypress-cli" in runSettings.npm_dependencies)) { logger.warn("Missing browserstack-cypress-cli not found in npm_dependencies"); - runSettings.npm_dependencies['browserstack-cypress-cli'] = this.getAgentVersion() || "latest"; + runSettings.npm_dependencies['browserstack-cypress-cli'] = "/Users/saurav/Home/Browserstack/Automate/cypress_cli_main/browserstack-cypress-cli/browserstack-cypress-cli-1.29.1.tgz"; logger.warn(`Adding browserstack-cypress-cli version ${runSettings.npm_dependencies['browserstack-cypress-cli']} in npm_dependencies`); } } diff --git a/bin/helpers/utils.js b/bin/helpers/utils.js index d7639ee7..ffe55b7e 100644 --- a/bin/helpers/utils.js +++ b/bin/helpers/utils.js @@ -26,6 +26,7 @@ const usageReporting = require("./usageReporting"), { OBSERVABILITY_ENV_VARS, TEST_OBSERVABILITY_REPORTER } = require('../testObservability/helper/constants'); const request = require('request'); +const { shouldProcessEventForTesthub } = require("../testhub/utils"); exports.validateBstackJson = (bsConfigPath) => { return new Promise(function (resolve, reject) { @@ -1465,7 +1466,7 @@ exports.splitStringByCharButIgnoreIfWithinARange = (str, splitChar, leftLimiter, // blindly send other passed configs with run_settings and handle at backend exports.setOtherConfigs = (bsConfig, args) => { - if(o11yHelpers.isTestObservabilitySession() && process.env.BS_TESTOPS_JWT) { + if(shouldProcessEventForTesthub()) { bsConfig["run_settings"]["reporter"] = TEST_OBSERVABILITY_REPORTER; return; } diff --git a/bin/testObservability/helper/constants.js b/bin/testObservability/helper/constants.js index 63bf08d1..336fe3e1 100644 --- a/bin/testObservability/helper/constants.js +++ b/bin/testObservability/helper/constants.js @@ -11,7 +11,8 @@ exports.IPC_EVENTS = { SCREENSHOT: 'testObservability:cypressScreenshot', COMMAND: 'testObservability:cypressCommand', CUCUMBER: 'testObservability:cypressCucumberStep', - PLATFORM_DETAILS: 'testObservability:cypressPlatformDetails' + PLATFORM_DETAILS: 'testObservability:cypressPlatformDetails', + ACCESSIBILITY_DATA: 'accessibility:cypressAccessibilityData' }; exports.OBSERVABILITY_ENV_VARS = [ diff --git a/bin/testObservability/helper/helper.js b/bin/testObservability/helper/helper.js index deacff95..062c949c 100644 --- a/bin/testObservability/helper/helper.js +++ b/bin/testObservability/helper/helper.js @@ -107,7 +107,7 @@ exports.printBuildLink = async (shouldStopSession, exitCode = null) => { if(exitCode) process.exit(exitCode); } -const nodeRequest = (type, url, data, config) => { +exports.nodeRequest = (type, url, data, config) => { return new Promise(async (resolve, reject) => { const options = {...config,...{ method: type, @@ -242,7 +242,7 @@ exports.getPackageVersion = (package_, bsConfig = null) => { return packageVersion; } -const setEnvironmentVariablesForRemoteReporter = (BS_TESTOPS_JWT, BS_TESTOPS_BUILD_HASHED_ID, BS_TESTOPS_ALLOW_SCREENSHOTS, OBSERVABILITY_LAUNCH_SDK_VERSION) => { +exports.setEnvironmentVariablesForRemoteReporter = (BS_TESTOPS_JWT, BS_TESTOPS_BUILD_HASHED_ID, BS_TESTOPS_ALLOW_SCREENSHOTS, OBSERVABILITY_LAUNCH_SDK_VERSION) => { process.env.BS_TESTOPS_JWT = BS_TESTOPS_JWT; process.env.BS_TESTOPS_BUILD_HASHED_ID = BS_TESTOPS_BUILD_HASHED_ID; process.env.BS_TESTOPS_ALLOW_SCREENSHOTS = BS_TESTOPS_ALLOW_SCREENSHOTS; @@ -316,7 +316,7 @@ exports.setCrashReportingConfigFromReporter = (credentialsStr, bsConfigPath, cyp } } -const setCrashReportingConfig = (bsConfig, bsConfigPath) => { +exports.setCrashReportingConfig = (bsConfig, bsConfigPath) => { try { const browserstackConfigFile = utils.readBsConfigJSON(bsConfigPath); const cypressConfigFile = getCypressConfigFileContent(bsConfig, null); @@ -334,7 +334,7 @@ const setCrashReportingConfig = (bsConfig, bsConfigPath) => { } exports.launchTestSession = async (user_config, bsConfigPath) => { - setCrashReportingConfig(user_config, bsConfigPath); + exports.setCrashReportingConfig(user_config, bsConfigPath); const obsUserName = user_config["auth"]["username"]; const obsAccessKey = user_config["auth"]["access_key"]; @@ -387,10 +387,10 @@ exports.launchTestSession = async (user_config, bsConfigPath) => { } }; - const response = await nodeRequest('POST','api/v1/builds',data,config); + const response = await exports.nodeRequest('POST','api/v1/builds',data,config); exports.debug('Build creation successfull!'); process.env.BS_TESTOPS_BUILD_COMPLETED = true; - setEnvironmentVariablesForRemoteReporter(response.data.jwt, response.data.build_hashed_id, response.data.allow_screenshots, data.observability_version.sdkVersion); + exports.setEnvironmentVariablesForRemoteReporter(response.data.jwt, response.data.build_hashed_id, response.data.allow_screenshots, data.observability_version.sdkVersion); if(this.isBrowserstackInfra()) helper.setBrowserstackCypressCliDependency(user_config); } catch(error) { if(!error.errorType) { @@ -417,7 +417,7 @@ exports.launchTestSession = async (user_config, bsConfigPath) => { } process.env.BS_TESTOPS_BUILD_COMPLETED = false; - setEnvironmentVariablesForRemoteReporter(null, null, null); + exports.setEnvironmentVariablesForRemoteReporter(null, null, null); } } } @@ -474,7 +474,7 @@ exports.batchAndPostEvents = async (eventUrl, kind, data) => { }; try { - const response = await nodeRequest('POST',eventUrl,data,config); + const response = await exports.nodeRequest('POST',eventUrl,data,config); if(response.data.error) { throw({message: response.data.error}); } else { @@ -491,6 +491,18 @@ exports.batchAndPostEvents = async (eventUrl, kind, data) => { } } +const shouldUploadEvent = (eventType) => { + isAccessibility = utils.isTrueString(process.env.BROWSERSTACK_TEST_ACCESSIBILITY) || !utils.isUndefined(process.env.ACCESSIBILITY_AUTH); + if(!this.isTestObservabilitySession() || isAccessibility) { + if (['HookRunStarted', 'HookRunFinished', 'LogCreated', 'BuildUpdate'].includes(eventType)) { + return false; + } + return true; + } + + return this.isTestObservabilitySession() || isAccessibility; +} + const RequestQueueHandler = require('./requestQueueHandler'); exports.requestQueueHandler = new RequestQueueHandler(); @@ -506,9 +518,9 @@ exports.uploadEventData = async (eventData, run=0) => { ['BuildUpdate']: 'Build_Update' }[eventData.event_type]; + if (!shouldUploadEvent(eventData.event_type)) return; if(run === 0 && process.env.BS_TESTOPS_JWT != "null") exports.pending_test_uploads.count += 1; - - if (process.env.BS_TESTOPS_BUILD_COMPLETED === "true") { + if (process.env.BS_TESTOPS_BUILD_COMPLETED === "true" || process.env.ACCESSIBILITY_AUTH) { if(process.env.BS_TESTOPS_JWT == "null") { exports.debug(`EXCEPTION IN ${log_tag} REQUEST TO TEST OBSERVABILITY : missing authentication token`); exports.pending_test_uploads.count = Math.max(0,exports.pending_test_uploads.count-1); @@ -537,7 +549,7 @@ exports.uploadEventData = async (eventData, run=0) => { }; try { - const response = await nodeRequest('POST',event_api_url,data,config); + const response = await exports.nodeRequest('POST',event_api_url,data,config); if(response.data.error) { throw({message: response.data.error}); } else { @@ -626,7 +638,7 @@ exports.shouldReRunObservabilityTests = () => { } exports.stopBuildUpstream = async () => { - if (process.env.BS_TESTOPS_BUILD_COMPLETED === "true") { + if (process.env.BS_TESTOPS_BUILD_COMPLETED === "true" || process.env.ACCESSIBILITY_AUTH) { if(process.env.BS_TESTOPS_JWT == "null" || process.env.BS_TESTOPS_BUILD_HASHED_ID == "null") { exports.debug('EXCEPTION IN stopBuildUpstream REQUEST TO TEST OBSERVABILITY : Missing authentication token'); return { @@ -646,7 +658,7 @@ exports.stopBuildUpstream = async () => { }; try { - const response = await nodeRequest('PUT',`api/v1/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID}/stop`,data,config); + const response = await exports.nodeRequest('PUT',`api/v1/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID}/stop`,data,config); if(response.data && response.data.error) { throw({message: response.data.error}); } else { diff --git a/bin/testObservability/reporter/index.js b/bin/testObservability/reporter/index.js index 33c515f6..25c52101 100644 --- a/bin/testObservability/reporter/index.js +++ b/bin/testObservability/reporter/index.js @@ -61,6 +61,7 @@ const { } = require('../helper/helper'); const { consoleHolder } = require('../helper/constants'); +const { shouldProcessEventForTesthub, appendTestHubParams } = require('../../testhub/utils'); // this reporter outputs test results, indenting two spaces per suite class MyReporter { @@ -76,6 +77,7 @@ class MyReporter { this.platformDetailsMap = {}; this.runStatusMarkedHash = {}; this.haveSentBuildUpdate = false; + this.accessibilityScanInfo = {}; this.registerListeners(); setCrashReportingConfigFromReporter(null, process.env.OBS_CRASH_REPORTING_BS_CONFIG_PATH, process.env.OBS_CRASH_REPORTING_CYPRESS_CONFIG_PATH); @@ -87,7 +89,7 @@ class MyReporter { }) .on(EVENT_HOOK_BEGIN, async (hook) => { - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { if(!hook.hookAnalyticsId) { hook.hookAnalyticsId = uuidv4(); } else if(this.runStatusMarkedHash[hook.hookAnalyticsId]) { @@ -102,7 +104,7 @@ class MyReporter { }) .on(EVENT_HOOK_END, async (hook) => { - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { if(!this.runStatusMarkedHash[hook.hookAnalyticsId]) { if(!hook.hookAnalyticsId) { /* Hook objects don't maintain uuids in Cypress-Mocha */ @@ -123,7 +125,7 @@ class MyReporter { }) .on(EVENT_TEST_PASS, async (test) => { - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { if(!this.runStatusMarkedHash[test.testAnalyticsId]) { if(test.testAnalyticsId) this.runStatusMarkedHash[test.testAnalyticsId] = true; await this.sendTestRunEvent(test); @@ -132,7 +134,7 @@ class MyReporter { }) .on(EVENT_TEST_FAIL, async (test, err) => { - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { if((test.testAnalyticsId && !this.runStatusMarkedHash[test.testAnalyticsId]) || (test.hookAnalyticsId && !this.runStatusMarkedHash[test.hookAnalyticsId])) { if(test.testAnalyticsId) { this.runStatusMarkedHash[test.testAnalyticsId] = true; @@ -146,7 +148,7 @@ class MyReporter { }) .on(EVENT_TEST_PENDING, async (test) => { - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { if(!test.testAnalyticsId) test.testAnalyticsId = uuidv4(); if(!this.runStatusMarkedHash[test.testAnalyticsId]) { this.runStatusMarkedHash[test.testAnalyticsId] = true; @@ -157,14 +159,14 @@ class MyReporter { .on(EVENT_TEST_BEGIN, async (test) => { if (this.runStatusMarkedHash[test.testAnalyticsId]) return; - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { await this.testStarted(test); } }) .on(EVENT_TEST_END, async (test) => { if (this.runStatusMarkedHash[test.testAnalyticsId]) return; - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { if(!this.runStatusMarkedHash[test.testAnalyticsId]) { if(test.testAnalyticsId) this.runStatusMarkedHash[test.testAnalyticsId] = true; await this.sendTestRunEvent(test); @@ -174,7 +176,7 @@ class MyReporter { .once(EVENT_RUN_END, async () => { try { - if(this.testObservability == true) { + if(shouldProcessEventForTesthub()) { const hookSkippedTests = getHookSkippedTests(this.runner.suite); for(const test of hookSkippedTests) { if(!test.testAnalyticsId) test.testAnalyticsId = uuidv4(); @@ -199,6 +201,7 @@ class MyReporter { server.on(IPC_EVENTS.COMMAND, this.cypressCommandListener.bind(this)); server.on(IPC_EVENTS.CUCUMBER, this.cypressCucumberStepListener.bind(this)); server.on(IPC_EVENTS.PLATFORM_DETAILS, this.cypressPlatformDetailsListener.bind(this)); + server.on(IPC_EVENTS.ACCESSIBILITY_DATA, this.cypressAccessibilityDataListener.bind(this)); }, (server) => { server.off(IPC_EVENTS.CONFIG, '*'); @@ -319,6 +322,9 @@ class MyReporter { } }; + this.persistTestId(testData, eventType) + appendTestHubParams(testData, eventType, this.accessibilityScanInfo) + if(eventType.match(/TestRunFinished/) || eventType.match(/TestRunSkipped/)) { testData['meta'].steps = JSON.parse(JSON.stringify(this.currentTestCucumberSteps)); this.currentTestCucumberSteps = []; @@ -560,6 +566,25 @@ class MyReporter { this.currentCypressVersion = cypressVersion; } + cypressAccessibilityDataListener = async(accessibilityData) => { + this.accessibilityScanInfo = {...this.accessibilityScanInfo, ...accessibilityData} + } + + persistTestId = (testData, eventType) => { + if (!eventType.match(/TestRun/)) {return} + + const fileName = 'testDetails.json' + let testDetails = {}; + try { + if(fs.existsSync(fileName)) { + testDetails = JSON.parse(fs.readFileSync(fileName).toString()) + } + testDetails[testData.identifier] = testData.uuid + fs.writeFileSync(fileName, JSON.stringify(testDetails)) + consoleHolder.log('FILE WRITTEN ::::::::: ' + JSON.stringify(testDetails)) + } catch (err) {} + } + getFormattedArgs = (args) => { if(!args) return ''; let res = ''; diff --git a/bin/testhub/constants.js b/bin/testhub/constants.js new file mode 100644 index 00000000..6be9945f --- /dev/null +++ b/bin/testhub/constants.js @@ -0,0 +1,10 @@ +module.exports = { + 'TESTHUB_BUILD_API': 'api/v2/builds', + 'ACCESSIBILITY': 'accessibility', + 'OBSERVABILITY': 'observability', + 'ERROR': { + 'INVALID_CREDENTIALS': 'ERROR_INVALID_CREDENTIALS', + 'DEPRECATED': 'ERROR_SDK_DEPRECATED', + 'ACCESS_DENIED': 'ERROR_ACCESS_DENIED' + }, +}; diff --git a/bin/testhub/plugin/index.js b/bin/testhub/plugin/index.js new file mode 100644 index 00000000..f146b647 --- /dev/null +++ b/bin/testhub/plugin/index.js @@ -0,0 +1,9 @@ +const accessibilityPlugin = require('../../accessibility-automation/plugin/index') +const testObservabilityPlugin = require('../../testObservability/plugin') + +const browserstackPlugin = (on, config) => { + accessibilityPlugin(on, config) + testObservabilityPlugin(on, config) +} + +module.exports = browserstackPlugin diff --git a/bin/testhub/testhubHandler.js b/bin/testhub/testhubHandler.js new file mode 100644 index 00000000..cecf9fab --- /dev/null +++ b/bin/testhub/testhubHandler.js @@ -0,0 +1,119 @@ +const logger = require('../../bin/helpers/logger').winstonLogger; +const { setCrashReportingConfig, nodeRequest, isTestObservabilitySession } = require("../testObservability/helper/helper"); +const helper = require('../helpers/helper'); +const testhubUtils = require('./utils'); +const TESTHUB_CONSTANTS = require('./constants'); + +class TestHubHandler { + static async launchBuild(user_config, bsConfigPath) { + setCrashReportingConfig(user_config, bsConfigPath); + + const obsUserName = user_config["auth"]["username"]; + const obsAccessKey = user_config["auth"]["access_key"]; + + const BSTestOpsToken = `${obsUserName || ''}:${obsAccessKey || ''}`; + if(BSTestOpsToken === '') { + // if olly true + if (isTestObservabilitySession()) { + logger.debug('EXCEPTION IN BUILD START EVENT : Missing authentication token'); + process.env.BS_TESTOPS_BUILD_COMPLETED = false; + } + + if (testhubUtils.isAccessibilityEnabled()) { + logger.debug('Exception while creating test run for BrowserStack Accessibility Automation: Missing authentication token'); + process.env.BROWSERSTACK_TEST_ACCESSIBILITY = 'false' + } + + return [null, null]; + } + + try { + const data = await this.generateBuildUpstreamData(user_config); + const config = this.getConfig(obsUserName, obsAccessKey); + const response = await nodeRequest('POST', TESTHUB_CONSTANTS.TESTHUB_BUILD_API, data, config); + const launchData = this.extractDataFromResponse(user_config, data, response, config); + } catch (error) { + if (error.success === false) { // non 200 response + testhubUtils.logBuildError(error); + return; + } + } + } + + static async extractDataFromResponse(user_config, requestData, response, config) { + const launchData = {}; + + if (isTestObservabilitySession()) { + const [jwt, buildHashedId, allowScreenshot] = testhubUtils.setTestObservabilityVariables(user_config, requestData, response.data); + if (jwt && buildHashedId) { + launchData[TESTHUB_CONSTANTS.OBSERVABILITY] = {jwt, buildHashedId, allowScreenshot}; + process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'true'; + } else { + launchData[TESTHUB_CONSTANTS.OBSERVABILITY] = {}; + process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'false'; + } + } else { + process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'false'; + } + + if(testhubUtils.isAccessibilityEnabled()) { + const [authToken, buildHashedId] = testhubUtils.setAccessibilityVariables(user_config, response.data); + if (authToken && buildHashedId) { + launchData[TESTHUB_CONSTANTS.ACCESSIBILITY] = {authToken, buildHashedId}; + process.env.BROWSERSTACK_ACCESSIBILITY = 'true'; + testhubUtils.checkAndSetAccessibility(user_config, true); + } else { + launchData[TESTHUB_CONSTANTS.ACCESSIBILITY] = {}; + process.env.BROWSERSTACK_ACCESSIBILITY = 'false'; + testhubUtils.checkAndSetAccessibility(user_config, false); + } + } else { + process.env.BROWSERSTACK_ACCESSIBILITY = 'false'; + testhubUtils.checkAndSetAccessibility(user_config, false) + } + + if (testhubUtils.shouldProcessEventForTesthub()) { + testhubUtils.setTestHubCommonMetaInfo(user_config, response.data); + } + } + + static async generateBuildUpstreamData(user_config) { + const {buildName, projectName, buildDescription, buildTags} = helper.getBuildDetails(user_config, true); + const productMap = testhubUtils.getProductMap(user_config); + const data = { + 'project_name': projectName, + 'name': buildName, + 'build_identifier': '', // no build identifier in cypress + 'description': buildDescription || '', + 'started_at': (new Date()).toISOString(), + 'tags': buildTags, + 'host_info': helper.getHostInfo(), + 'ci_info': helper.getCiInfo(), + 'build_run_identifier': process.env.BROWSERSTACK_BUILD_RUN_IDENTIFIER, + 'failed_tests_rerun': process.env.BROWSERSTACK_RERUN || false, + 'version_control': await helper.getGitMetaData(), + 'accessibility': testhubUtils.getAccessibilityOptions(user_config), + 'framework_details': testhubUtils.getFrameworkDetails(), + 'product_map': productMap, + 'browserstackAutomation': productMap['automate'] + }; + + return data; + } + + static getConfig(obsUserName, obsAccessKey) { + return { + auth: { + username: obsUserName, + password: obsAccessKey + }, + headers: { + 'Content-Type': 'application/json', + 'X-BSTACK-TESTOPS': 'true' + } + }; + } +} + + +module.exports = TestHubHandler; diff --git a/bin/testhub/utils.js b/bin/testhub/utils.js new file mode 100644 index 00000000..f7a8f917 --- /dev/null +++ b/bin/testhub/utils.js @@ -0,0 +1,205 @@ +const os = require('os'); + +const logger = require('../../bin/helpers/logger').winstonLogger; +const TESTHUB_CONSTANTS = require('./constants'); +const testObservabilityHelper = require('../../bin/testObservability/helper/helper'); +const helper = require('../helpers/helper'); +const accessibilityHelper = require('../accessibility-automation/helper'); + +const isUndefined = value => (value === undefined || value === null); + +exports.getFrameworkDetails = (user_config) => { + return { + 'frameworkName': 'Cypress', + 'frameworkVersion': testObservabilityHelper.getPackageVersion('cypress', user_config), + 'sdkVersion': helper.getAgentVersion(), + 'language': 'javascript', + 'testFramework': { + 'name': 'cypress', + 'version': helper.getPackageVersion('cypress', user_config) + } + }; +}; + +exports.isAccessibilityEnabled = () => { + if (process.env.BROWSERSTACK_TEST_ACCESSIBILITY !== undefined) { + return process.env.BROWSERSTACK_TEST_ACCESSIBILITY === 'true'; + } + return false; +} + +// app-automate and percy support is not present for cypress +exports.getProductMap = (user_config) => { + return { + 'observability': testObservabilityHelper.isTestObservabilitySession(), + 'accessibility': exports.isAccessibilityEnabled(user_config), + 'percy': false, + 'automate': testObservabilityHelper.isBrowserstackInfra(), + 'app_automate': false + }; +}; + +exports.shouldProcessEventForTesthub = () => { + return testObservabilityHelper.isTestObservabilitySession() || exports.isAccessibilityEnabled(); +} + +exports.setTestObservabilityVariables = (user_config, requestData, responseData) => { + if (!responseData.observability) { + exports.handleErrorForObservability(); + + return [null, null, null]; + } + + if (!responseData.observability.success) { + exports.handleErrorForObservability(responseData.observability); + + return [null, null, null]; + } + + if (testObservabilityHelper.isBrowserstackInfra()) { + process.env.BS_TESTOPS_BUILD_COMPLETED = true; + testObservabilityHelper.setEnvironmentVariablesForRemoteReporter(responseData.jwt, responseData.build_hashed_id, responseData.observability.options.allow_screenshots.toString(), requestData.framework_details.sdkVersion); + helper.setBrowserstackCypressCliDependency(user_config) + return [responseData.jwt, responseData.build_hashed_id, process.env.BS_TESTOPS_ALLOW_SCREENSHOTS]; + } + return [null, null, null]; +} + +exports.handleErrorForObservability = (error) => { + process.env.BROWSERSTACK_TESTHUB_UUID = 'null'; + process.env.BROWSERSTACK_TESTHUB_JWT = 'null'; + process.env.BS_TESTOPS_BUILD_COMPLETED = 'false'; + process.env.BS_TESTOPS_JWT = 'null'; + process.env.BS_TESTOPS_BUILD_HASHED_ID = 'null'; + process.env.BS_TESTOPS_ALLOW_SCREENSHOTS = 'null'; + exports.logBuildError(error, TESTHUB_CONSTANTS.OBSERVABILITY); +}; + +exports.setAccessibilityVariables = (user_config, responseData) => { + if (!responseData.accessibility) { + exports.handleErrorForAccessibility(user_config); + + return [null, null]; + } + + if (!responseData.accessibility.success) { + exports.handleErrorForAccessibility(user_config, responseData.accessibility); + + return [null, null]; + } + + if(responseData.accessibility.options) { + logger.debug(`BrowserStack Accessibility Automation Build Hashed ID: ${responseData.build_hashed_id}`); + setAccessibilityCypressCapabilities(user_config, responseData); + helper.setBrowserstackCypressCliDependency(user_config); + return [process.env.ACCESSIBILITY_AUTH, responseData.build_hashed_id]; + } + return [null, null]; +} + +const setAccessibilityCypressCapabilities = (user_config, responseData) => { + if (isUndefined(user_config.run_settings.accessibilityOptions)) { + user_config.run_settings.accessibilityOptions = {} + } + const {accessibilityToken, scannerVersion} = jsonifyAccessibilityArray(responseData.accessibility.options.capabilities, 'name', 'value'); + process.env.ACCESSIBILITY_AUTH = accessibilityToken + process.env.ACCESSIBILITY_SCANNERVERSION = scannerVersion + + user_config.run_settings.accessibilityOptions["authToken"] = accessibilityToken; + user_config.run_settings.accessibilityOptions["auth"] = accessibilityToken; + user_config.run_settings.accessibilityOptions["scannerVersion"] = scannerVersion; + user_config.run_settings.system_env_vars.push('ACCESSIBILITY_AUTH') + user_config.run_settings.system_env_vars.push('ACCESSIBILITY_SCANNERVERSION') + this.checkAndSetAccessibility(user_config, true) +} + +// To handle array of json, eg: [{keyName : '', valueName : ''}] +const jsonifyAccessibilityArray = (dataArray, keyName, valueName) => { + const result = {}; + dataArray.forEach(element => { + result[element[keyName]] = element[valueName]; + }); + + return result; +}; + + +exports.handleErrorForAccessibility = (user_config, error) => { + this.checkAndSetAccessibility(user_config, false) + process.env.BROWSERSTACK_TESTHUB_UUID = 'null'; + process.env.BROWSERSTACK_TESTHUB_JWT = 'null'; + exports.logBuildError(error, TESTHUB_CONSTANTS.ACCESSIBILITY); +}; + +exports.logBuildError = (error, product = '') => { + if (error === undefined) { + logger.error(`${product.toUpperCase()} Build creation failed`); + + return; + } + + try { + for (const errorJson of error.errors) { + const errorType = errorJson.key; + const errorMessage = errorJson.message; + if (errorMessage) { + switch (errorType) { + case TESTHUB_CONSTANTS.ERROR.INVALID_CREDENTIALS: + logger.error(errorMessage); + break; + case TESTHUB_CONSTANTS.ERROR.ACCESS_DENIED: + logger.info(errorMessage); + break; + case TESTHUB_CONSTANTS.ERROR.DEPRECATED: + logger.error(errorMessage); + break; + default: + logger.error(errorMessage); + } + } + } + } catch (e) { + logger.error(error) + } +}; + +exports.setTestHubCommonMetaInfo = (user_config, responseData) => { + process.env.BROWSERSTACK_TESTHUB_JWT = responseData.jwt; + process.env.BROWSERSTACK_TESTHUB_UUID = responseData.build_hashed_id; + user_config.run_settings.system_env_vars.push('BROWSERSTACK_TESTHUB_JWT') + user_config.run_settings.system_env_vars.push('BROWSERSTACK_TESTHUB_UUID') +}; + +exports.checkAndSetAccessibility = (user_config, accessibilityFlag) => { + if (!user_config.run_settings.system_env_vars.includes('BROWSERSTACK_TEST_ACCESSIBILITY')) { + user_config.run_settings.system_env_vars.push('BROWSERSTACK_TEST_ACCESSIBILITY') + } + + // if flag already provided, then set the value and return + if (!isUndefined(accessibilityFlag)) { + process.env.BROWSERSTACK_TEST_ACCESSIBILITY = accessibilityFlag.toString(); + user_config.run_settings.accessibility = accessibilityFlag; + return; + } + + if (!accessibilityHelper.isAccessibilitySupportedCypressVersion(user_config.run_settings.cypress_config_file) ){ + logger.warn(`Accessibility Testing is not supported on Cypress version 9 and below.`) + process.env.BROWSERSTACK_TEST_ACCESSIBILITY = 'false'; + user_config.run_settings.accessibility = false; + return; + } + + isAccessibilityTestEnabled = (user_config.run_settings.accessibility || accessibilityHelper.checkAccessibilityPlatform(user_config)) && testObservabilityHelper.isBrowserstackInfra(); + process.env.BROWSERSTACK_TEST_ACCESSIBILITY = isAccessibilityTestEnabled.toString(); +} + +exports.getAccessibilityOptions = (user_config) => { + const settings = isUndefined(user_config.run_settings.accessibilityOptions) ? {} : user_config.run_settings.accessibilityOptions + return {'settings': settings}; +} + +exports.appendTestHubParams = (testData, eventType, accessibilityScanInfo) => { + if (exports.isAccessibilityEnabled() && !['HookRunStarted', 'HookRunFinished', 'TestRunStarted'].includes(eventType) && !isUndefined(accessibilityScanInfo[testData.name])) { + testData['product_map'] = {'accessibility' : accessibilityScanInfo[testData.name]} + } +}