diff --git a/nightwatch/globals.js b/nightwatch/globals.js index 1941ac2..5ee597b 100644 --- a/nightwatch/globals.js +++ b/nightwatch/globals.js @@ -11,6 +11,9 @@ const eventHelper = require('../src/utils/eventHelper'); const localTunnel = new LocalTunnel(); const testObservability = new TestObservability(); const accessibilityAutomation = new AccessibilityAutomation(); +const TestHubHandler = require('../src/testHub/testHubHandler'); +const testHubHandler = new TestHubHandler(); +const testHubUtils = require('../src/testHub/utils'); const nightwatchRerun = process.env.NIGHTWATCH_RERUN_FAILED; const nightwatchRerunFile = process.env.NIGHTWATCH_RERUN_REPORT_FILE; @@ -40,6 +43,46 @@ const handleScreenshotUpload = async (data) => { } }; +const setupAccessibility = async (settings) => { + if (helper.isCucumberTestSuite()) { return } + + try { + accessibilityAutomation.configure(settings); + if (helper.isAccessibilitySession()) { + if (accessibilityAutomation._user && accessibilityAutomation._key) { + const [jwtToken, testRunId] = await accessibilityAutomation.createAccessibilityTestRun(); + process.env.BS_A11Y_JWT = jwtToken; + process.env.BS_A11Y_TEST_RUN_ID = testRunId; + if (helper.isAccessibilitySession()) { + accessibilityAutomation.setAccessibilityCapabilities(settings); + } + } + } + } catch (error) { + Logger.error(`Could not configure or launch accessibility automation - ${error}`); + } +}; + +const setupTestObservability = async (settings) => { + if (helper.isCucumberTestSuite()) { return } + + try { + testObservability.configure(settings); + if (helper.isTestObservabilitySession()) { + settings.globals['customReporterCallbackTimeout'] = CUSTOM_REPORTER_CALLBACK_TIMEOUT; + if (testObservability._user && testObservability._key) { + await testObservability.launchTestSession(); + } + if (process.env.BROWSERSTACK_RERUN === 'true' && process.env.BROWSERSTACK_RERUN_TESTS && process.env.BROWSERSTACK_RERUN_TESTS!=='null') { + const specs = process.env.BROWSERSTACK_RERUN_TESTS.split(','); + await helper.handleNightwatchRerun(specs); + } + } + } catch (error) { + Logger.error(`Could not configure or launch test observability - ${error}`); + } +}; + module.exports = { reporter: async function(results, done) { @@ -77,7 +120,7 @@ module.exports = { registerEventHandlers(eventBroadcaster) { eventBroadcaster.on('TestCaseStarted', async (args) => { - if (!helper.isTestObservabilitySession()) { + if (!testHubUtils.shouldProcessEventForTestHub()) { return; } try { @@ -133,7 +176,7 @@ module.exports = { }); eventBroadcaster.on('TestCaseFinished', async (args) => { - if (!helper.isTestObservabilitySession()) { + if (!testHubUtils.shouldProcessEventForTestHub()) { return; } try { @@ -156,7 +199,7 @@ module.exports = { }); eventBroadcaster.on('TestStepStarted', async (args) => { - if (!helper.isTestObservabilitySession()) { + if (!testHubUtils.shouldProcessEventForTestHub()) { return; } try { @@ -189,7 +232,7 @@ module.exports = { }); eventBroadcaster.on('TestStepFinished', async (args) => { - if (!helper.isTestObservabilitySession()) { + if (!testHubUtils.shouldProcessEventForTestHub()) { return; } try { @@ -293,47 +336,35 @@ module.exports = { } try { - testObservability.configure(settings); - if (helper.isTestObservabilitySession()) { - if (helper.isCucumberTestSuite(settings)) { - cucumberPatcher(); - process.env.CUCUMBER_SUITE = 'true'; - settings.test_runner.options['require'] = path.resolve(__dirname, 'observabilityLogPatcherHook.js'); - } - settings.globals['customReporterCallbackTimeout'] = CUSTOM_REPORTER_CALLBACK_TIMEOUT; - if (testObservability._user && testObservability._key) { - await testObservability.launchTestSession(); - } + testHubHandler.configure(settings); + if (testHubUtils.shouldProcessEventForTestHub()) { + await testHubHandler.launchBuild(); + cucumberPatcher(); if (process.env.BROWSERSTACK_RERUN === 'true' && process.env.BROWSERSTACK_RERUN_TESTS && process.env.BROWSERSTACK_RERUN_TESTS!=='null') { const specs = process.env.BROWSERSTACK_RERUN_TESTS.split(','); await helper.handleNightwatchRerun(specs); } } - } catch (error) { - Logger.error(`Could not configure or launch test observability - ${error}`); - } - - try { - accessibilityAutomation.configure(settings); if (helper.isAccessibilitySession()) { - if (accessibilityAutomation._user && accessibilityAutomation._key) { - const [jwtToken, testRunId] = await accessibilityAutomation.createAccessibilityTestRun(); - process.env.BS_A11Y_JWT = jwtToken; - process.env.BS_A11Y_TEST_RUN_ID = testRunId; - if (helper.isAccessibilitySession()) { - accessibilityAutomation.setAccessibilityCapabilities(settings); - } - } + accessibilityAutomation.setAccessibilityCapabilities(settings); } + } catch (error) { - Logger.error(`Could not configure or launch accessibility automation - ${error}`); + Logger.error(`Could not configure or launch testHub Build - ${error}`); } + await setupTestObservability(settings); + await setupAccessibility(settings); + + if (testHubUtils.shouldProcessEventForTestHub()) { + settings.test_runner.options['require'] = path.resolve(__dirname, 'observabilityLogPatcherHook.js'); + } }, async after() { localTunnel.stop(); - if (helper.isTestObservabilitySession()) { + await testHubHandler.stopTestHub(); + if (helper.isTestObservabilitySession() && !helper.isCucumberTestSuite()) { process.env.NIGHTWATCH_RERUN_FAILED = nightwatchRerun; process.env.NIGHTWATCH_RERUN_REPORT_FILE = nightwatchRerunFile; if (process.env.BROWSERSTACK_RERUN === 'true' && process.env.BROWSERSTACK_RERUN_TESTS) { @@ -347,27 +378,28 @@ module.exports = { } catch (error) { Logger.error(`Something went wrong in stopping build session for test observability - ${error}`); } - process.exit(); } - if (helper.isAccessibilitySession()){ + if (helper.isAccessibilitySession() && !helper.isCucumberTestSuite()){ try { await accessibilityAutomation.stopAccessibilityTestRun(); } catch (error) { Logger.error(`Exception in stop accessibility test run: ${error}`); } - } + process.exit(); }, async beforeEach(settings) { - browser.getAccessibilityResults = () => { return accessibilityAutomation.getAccessibilityResults() }; - browser.getAccessibilityResultsSummary = () => { return accessibilityAutomation.getAccessibilityResultsSummary() }; - // await accessibilityAutomation.beforeEachExecution(browser); + if (helper.isAccessibilitySession()) { + helper.modifySeleniumCommands(); + helper.modifyNightwatchCommands(); + browser.getAccessibilityResults = () => { return accessibilityAutomation.getAccessibilityResults() }; + browser.getAccessibilityResultsSummary = () => { return accessibilityAutomation.getAccessibilityResultsSummary() }; + } }, - // This will be run after each test suite is finished + // This will be run after each test suite is finished for default nightwatch runner async afterEach(settings) { - // await accessibilityAutomation.afterEachExecution(browser); }, beforeChildProcess(settings) { diff --git a/nightwatch/observabilityLogPatcherHook.js b/nightwatch/observabilityLogPatcherHook.js index d39e071..2c22b75 100644 --- a/nightwatch/observabilityLogPatcherHook.js +++ b/nightwatch/observabilityLogPatcherHook.js @@ -1,8 +1,13 @@ try { - const {Before} = require('@cucumber/cucumber'); - - Before((testCase) => { - console.log(`TEST-OBSERVABILITY-PID-TESTCASE-MAPPING-${testCase.testCaseStartedId}`); + const {Before, After} = require('@cucumber/cucumber'); + const testhubUtils = require('@nightwatch/browserstack/src/testHub/utils'); + + Before(async (testCase) => { + await testhubUtils.beforeEachCucumberTest(testCase); + }); + + After(async (testCase) => { + await testhubUtils.afterEachCucumberTest(testCase); }); } catch (error) { /* empty */ } diff --git a/src/accessibilityAutomation.js b/src/accessibilityAutomation.js index af97fa0..7ace0db 100644 --- a/src/accessibilityAutomation.js +++ b/src/accessibilityAutomation.js @@ -4,6 +4,7 @@ const {makeRequest} = require('./utils/requestHelper'); const Logger = require('./utils/logger'); const {ACCESSIBILITY_URL} = require('./utils/constants'); const util = require('util'); +const scripts = require('./utils/scripts'); class AccessibilityAutomation { configure(settings = {}) { @@ -73,7 +74,10 @@ class AccessibilityAutomation { source: { frameworkName: helper.getFrameworkName(this._testRunner), frameworkVersion: helper.getPackageVersion('nightwatch'), - sdkVersion: helper.getAgentVersion() + sdkVersion: helper.getAgentVersion(), + language: 'javascript', + testFramework: 'selenium', + testFrameworkVersion: helper.getPackageVersion('selenium-webdriver') }, settings: accessibilityOptions, versionControl: await helper.getGitMetaData(), @@ -91,9 +95,11 @@ class AccessibilityAutomation { } }; - const response = await makeRequest('POST', 'test_runs', data, config, ACCESSIBILITY_URL); + const response = await makeRequest('POST', 'v2/test_runs', data, config, ACCESSIBILITY_URL); const responseData = response.data.data || {}; + scripts.parseFromJson(responseData); + scripts.toJson(); accessibilityOptions.scannerVersion = responseData.scannerVersion; process.env.BROWSERSTACK_ACCESSIBILITY_OPTIONS = JSON.stringify(accessibilityOptions); @@ -353,11 +359,13 @@ class AccessibilityAutomation { async beforeEachExecution(testMetaData) { try { - this.currentTest = browser.currentTest; + this.currentTest = browser.currentTest || {}; this.currentTest.shouldScanTestForAccessibility = this.shouldScanTestForAccessibility( testMetaData ); + global.shouldScanTestForAccessibility = this.currentTest.shouldScanTestForAccessibility; this.currentTest.accessibilityScanStarted = true; + global.isAccessibilityPlatform = true; this._isAccessibilitySession = this.setExtension(browser); if (this.isAccessibilityAutomationSession() && browser && helper.isAccessibilitySession() && this._isAccessibilitySession) { @@ -381,25 +389,6 @@ class AccessibilityAutomation { Logger.info( 'Setup for Accessibility testing has started. Automate test case execution will begin momentarily.' ); - - await browser.executeAsyncScript(` - const callback = arguments[arguments.length - 1]; - const fn = () => { - window.addEventListener('A11Y_TAP_STARTED', fn2); - const e = new CustomEvent('A11Y_FORCE_START'); - window.dispatchEvent(e); - }; - const fn2 = () => { - window.removeEventListener('A11Y_TAP_STARTED', fn); - callback(); - } - fn(); - `); - } else { - await browser.executeAsyncScript(` - const e = new CustomEvent('A11Y_FORCE_STOP'); - window.dispatchEvent(e); - `); } } this.currentTest.accessibilityScanStarted = @@ -419,51 +408,43 @@ class AccessibilityAutomation { async afterEachExecution(testMetaData) { try { - if (this.currentTest.accessibilityScanStarted && this.isAccessibilityAutomationSession() && this._isAccessibilitySession) { - if (this.currentTest.shouldScanTestForAccessibility) { + const shouldScanTestForAccessibility = this.currentTest ? this.currentTest.shouldScanTestForAccessibility : this.shouldScanTestForAccessibility( + testMetaData + ); + const accessibilityScanStarted = this.currentTest ? this.currentTest.accessibilityScanStarted : true; + this._isAccessibilitySession = this.setExtension(browser); + if (accessibilityScanStarted && this.isAccessibilityAutomationSession() && this._isAccessibilitySession) { + if (shouldScanTestForAccessibility) { Logger.info( 'Automate test case execution has ended. Processing for accessibility testing is underway. ' ); } - const dataForExtension = { - saveResults: this.currentTest.shouldScanTestForAccessibility, - testDetails: { - name: testMetaData.testcase, - testRunId: process.env.BS_A11Y_TEST_RUN_ID, - filePath: testMetaData.metadata.modulePath, - scopeList: [testMetaData.metadata.name, testMetaData.testcase] - }, - platform: await this.fetchPlatformDetails(browser) - }; - const final_res = await browser.executeAsyncScript( - ` - const callback = arguments[arguments.length - 1]; - - this.res = null; - if (arguments[0].saveResults) { - window.addEventListener('A11Y_TAP_TRANSPORTER', (event) => { - window.tapTransporterData = event.detail; - this.res = window.tapTransporterData; - callback(this.res); - }); - } - const e = new CustomEvent('A11Y_TEST_END', {detail: arguments[0]}); - window.dispatchEvent(e); - if (arguments[0].saveResults !== true ) { - callback(); - } - `, - dataForExtension - ); - if (this.currentTest.shouldScanTestForAccessibility) { - Logger.info('Accessibility testing for this test case has ended.'); + let dataForExtension = {}; + if (helper.isCucumberTestSuite()) { + dataForExtension = { + thTestRunUuid: process.env.TEST_OPS_TEST_UUID, + thBuildUuid: process.env.BROWSERSTACK_TESTHUB_UUID, + thJwtToken: process.env.BROWSERSTACK_TESTHUB_JWT + }; + } else { + dataForExtension = { + saveResults: shouldScanTestForAccessibility, + testDetails: { + name: testMetaData.testcase, + testRunId: process.env.BS_A11Y_TEST_RUN_ID, + filePath: testMetaData.metadata.modulePath, + scopeList: [testMetaData.metadata.name, testMetaData.testcase] + }, + platform: await this.fetchPlatformDetails(browser) + }; } + Logger.debug('Performing scan before saving results'); + Logger.debug(util.format(await browser.executeAsyncScript(scripts.performScan, {method: testMetaData.testcase}))); + await browser.executeAsyncScript(scripts.saveTestResults, dataForExtension); + Logger.info('Accessibility testing for this test case has ended.'); } } catch (er) { - Logger.error( - `Accessibility results could not be processed for the test case ${this.currentTest.module}. Error :`, - er - ); + Logger.error('Accessibility results could not be processed for the test case. Error: ' + er.toString()); } } diff --git a/src/testHub/testHubHandler.js b/src/testHub/testHubHandler.js new file mode 100644 index 0000000..ddcee19 --- /dev/null +++ b/src/testHub/testHubHandler.js @@ -0,0 +1,236 @@ +const helper = require('../utils/helper'); +const CrashReporter = require('../utils/crashReporter'); +const Logger = require('../utils/logger'); +const testHubUtils = require('./utils'); +const {makeRequest} = require('../utils/requestHelper'); +const constants = require('../utils/constants'); + + +class TestHubHandler { + configure(settings = {}) { + this._settings = settings['@nightwatch/browserstack'] || {}; + process.env.BROWSERSTACK_INFRA = true; + if (settings && settings.webdriver && settings.webdriver.host && settings.webdriver.host.indexOf('browserstack') === -1){ + process.env.BROWSERSTACK_INFRA = false; + } + this.#configureAccessibility(settings); + this.#configureObservability(settings); + this.setCredentials(settings); + } + + #configureAccessibility(settings) { + if (this._settings.accessibility) { + process.env.BROWSERSTACK_ACCESSIBILITY = String(this._settings.accessibility).toLowerCase() === 'true'; + } + if (process.argv.includes('--disable-accessibility')) { + process.env.BROWSERSTACK_ACCESSIBILITY = false; + + return; + } + + this._testRunner = settings.test_runner; + this._bstackOptions = {}; + if (settings && settings.desiredCapabilities && settings.desiredCapabilities['bstack:options']) { + this._bstackOptions = settings.desiredCapabilities['bstack:options']; + } + } + + #configureObservability(settings) { + if (this._settings.test_observability) { + process.env.BROWSERSTACK_TEST_OBSERVABILITY = this._settings.test_observability.enabled; + } + if (process.argv.includes('--disable-test-observability')) { + process.env.BROWSERSTACK_TEST_OBSERVABILITY = false; + + return; + } + + this._testRunner = settings.test_runner; + this._bstackOptions = {}; + if (settings && settings.desiredCapabilities && settings.desiredCapabilities['bstack:options']) { + this._bstackOptions = settings.desiredCapabilities['bstack:options']; + } + + if (helper.isCucumberTestSuite(settings)) { + process.env.CUCUMBER_SUITE = 'true'; + } + + } + + async launchBuild() { + try { + const data = await this.generateBuildUpstreamData(); + const config = this.#getConfig(); + // Logger.info('DATA => ' + JSON.stringify(data)); + const response = await makeRequest('POST', constants.TH_BUILD_API, data, config, constants.API_URL); + // Logger.info('Build Response :' + JSON.stringify(response)); + this.extractDataFromResponse(response, data); + + } catch (error) { + Logger.error(error); + } + } + + async generateBuildUpstreamData() { + const options = this._settings.test_observability || {}; + + const data = { + 'project_name': helper.getProjectName(this._settings), + 'name': helper.getBuildName(this._settings, this._bstackOptions, testHubUtils.getProductMap), + 'build_identifier': options.buildIdentifier, + 'description': options.buildDescription || '', + 'started_at': new Date().toISOString(), + 'tags': helper.getObservabilityBuildTags(this._settings, this._bstackOptions), + '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': this.getAccessibilityOptions(), + 'framework_details': testHubUtils.getFrameworkDetails(this._testRunner), + 'product_map': testHubUtils.getProductMap(), + 'browserstackAutomation': helper.isBrowserstackInfra() + }; + + return data; + } + + #getConfig() { + return { + auth: { + username: this._user, + password: this._key + }, + headers: { + 'Content-Type': 'application/json', + 'X-BSTACK-TESTOPS': 'true' + } + }; + } + + getAccessibilityOptions() { + if (helper.isUndefined(this._settings.accessibilityOptions)) { + return {}; + } + + return {'settings': this._settings.accessibilityOptions}; + } + + setCredentials(settings) { + if (this._settings.accessibility || this._bstackOptions) { + this._user = helper.getUserName(settings, this._settings); + this._key = helper.getAccessKey(settings, this._settings); + } + if (this._settings.test_observability || this._bstackOptions) { + const _user = helper.getObservabilityUser(this._settings.test_observability, this._bstackOptions); + const _key = helper.getObservabilityKey(this._settings.test_observability, this._bstackOptions); + if (!_user || !_key) { + Logger.error('Could not start Test Observability : Missing authentication token'); + process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'false'; + + return; + } + this._user = _user; + this._key = _key; + CrashReporter.setCredentialsForCrashReportUpload(this._user, this._key); + CrashReporter.setConfigDetails(settings); + } + } + + extractDataFromResponse(response, requestData) { + const launchData = {}; + if (helper.isTestObservabilitySession()) { + const [jwt, buildHashedId, allowScreenshot] = testHubUtils.setTestObservabilityVariables(response.data); + if (jwt && buildHashedId) { + launchData['observability'] = {jwt, buildHashedId, allowScreenshot}; + process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'true'; + } else { + launchData['observability'] = {}; + process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'false'; + } + } else { + process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'false'; + } + if (helper.isAccessibilitySession()) { + const [authToken, buildHashedId] = testHubUtils.setAccessibilityVariables(response.data, requestData); + if (authToken && buildHashedId) { + launchData['accessibility'] = {authToken, buildHashedId}; + process.env.BROWSERSTACK_ACCESSIBILITY = 'true'; + } else { + launchData['accessibility'] = {}; + process.env.BROWSERSTACK_ACCESSIBILITY = 'false'; + } + } else { + process.env.BROWSERSTACK_ACCESSIBILITY = 'false'; + } + + if (testHubUtils.shouldProcessEventForTestHub()) { + testHubUtils.setTestHubCommonMetaInfo(response.data); + } + + return launchData; + } + + async stopTestHub() { + if (testHubUtils.shouldProcessEventForTestHub()) { + if (process.env.BROWSERSTACK_RERUN === 'true' && process.env.BROWSERSTACK_RERUN_TESTS) { + await helper.deleteRerunFile(); + } + try { + await this.stopBuildUpstream(); + if (process.env.BS_TESTOPS_BUILD_HASHED_ID) { + Logger.info(`\nVisit https://observability.browserstack.com/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID} to view build report, insights, and many more debugging information all at one place!\n`); + } + } catch (error) { + Logger.error(`Something went wrong in stopping build session for test observability - ${error}`); + } + } + } + + async stopBuildUpstream () { + if (!process.env.BROWSERSTACK_TESTHUB_JWT || !process.env.BROWSERSTACK_TESTHUB_UUID) { + Logger.info('[STOP_BUILD] Missing Authentication Token/ Build ID'); + + return { + status: 'error', + message: 'Token/buildID is undefined, build creation might have failed' + }; + } + const data = { + 'finished_at': new Date().toISOString() + }; + const config = { + headers: { + 'Authorization': `Bearer ${process.env.BROWSERSTACK_TESTHUB_JWT}`, + 'Content-Type': 'application/json', + 'X-BSTACK-TESTOPS': 'true' + } + }; + await helper.uploadPending(); + await helper.shutDownRequestHandler(); + try { + const response = await makeRequest('PUT', `api/v1/builds/${process.env.BROWSERSTACK_TESTHUB_UUID}/stop`, data, config, constants.API_URL, false); + if (response.data?.error) { + throw {message: response.data.error}; + } else { + return { + status: 'success', + message: '' + }; + } + } catch (error) { + if (error.response) { + Logger.error(`Exception in stopBuildUpstream request to TestHub : ${error.response.status} ${error.response.statusText} ${JSON.stringify(error.response.data)}`); + } else { + Logger.error(`Exception in stopBuildUpstream request to TestHub : ${error.message || error}`); + } + + return { + status: 'error', + message: error + }; + } + } +} + +module.exports = TestHubHandler; diff --git a/src/testHub/utils.js b/src/testHub/utils.js new file mode 100644 index 0000000..eb34774 --- /dev/null +++ b/src/testHub/utils.js @@ -0,0 +1,196 @@ +const helper = require('../utils/helper'); +const logger = require('../utils/logger'); +const constants = require('../utils/constants'); +const scripts = require('../utils/scripts'); +const AccessibilityAutomation = require('../accessibilityAutomation'); + +exports.getFrameworkDetails = (testRunner) => { + return { + frameworkName: helper.getFrameworkName(testRunner), + frameworkVersion: helper.getPackageVersion('nightwatch'), + sdkVersion: helper.getAgentVersion(), + language: 'javascript', + testFramework: { + name: 'selenium', + version: helper.getPackageVersion('selenium-webdriver') + } + }; +}; + +exports.getProductMap = () => { + return { + 'observability': helper.isTestObservabilitySession(), + 'accessibility': helper.isAccessibilitySession(), + 'percy': false, + 'automate': helper.isBrowserstackInfra(), + 'app_automate': false + }; +}; + +exports.shouldProcessEventForTestHub = () => { + // Do not run build Unification for accessibility + if (!helper.isCucumberTestSuite()) {return false}; + + return helper.isTestObservabilitySession() || helper.isAccessibilitySession(); +}; + +exports.shouldUploadEventToTestHub = (eventType) => { + if (!helper.isCucumberTestSuite()) {return true}; + + if (helper.isAccessibilitySession() && !helper.isTestObservabilitySession()) { + if (['TestRunFinished', 'TestRunStarted'].includes(eventType)) { + return true; + } + + return false; + } + + return helper.isTestObservabilitySession() || helper.isAccessibilitySession(); +}; + +exports.setTestObservabilityVariables = (responseData) => { + if (!responseData.observability) { + exports.handleErrorForObservability(); + + return [null, null, null]; + } + + if (!responseData.observability.success) { + exports.handleErrorForObservability(responseData.observability); + + return [null, null, null]; + } + + if (helper.isTestObservabilitySession()) { + process.env.BS_TESTOPS_BUILD_COMPLETED = 'true'; + if (responseData.jwt) { + process.env.BS_TESTOPS_JWT = responseData.jwt; + } + if (responseData.build_hashed_id) { + process.env.BS_TESTOPS_BUILD_HASHED_ID = responseData.build_hashed_id; + } + if (responseData.observability.options) { + process.env.BS_TESTOPS_ALLOW_SCREENSHOTS = responseData.observability.options.allow_screenshots.toString(); + } + logger.info(`Build Created Successfully with hashed id: ${responseData.build_hashed_id}`); + + return [responseData.jwt, responseData.build_hashed_id, process.env.BS_TESTOPS_ALLOW_SCREENSHOTS]; + } + + return [null, null, null]; +}; + +exports.setAccessibilityVariables = (responseData, requestData) => { + if (!responseData.accessibility) { + exports.handleErrorForAccessibility(); + + return [null, null]; + } + + if (!responseData.accessibility.success) { + exports.handleErrorForAccessibility(responseData.accessibility); + + return [null, null]; + } + + if (responseData?.accessibility?.options) { + const {accessibilityToken, scannerVersion} = jsonifyAccessibilityArray(responseData.accessibility.options.capabilities, 'name', 'value'); + const scriptsJson = {'scripts': jsonifyAccessibilityArray(responseData.accessibility.options.scripts, 'name', 'command')}; + scriptsJson['commands'] = responseData.accessibility.options.commandsToWrap.commands; + scripts.parseFromJson(scriptsJson); + scripts.toJson(); + const accessibilityOptions = requestData.accessibility; + accessibilityOptions.scannerVersion = scannerVersion; + process.env.BROWSERSTACK_ACCESSIBILITY_OPTIONS = JSON.stringify(accessibilityOptions); + process.env.BS_A11Y_JWT = accessibilityToken; + logger.info(`Build Created Successfully with hashed id: ${responseData.build_hashed_id}`); + + return [accessibilityToken, responseData.build_hashed_id]; + } + + return [null, null]; +}; + +exports.handleErrorForObservability = (error) => { + process.env.BROWSERSTACK_TESTHUB_UUID = 'null'; + process.env.BROWSERSTACK_TESTHUB_JWT = 'null'; + process.env.BROWSERSTACK_TEST_OBSERVABILITY = 'false'; + 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, 'observability'); +}; + +exports.handleErrorForAccessibility = (error) => { + process.env.BROWSERSTACK_TESTHUB_UUID = 'null'; + process.env.BROWSERSTACK_TESTHUB_JWT = 'null'; + process.env.BROWSERSTACK_TEST_ACCESSIBILITY_YML = 'false'; + process.env.BROWSERSTACK_TEST_ACCESSIBILITY_PLATFORM = 'false'; + exports.logBuildError(error, 'accessibility'); +}; + +exports.logBuildError = (error, product = '') => { + if (error === undefined) { + logger.error(`${product.toUpperCase()} Build creation failed`); + + return; + } + + for (const errorJson of error.errors) { + const errorType = errorJson.key; + const errorMessage = errorJson.message; + if (errorMessage) { + switch (errorType) { + case constants.TESTHUB_ERROR.INVALID_CREDENTIALS: + logger.error(errorMessage); + break; + case constants.TESTHUB_ERROR.ACCESS_DENIED: + logger.info(errorMessage); + break; + case constants.TESTHUB_ERROR.DEPRECATED: + logger.error(errorMessage); + break; + default: + logger.error(errorMessage); + } + } + } +}; + +// 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.setTestHubCommonMetaInfo = (responseData) => { + if (responseData.jwt) { + process.env.BROWSERSTACK_TESTHUB_JWT = responseData.jwt; + } + if (responseData.build_hashed_id) { + process.env.BROWSERSTACK_TESTHUB_UUID = responseData.build_hashed_id; + }; +}; + +exports.beforeEachCucumberTest = async (testCase) => { + console.log(`TEST-OBSERVABILITY-PID-TESTCASE-MAPPING-${testCase.testCaseStartedId}`); + + if (helper.isAccessibilitySession()) { + helper.modifySeleniumCommands(); + helper.modifyNightwatchCommands(); + const testMeta = helper.getCucumberTestMetaData(testCase); + await AccessibilityAutomation.prototype.beforeEachExecution(testMeta); + } +}; + +exports.afterEachCucumberTest = async (testCase) => { + if (helper.isAccessibilitySession()) { + const testMeta = helper.getCucumberTestMetaData(testCase); + await AccessibilityAutomation.prototype.afterEachExecution(testMeta); + } +}; diff --git a/src/testObservability.js b/src/testObservability.js index c8f0499..e10e504 100644 --- a/src/testObservability.js +++ b/src/testObservability.js @@ -405,6 +405,9 @@ class TestObservability { vc_filepath: (this._gitMetadata && this._gitMetadata.root) ? path.relative(this._gitMetadata.root, feature.path) : null, framework: 'nightwatch', result: 'pending', + product_map: { + 'accessibility': helper.isAccessibilitySession() + }, meta: { feature: feature, scenario: scenario, diff --git a/src/utils/constants.js b/src/utils/constants.js index 4e54f42..024895d 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -8,6 +8,7 @@ exports.DEFAULT_WAIT_TIMEOUT_FOR_PENDING_UPLOADS = 5000; exports.DEFAULT_WAIT_INTERVAL_FOR_PENDING_UPLOADS = 100; exports.CUSTOM_REPORTER_CALLBACK_TIMEOUT = 3600000; exports.consoleHolder = Object.assign({}, console); +exports.TH_BUILD_API = 'api/v2/builds'; // Regex = TEST-OBSERVABILITY-PID-TESTCASE-MAPPING-ea78bf4a-d02b-40bc-8f52-7b53a4350b2c exports.PID_MAPPING_REGEX = /^TEST-OBSERVABILITY-PID-TESTCASE-MAPPING-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; @@ -18,3 +19,8 @@ exports.EVENTS = { SCREENSHOT: 'testObservability:screenshot' }; exports.ACCESSIBILITY_URL= 'https://accessibility.browserstack.com/api'; +exports.TESTHUB_ERROR = { + INVALID_CREDENTIALS: 'ERROR_INVALID_CREDENTIALS', + DEPRECATED: 'ERROR_SDK_DEPRECATED', + ACCESS_DENIED: 'ERROR_ACCESS_DENIED' +}; diff --git a/src/utils/helper.js b/src/utils/helper.js index 4ba5599..b388e16 100644 --- a/src/utils/helper.js +++ b/src/utils/helper.js @@ -14,6 +14,8 @@ const Logger = require('./logger'); const LogPatcher = require('./logPatcher'); const BSTestOpsPatcher = new LogPatcher({}); const sessions = {}; +const scripts = require('./scripts'); +const {shouldUploadEventToTestHub} = require('../testHub/utils'); console = {}; Object.keys(consoleHolder).forEach(method => { @@ -483,6 +485,8 @@ exports.uploadEventData = async (eventData) => { ['HookRunFinished']: 'Hook_End_Upload' }[eventData.event_type]; + if (!shouldUploadEventToTestHub(eventData.event_type)) { return } + if (process.env.BS_TESTOPS_JWT && process.env.BS_TESTOPS_JWT !== 'null') { requestQueueHandler.pending_test_uploads += 1; } @@ -763,3 +767,126 @@ exports.deepClone = (obj) => { exports.shouldSendLogs = () => { return exports.isTestObservabilitySession() && exports.isCucumberTestSuite(); }; + +exports.homedir = () => { + if (typeof os.homedir === 'function') {return os.homedir()} + + var env = process.env; + var home = env.HOME; + var user = env.LOGNAME || env.USER || env.LNAME || env.USERNAME; + + if (process.platform === 'win32') { + return env.USERPROFILE || env.HOMEDRIVE + env.HOMEPATH || home || null; + } + + if (process.platform === 'darwin') { + return home || (user ? '/Users/' + user : null); + } + + if (process.platform === 'linux') { + return home || (process.getuid() === 0 ? '/root' : (user ? '/home/' + user : null)); + } + + return home || null; +}; + +exports.isBrowserStackCommandExecutor = (parameters) => { + if (parameters && parameters.script && typeof parameters.script === 'string') { + return parameters.script.includes('browserstack_executor'); + } + + return false; +}; + +exports.modifySeleniumCommands = () => { + try { + let Executor = exports.requireModule('selenium-webdriver/lib/webdriver.js').WebDriver; + if (!Executor.prototype || !Executor.prototype.execute) { + Executor = exports.requireModule('selenium-webdriver/lib/http.js').Executor; + } + + if (Executor.prototype && Executor.prototype.execute) { + const originalExecute = Executor.prototype.execute; + Logger.debug('Modifying webdriver execute'); + + Executor.prototype.execute = async function() { + exports.performAccessibilityScan({commandType: 'selenium', arguments}); + + return originalExecute.apply(this, arguments); + }; + } + } catch (err) { + Logger.debug('Unable to find executor class ' + err); + } +}; + +exports.modifyNightwatchCommands = () => { + const modifyClientCommand = (commandMeta) => { + try { + const CommandClass = exports.requireModule(commandMeta.packagePath); + if (CommandClass?.prototype?.performAction) { + const originalCommandClass = CommandClass.prototype.performAction; + + CommandClass.prototype.performAction = function(...args) { + exports.performAccessibilityScan({commandType: 'nightwatch', methodName: commandMeta.commandName}); + + return originalCommandClass.apply(this, ...args); + }; + } + } catch (err) { + Logger.debug(`Unable to find executor class from method ${commandMeta.commandName}, Error: ` + err); + } + }; + + const commandsToModify = [ + { + commandName: 'registerBasicAuth', + packagePath: 'nightwatch/lib/api/client-commands/registerBasicAuth.js' + }, + { + commandName: 'setDeviceDimensions', + packagePath: 'nightwatch/lib/api/client-commands/setDeviceDimensions.js' + }, + { + commandName: 'setGeolocation', + packagePath: 'nightwatch/lib/api/client-commands/setGeolocation.js' + } + ]; + commandsToModify.forEach(commandMeta => { + modifyClientCommand(commandMeta); + }); +}; + +exports.performAccessibilityScan = (data) => { + let shouldScanCommand = false; + let methodName = ''; + if (data.commandType === 'selenium') { + methodName = data.arguments[0].name_; + shouldScanCommand = !global.bstackAllyScanning && global.isAccessibilityPlatform && global.shouldScanTestForAccessibility && scripts.shouldWrapCommand(methodName) && !exports.isBrowserStackCommandExecutor(data.arguments[0].parameters_); + } else if (data.commandType === 'nightwatch') { + methodName = data.methodName; + shouldScanCommand = !global.bstackAllyScanning && global.isAccessibilityPlatform && global.shouldScanTestForAccessibility && scripts.shouldWrapCommand(methodName); + } + + try { + if (shouldScanCommand) { + global.bstackAllyScanning = true, + Logger.debug(`Performing scan for ${methodName}`); + browser.executeAsyncScript(scripts.performScan); + } + } catch (er) { + Logger.debug(`Failed to perform scan for ${methodName}, Error: ${er}`); + } + global.bstackAllyScanning = false; +}; + +exports.getCucumberTestMetaData = (testCase) => { + return { + testcase: testCase.pickle?.name, + metadata: { + name: testCase.gherkinDocument?.feature?.name, + modulePath: path.relative(process.cwd(), testCase.pickle?.uri), + tags: testCase.pickle?.tags?.map(({name}) => (name)) + } + }; +}; diff --git a/src/utils/scripts.js b/src/utils/scripts.js new file mode 100644 index 0000000..3643038 --- /dev/null +++ b/src/utils/scripts.js @@ -0,0 +1,61 @@ +const path = require('path'); +const fs = require('fs'); + +const os = require('os'); + +class Scripts { + constructor() { + this.performScan = null; + this.getResults = null; + this.getResultsSummary = null; + this.saveTestResults = null; + + this.browserstackFolderPath = path.join(os.homedir(), '.browserstack'); + this.commandsPath = path.join(this.browserstackFolderPath, 'commands.json'); + + this.fromJson(); + } + + parseFromJson(responseData) { + if (responseData.scripts) { + this.performScan = responseData.scripts.scan; + this.getResults = responseData.scripts.getResults; + this.getResultsSummary = responseData.scripts.getResultsSummary; + this.saveTestResults = responseData.scripts.saveResults; + } + + this.commandsToWrap = responseData.commands; + } + + shouldWrapCommand(method) { + try { + return this.commandsToWrap.findIndex(el => el.name.toLowerCase() === method.toLowerCase()) !== -1; + } catch { /* empty */ } + + return false; + } + + toJson() { + if (!fs.existsSync(this.browserstackFolderPath)){ + fs.mkdirSync(this.browserstackFolderPath); + } + + fs.writeFileSync(this.commandsPath, JSON.stringify({ + scripts: { + scan: this.performScan, + getResults: this.getResults, + getResultsSummary: this.getResultsSummary, + saveResults: this.saveTestResults + }, + commands: this.commandsToWrap + })); + } + + fromJson() { + if (fs.existsSync(this.commandsPath)) { + this.parseFromJson(require(this.commandsPath)); + } + } +} + +module.exports = new Scripts();