@nightwatch/browserstack
Advanced tools
@@ -263,3 +263,3 @@ const LocalTunnel = require('../src/local-tunnel'); | ||
| eventBroadcaster.on('TestRunStarted', async (test) => { | ||
| process.env.VALID_ALLY_PLATFORM = accessibilityAutomation.validateA11yCaps(browser); | ||
| process.env.VALID_ALLY_PLATFORM = process.env.BROWSERSTACK_APP_AUTOMATE ? accessibilityAutomation.validateAppA11yCaps(test.metadata.sessionCapabilities) : accessibilityAutomation.validateA11yCaps(browser); | ||
| await accessibilityAutomation.beforeEachExecution(test); | ||
@@ -361,3 +361,5 @@ if (testRunner !== 'cucumber'){ | ||
| accessibilityAutomation.commandWrapper(); | ||
| helper.patchBrowserTerminateCommand(); | ||
| if (!process.env.BROWSERSTACK_APP_AUTOMATE){ | ||
| helper.patchBrowserTerminateCommand(); | ||
| }; | ||
| } | ||
@@ -494,4 +496,10 @@ } catch (err){ | ||
| async beforeEach(settings) { | ||
| browser.getAccessibilityResults = () => { return accessibilityAutomation.getAccessibilityResults() }; | ||
| browser.getAccessibilityResultsSummary = () => { return accessibilityAutomation.getAccessibilityResultsSummary() }; | ||
| if (helper.isAppAccessibilitySession()){ | ||
| browser.getAccessibilityResults = () => { return accessibilityAutomation.getAppAccessibilityResults(browser) }; | ||
| browser.getAccessibilityResultsSummary = () => { return accessibilityAutomation.getAppAccessibilityResultsSummary(browser) }; | ||
| } else { | ||
| browser.getAccessibilityResults = () => { return accessibilityAutomation.getAccessibilityResults() }; | ||
| browser.getAccessibilityResultsSummary = () => { return accessibilityAutomation.getAccessibilityResultsSummary() }; | ||
| } | ||
| // await accessibilityAutomation.beforeEachExecution(browser); | ||
| }, | ||
@@ -537,3 +545,5 @@ | ||
| accessibilityAutomation.commandWrapper(); | ||
| helper.patchBrowserTerminateCommand(); | ||
| if (!process.env.BROWSERSTACK_APP_AUTOMATE){ | ||
| helper.patchBrowserTerminateCommand(); | ||
| }; | ||
| } | ||
@@ -540,0 +550,0 @@ } catch (err){ |
+1
-1
| { | ||
| "name": "@nightwatch/browserstack", | ||
| "version": "3.7.1", | ||
| "version": "3.8.0", | ||
| "description": "Nightwatch plugin for integration with browserstack.", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
| const path = require('path'); | ||
| const helper = require('./utils/helper'); | ||
| const Logger = require('./utils/logger'); | ||
| const {APP_ALLY_ENDPOINT, APP_ALLY_ISSUES_SUMMARY_ENDPOINT, APP_ALLY_ISSUES_ENDPOINT} = require('./utils/constants'); | ||
| const util = require('util'); | ||
@@ -165,2 +166,20 @@ const AccessibilityScripts = require('./scripts/accessibilityScripts'); | ||
| validateAppA11yCaps(capabilities = {}) { | ||
| /* Check if the current driver platform is eligible for AppAccessibility scan */ | ||
| if ( | ||
| capabilities?.platformName && | ||
| String(capabilities?.platformName).toLowerCase() === 'android' && | ||
| capabilities?.platformVersion && | ||
| parseInt(capabilities?.platformVersion?.toString()) < 11 | ||
| ) { | ||
| Logger.warn( | ||
| 'App Accessibility Automation tests are supported on OS version 11 and above for Android devices.' | ||
| ); | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
| async beforeEachExecution(testMetaData) { | ||
@@ -173,4 +192,10 @@ try { | ||
| this.currentTest.accessibilityScanStarted = true; | ||
| this._isAccessibilitySession = this.validateA11yCaps(browser); | ||
| this._isAppAccessibility = helper.isAppAccessibilitySession(); | ||
| if (this._isAppAccessibility) { | ||
| this._isAccessibilitySession = this.validateAppA11yCaps(testMetaData.metadata.sessionCapabilities); | ||
| } else { | ||
| this._isAccessibilitySession = this.validateA11yCaps(browser); | ||
| } | ||
| if (this.isAccessibilityAutomationSession() && browser && this._isAccessibilitySession) { | ||
@@ -272,6 +297,5 @@ try { | ||
| if (this.currentTest.shouldScanTestForAccessibility === false) { | ||
| Logger.info('Skipping Accessibility scan for this test as it\'s disabled.'); | ||
| return; | ||
| } | ||
| try { | ||
@@ -285,2 +309,12 @@ const browser = browserInstance; | ||
| } | ||
| if (helper.isAppAccessibilitySession()){ | ||
| const results = await browser.executeScript( | ||
| helper.formatString(AccessibilityScripts.performScan, JSON.stringify(this.getParamsForAppAccessibility(commandName))), | ||
| {} | ||
| ); | ||
| Logger.debug(util.inspect(results)); | ||
| return results; | ||
| } | ||
| AccessibilityAutomation.pendingAllyReq++; | ||
@@ -304,5 +338,75 @@ const results = await browser.executeAsyncScript(AccessibilityScripts.performScan, { | ||
| async getAppAccessibilityResults(browser) { | ||
| if (!helper.isBrowserstackInfra()) { | ||
| return []; | ||
| } | ||
| if (!helper.isAppAccessibilitySession()) { | ||
| Logger.warn('Not an Accessibility Automation session, cannot retrieve Accessibility results.'); | ||
| return []; | ||
| } | ||
| try { | ||
| const apiUrl = `${APP_ALLY_ENDPOINT}/${APP_ALLY_ISSUES_ENDPOINT}`; | ||
| const apiRespone = await this.getAppA11yResultResponse(apiUrl, browser, browser.sessionId); | ||
| const result = apiRespone?.data?.data?.issues; | ||
| Logger.debug(`Results: ${JSON.stringify(result)}`); | ||
| return result; | ||
| } catch (error) { | ||
| Logger.error('No accessibility results were found.'); | ||
| Logger.debug(`getAppAccessibilityResults Failed. Error: ${error}`); | ||
| return []; | ||
| } | ||
| } | ||
| async getAppAccessibilityResultsSummary(browser) { | ||
| if (!helper.isBrowserstackInfra()) { | ||
| return {}; | ||
| } | ||
| if (!helper.isAppAccessibilitySession()) { | ||
| Logger.warn('Not an Accessibility Automation session, cannot retrieve Accessibility results summary.'); | ||
| return {}; | ||
| } | ||
| try { | ||
| const apiUrl = `${APP_ALLY_ENDPOINT}/${APP_ALLY_ISSUES_SUMMARY_ENDPOINT}`; | ||
| const apiRespone = await this.getAppA11yResultResponse(apiUrl, browser, browser.sessionId); | ||
| const result = apiRespone?.data?.data?.summary; | ||
| Logger.debug(`Results Summary: ${JSON.stringify(result)}`); | ||
| return result; | ||
| } catch (error) { | ||
| Logger.error('No accessibility result summary were found.'); | ||
| Logger.debug(`getAppAccessibilityResultsSummary Failed. Error: ${error}`); | ||
| return {}; | ||
| } | ||
| } | ||
| async getAppA11yResultResponse(apiUrl, browser, sessionId){ | ||
| Logger.debug('Performing scan before getting results/results summary'); | ||
| await this.performScan(browser); | ||
| const upperTimeLimit = process.env.BSTACK_A11Y_POLLING_TIMEOUT ? Date.now() + parseInt(process.env.BSTACK_A11Y_POLLING_TIMEOUT) * 1000 : Date.now() + 30000; | ||
| const params = {test_run_uuid: process.env.TEST_RUN_UUID, session_id: sessionId, timestamp: Date.now()}; // Query params to pass | ||
| const header = {Authorization: `Bearer ${process.env.BSTACK_A11Y_JWT}`}; | ||
| const apiRespone = await helper.pollApi(apiUrl, params, header, upperTimeLimit); | ||
| Logger.debug(`Polling Result: ${JSON.stringify(apiRespone.message)}`); | ||
| return apiRespone; | ||
| } | ||
| async saveAccessibilityResults(browser, dataForExtension = {}) { | ||
| Logger.debug('Performing scan before saving results'); | ||
| await this.performScan(browser); | ||
| if (helper.isAppAccessibilitySession()){ | ||
| return; | ||
| } | ||
| const results = await browser.executeAsyncScript(AccessibilityScripts.saveTestResults, dataForExtension); | ||
@@ -344,3 +448,8 @@ | ||
| originalCommand.command = async function(...args) { | ||
| await accessibilityInstance.performScan(browser, commandName); | ||
| if ( | ||
| !commandName.includes('execute') || | ||
| !accessibilityInstance.shouldPatchExecuteScript(args.length ? args[0] : null) | ||
| ) { | ||
| await accessibilityInstance.performScan(browser, commandName); | ||
| } | ||
@@ -356,4 +465,26 @@ return originalCommandFn.apply(this, args); | ||
| } | ||
| shouldPatchExecuteScript(script) { | ||
| if (!script || typeof script !== 'string') { | ||
| return true; | ||
| } | ||
| return ( | ||
| script.toLowerCase().indexOf('browserstack_executor') !== -1 || | ||
| script.toLowerCase().indexOf('browserstack_accessibility_automation_script') !== -1 | ||
| ); | ||
| } | ||
| getParamsForAppAccessibility(commandName) { | ||
| return { | ||
| 'thTestRunUuid': process.env.TEST_RUN_UUID, | ||
| 'thBuildUuid': process.env.BROWSERSTACK_TESTHUB_UUID, | ||
| 'thJwtToken': process.env.BROWSERSTACK_TESTHUB_JWT, | ||
| 'authHeader': process.env.BSTACK_A11Y_JWT, | ||
| 'scanTimestamp': Date.now(), | ||
| 'method': commandName | ||
| }; | ||
| } | ||
| } | ||
| module.exports = AccessibilityAutomation; |
@@ -238,2 +238,3 @@ const os = require('os'); | ||
| } | ||
| process.env.IS_APP_ACCESSIBILITY = accessibilityAutomation.isAccessibilityAutomationSession() && helper.isAppAutomate(); | ||
@@ -240,0 +241,0 @@ } |
@@ -21,3 +21,5 @@ exports.BATCH_SIZE = 1000; | ||
| exports.ACCESSIBILITY_URL= 'https://accessibility.browserstack.com/api'; | ||
| exports.APP_ALLY_ENDPOINT = 'https://app-accessibility.browserstack.com/automate'; | ||
| exports.APP_ALLY_ISSUES_SUMMARY_ENDPOINT ='api/v1/issues-summary'; | ||
| exports.APP_ALLY_ISSUES_ENDPOINT = 'api/v1/issues'; | ||
| // Maximum size of VCS info which is allowed | ||
@@ -24,0 +26,0 @@ exports.MAX_GIT_META_DATA_SIZE_IN_BYTES = 64 * 1024; |
+104
-0
@@ -20,2 +20,3 @@ const os = require('os'); | ||
| const {execSync} = require('child_process'); | ||
| const request = require('@cypress/request'); | ||
@@ -105,2 +106,6 @@ console = {}; | ||
| exports.isAppAccessibilitySession = () => { | ||
| return process.env.IS_APP_ACCESSIBILITY === 'true'; | ||
| }; | ||
| exports.isAccessibilityEnabled = (settings) => { | ||
@@ -1310,1 +1315,100 @@ if (process.argv.includes('--disable-accessibility')) {return false} | ||
| exports.formatString = (template, ...values) => { | ||
| let i = 0; | ||
| if (template === null) { | ||
| return ''; | ||
| } | ||
| return template.replace(/%s/g, () => { | ||
| const value = values[i++]; | ||
| return value !== null && value !== undefined ? value : ''; | ||
| }); | ||
| }; | ||
| exports.pollApi = async (url, params, headers, upperLimit, startTime = Date.now()) => { | ||
| params.timestamp = Math.round(Date.now() / 1000); | ||
| Logger.debug(`current timestamp ${params.timestamp}`); | ||
| try { | ||
| const queryString = new URLSearchParams(params).toString(); | ||
| const fullUrl = `${url}?${queryString}`; | ||
| const response = await new Promise((resolve, reject) => { | ||
| request({ | ||
| method: 'GET', | ||
| url: fullUrl, | ||
| headers: headers, | ||
| json: false | ||
| }, (error, response, body) => { | ||
| if (error) { | ||
| reject(error); | ||
| } else { | ||
| resolve(response); | ||
| } | ||
| }); | ||
| }); | ||
| const responseData = JSON.parse(response.body); | ||
| if (response.statusCode === 404) { | ||
| const nextPollTime = parseInt(response.headers?.next_poll_time, 10) * 1000; | ||
| Logger.debug(`nextPollTime: ${nextPollTime}`); | ||
| if (isNaN(nextPollTime)) { | ||
| Logger.warn('Invalid or missing `nextPollTime` header. Stopping polling.'); | ||
| return { | ||
| data: {}, | ||
| headers: response.headers || {}, | ||
| message: 'Invalid nextPollTime header value. Polling stopped.' | ||
| }; | ||
| } | ||
| // Stop polling if the upper time limit is reached | ||
| if (nextPollTime > upperLimit) { | ||
| Logger.warn('Polling stopped due to upper time limit.'); | ||
| return { | ||
| data: {}, | ||
| headers: response.headers || {}, | ||
| message: 'Polling stopped due to upper time limit.' | ||
| }; | ||
| } | ||
| const elapsedTime = Math.max(0, nextPollTime - Date.now()); | ||
| Logger.debug( | ||
| `elapsedTime ${elapsedTime} nextPollTimes ${nextPollTime} upperLimit ${upperLimit}` | ||
| ); | ||
| Logger.debug(`Polling for results again in ${elapsedTime}ms`); | ||
| // Wait for the specified time and poll again | ||
| await new Promise((resolve) => setTimeout(resolve, elapsedTime)); | ||
| return exports.pollApi(url, params, headers, upperLimit, startTime); | ||
| } | ||
| return { | ||
| data: responseData, | ||
| headers: response.headers, | ||
| message: 'Polling succeeded.' | ||
| }; | ||
| } catch (error) { | ||
| if (error.response) { | ||
| throw { | ||
| data: {}, | ||
| headers: {}, | ||
| message: error.response.body ? JSON.parse(error.response.body).message : 'Unknown error' | ||
| }; | ||
| } else { | ||
| Logger.error(`Unexpected error occurred: ${error}`); | ||
| return {data: {}, headers: {}, message: 'Unexpected error occurred.'}; | ||
| } | ||
| } | ||
| }; | ||
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 33 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Debug access
Supply chain riskUses debug, reflection and dynamic code execution features.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 32 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
181259
4.77%4411
4.77%152
9.35%