diff --git a/nightwatch/globals.js b/nightwatch/globals.js index 1941ac2..2e009f4 100644 --- a/nightwatch/globals.js +++ b/nightwatch/globals.js @@ -77,7 +77,7 @@ module.exports = { registerEventHandlers(eventBroadcaster) { eventBroadcaster.on('TestCaseStarted', async (args) => { - if (!helper.isTestObservabilitySession()) { + if (!helper.isTestObservabilitySession() && !helper.isAccessibilitySession()) { return; } try { @@ -133,7 +133,7 @@ module.exports = { }); eventBroadcaster.on('TestCaseFinished', async (args) => { - if (!helper.isTestObservabilitySession()) { + if (!helper.isTestObservabilitySession() && !helper.isAccessibilitySession()) { return; } try { @@ -147,7 +147,9 @@ module.exports = { if (testMetaData) { delete _tests[testCaseId]; testMetaData.finishedAt = new Date().toISOString(); - await testObservability.sendTestRunEventForCucumber(reportData, gherkinDocument, pickleData, 'TestRunFinished', testMetaData, args); + if (helper.isTestObservabilitySession()) { + await testObservability.sendTestRunEventForCucumber(reportData, gherkinDocument, pickleData, 'TestRunFinished', testMetaData, args); + } } } catch (error) { CrashReporter.uploadCrashReport(error.message, error.stack); @@ -156,7 +158,7 @@ module.exports = { }); eventBroadcaster.on('TestStepStarted', async (args) => { - if (!helper.isTestObservabilitySession()) { + if (!helper.isTestObservabilitySession() && !helper.isAccessibilitySession()) { return; } try { @@ -189,7 +191,7 @@ module.exports = { }); eventBroadcaster.on('TestStepFinished', async (args) => { - if (!helper.isTestObservabilitySession()) { + if (!helper.isTestObservabilitySession() && !helper.isAccessibilitySession()) { return; } try { @@ -298,7 +300,6 @@ module.exports = { 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) { @@ -329,6 +330,9 @@ module.exports = { Logger.error(`Could not configure or launch accessibility automation - ${error}`); } + if ((helper.isAccessibilitySession() || helper.isTestObservabilitySession()) && helper.isCucumberTestSuite(settings)) { + settings.test_runner.options['require'] = path.resolve(__dirname, 'observabilityLogPatcherHook.js'); + } }, async after() { @@ -347,7 +351,6 @@ module.exports = { } catch (error) { Logger.error(`Something went wrong in stopping build session for test observability - ${error}`); } - process.exit(); } if (helper.isAccessibilitySession()){ try { @@ -355,13 +358,18 @@ module.exports = { } 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() }; + if (helper.isAccessibilitySession()) { + helper.modifySeleniumCommands(); + helper.modifyNightwatchCommands(); + browser.getAccessibilityResults = () => { return accessibilityAutomation.getAccessibilityResults() }; + browser.getAccessibilityResultsSummary = () => { return accessibilityAutomation.getAccessibilityResultsSummary() }; + } + // await accessibilityAutomation.beforeEachExecution(browser); }, diff --git a/nightwatch/observabilityLogPatcherHook.js b/nightwatch/observabilityLogPatcherHook.js index d39e071..ab0bef3 100644 --- a/nightwatch/observabilityLogPatcherHook.js +++ b/nightwatch/observabilityLogPatcherHook.js @@ -1,8 +1,19 @@ try { - const {Before} = require('@cucumber/cucumber'); - - Before((testCase) => { + const {Before, After} = require('@cucumber/cucumber'); + const nightwatchPluginHelper = require('@nightwatch/browserstack/src/utils/helper'); + const AccessibilityAutomation = require('@nightwatch/browserstack/src/accessibilityAutomation'); + + Before(async (testCase) => { + nightwatchPluginHelper.modifySeleniumCommands(); + nightwatchPluginHelper.modifyNightwatchCommands(); console.log(`TEST-OBSERVABILITY-PID-TESTCASE-MAPPING-${testCase.testCaseStartedId}`); + const testMeta = nightwatchPluginHelper.getCucumberTestMetaData(testCase); + await AccessibilityAutomation.prototype.beforeEachExecution(testMeta); + }); + + After(async (testCase) => { + const testMeta = nightwatchPluginHelper.getCucumberTestMetaData(testCase); + await AccessibilityAutomation.prototype.afterEachExecution(testMeta); }); } catch (error) { /* empty */ } diff --git a/src/accessibilityAutomation.js b/src/accessibilityAutomation.js index af97fa0..2926c40 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,14 +408,19 @@ 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, + saveResults: shouldScanTestForAccessibility, testDetails: { name: testMetaData.testcase, testRunId: process.env.BS_A11Y_TEST_RUN_ID, @@ -435,35 +429,13 @@ class AccessibilityAutomation { }, 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.'); - } + 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/utils/helper.js b/src/utils/helper.js index 4ba5599..4056162 100644 --- a/src/utils/helper.js +++ b/src/utils/helper.js @@ -14,6 +14,7 @@ const Logger = require('./logger'); const LogPatcher = require('./logPatcher'); const BSTestOpsPatcher = new LogPatcher({}); const sessions = {}; +const scripts = require('./scripts'); console = {}; Object.keys(consoleHolder).forEach(method => { @@ -763,3 +764,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();