diff --git a/bin/accessibility-automation/constants.js b/bin/accessibility-automation/constants.js new file mode 100644 index 00000000..496667a9 --- /dev/null +++ b/bin/accessibility-automation/constants.js @@ -0,0 +1 @@ +exports.API_URL = 'https://accessibility.browserstack.com/api'; diff --git a/bin/accessibility-automation/cypress/index.js b/bin/accessibility-automation/cypress/index.js new file mode 100644 index 00000000..7a77d3d9 --- /dev/null +++ b/bin/accessibility-automation/cypress/index.js @@ -0,0 +1,140 @@ +/* Event listeners + custom commands for Cypress */ + +Cypress.on('test:before:run', () => { + try { + if (Cypress.env("IS_ACCESSIBILITY_EXTENSION_LOADED") !== "true") return + const extensionPath = Cypress.env("ACCESSIBILITY_EXTENSION_PATH") + + if (extensionPath !== undefined) { + new Promise((resolve, reject) => { + window.parent.addEventListener('A11Y_TAP_STARTED', () => { + resolve("A11Y_TAP_STARTED"); + }); + const e = new CustomEvent('A11Y_FORCE_START'); + window.parent.dispatchEvent(e); + }) + } + } catch {} + +}); + +Cypress.on('test:after:run', (attributes, runnable) => { + try { + if (Cypress.env("IS_ACCESSIBILITY_EXTENSION_LOADED") !== "true") return + const extensionPath = Cypress.env("ACCESSIBILITY_EXTENSION_PATH") + const isHeaded = Cypress.browser.isHeaded; + if (isHeaded && extensionPath !== undefined) { + + let shouldScanTestForAccessibility = true; + if (Cypress.env("INCLUDE_TAGS_FOR_ACCESSIBILITY") || Cypress.env("EXCLUDE_TAGS_FOR_ACCESSIBILITY")) { + + try { + let includeTagArray = []; + let excludeTagArray = []; + if (Cypress.env("INCLUDE_TAGS_FOR_ACCESSIBILITY")) { + includeTagArray = Cypress.env("INCLUDE_TAGS_FOR_ACCESSIBILITY").split(";") + } + if (Cypress.env("EXCLUDE_TAGS_FOR_ACCESSIBILITY")) { + excludeTagArray = Cypress.env("EXCLUDE_TAGS_FOR_ACCESSIBILITY").split(";") + } + + const fullTestName = attributes.title; + const excluded = excludeTagArray.some((exclude) => fullTestName.includes(exclude)); + const included = includeTagArray.length === 0 || includeTags.some((include) => fullTestName.includes(include)); + shouldScanTestForAccessibility = !excluded && included; + } catch (error) { + console.log("Error while validating test case for accessibility before scanning. Error : ", error); + } + } + 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 dataForExtension = { + "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 + } + }; + return new Promise((resolve, reject) => { + if (dataForExtension.saveResults) { + window.parent.addEventListener('A11Y_TAP_TRANSPORTER', (event) => { + resolve(event.detail); + }); + } + const e = new CustomEvent('A11Y_TEST_END', {detail: dataForExtension}); + window.parent.dispatchEvent(e); + if (dataForExtension.saveResults !== true ) + resolve(); + }); + } + + } catch {} +}); + +Cypress.Commands.add('getAccessibilityResultsSummary', () => { + try { + if (Cypress.env("IS_ACCESSIBILITY_EXTENSION_LOADED") !== "true") { + console.log(`Not a Accessibility Automation session, cannot retrieve Accessibility results.`); + return + } + return new Promise(function (resolve, reject) { + try{ + const e = new CustomEvent('A11Y_TAP_GET_RESULTS_SUMMARY'); + const fn = function (event) { + window.parent.removeEventListener('A11Y_RESULTS_SUMMARY_RESPONSE', fn); + resolve(event.detail.summary); + }; + window.parent.addEventListener('A11Y_RESULTS_SUMMARY_RESPONSE', fn); + window.parent.dispatchEvent(e); + } catch (err) { + console.log("No accessibility results summary was found."); + reject(err); + } + }); + } catch {} + +}); + +Cypress.Commands.add('getAccessibilityResults', () => { + try { + if (Cypress.env("IS_ACCESSIBILITY_EXTENSION_LOADED") !== "true") { + console.log(`Not a Accessibility Automation session, cannot retrieve Accessibility results.`); + return + } + return new Promise(function (resolve, reject) { + try{ + const e = new CustomEvent('A11Y_TAP_GET_RESULTS'); + const fn = function (event) { + window.parent.removeEventListener('A11Y_RESULTS_RESPONSE', fn); + resolve(event.detail.summary); + }; + window.parent.addEventListener('A11Y_RESULTS_RESPONSE', fn); + window.parent.dispatchEvent(e); + } catch (err) { + console.log("No accessibility results were found."); + reject(err); + } + }); + } catch {} + +}); + diff --git a/bin/accessibility-automation/helper.js b/bin/accessibility-automation/helper.js new file mode 100644 index 00000000..7059bb0c --- /dev/null +++ b/bin/accessibility-automation/helper.js @@ -0,0 +1,218 @@ +const logger = require("../helpers/logger").winstonLogger; +const { API_URL } = require('./constants'); +const utils = require('../helpers/utils'); +const fs = require('fs'); +const path = require('path'); +const request = require('request'); +const os = require('os'); +const glob = require('glob'); +const helper = require('../helpers/helper'); +const { CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS } = require('../helpers/constants'); +const supportFileContentMap = {} + +exports.checkAccessibilityPlatform = (user_config) => { + let accessibility = false; + try { + user_config.browsers.forEach(browser => { + if (browser.accessibility) { + accessibility = true; + } + }) + } catch {} + + return accessibility; +} + +exports.setAccessibilityCypressCapabilities = async (user_config, accessibilityResponse) => { + if (utils.isUndefined(user_config.run_settings.accessibilityOptions)) { + user_config.run_settings.accessibilityOptions = {} + } + user_config.run_settings.accessibilityOptions["authToken"] = accessibilityResponse.data.accessibilityToken; + user_config.run_settings.accessibilityOptions["auth"] = accessibilityResponse.data.accessibilityToken; + user_config.run_settings.accessibilityOptions["scannerVersion"] = accessibilityResponse.data.scannerVersion; + user_config.run_settings.system_env_vars.push(`ACCESSIBILITY_AUTH=${accessibilityResponse.data.accessibilityToken}`) + user_config.run_settings.system_env_vars.push(`ACCESSIBILITY_SCANNERVERSION=${accessibilityResponse.data.scannerVersion}`) +} + +exports.isAccessibilitySupportedCypressVersion = (cypress_config_filename) => { + const extension = cypress_config_filename.split('.').pop(); + return CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS.includes(extension); +} + +exports.createAccessibilityTestRun = async (user_config, framework) => { + + try { + if (!this.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; + } + const userName = user_config["auth"]["username"]; + const accessKey = user_config["auth"]["access_key"]; + let settings = utils.isUndefined(user_config.run_settings.accessibilityOptions) ? {} : user_config.run_settings.accessibilityOptions + + const { + buildName, + projectName, + buildDescription + } = helper.getBuildDetails(user_config); + + const data = { + 'projectName': projectName, + 'buildName': buildName, + 'startTime': (new Date()).toISOString(), + 'description': buildDescription, + 'source': { + frameworkName: "Cypress", + frameworkVersion: helper.getPackageVersion('cypress', user_config), + sdkVersion: helper.getAgentVersion() + }, + 'settings': settings, + 'versionControl': await helper.getGitMetaData(), + 'ciInfo': helper.getCiInfo(), + 'hostInfo': { + hostname: os.hostname(), + platform: os.platform(), + type: os.type(), + version: os.version(), + arch: os.arch() + }, + 'browserstackAutomation': process.env.BROWSERSTACK_AUTOMATION === 'true' + }; + + const config = { + auth: { + user: userName, + pass: accessKey + }, + headers: { + 'Content-Type': 'application/json' + } + }; + + const response = await nodeRequest( + 'POST', 'test_runs', data, config, API_URL + ); + if(!utils.isUndefined(response.data)) { + process.env.BS_A11Y_JWT = response.data.data.accessibilityToken; + process.env.BS_A11Y_TEST_RUN_ID = response.data.data.id; + } + if (process.env.BS_A11Y_JWT) { + process.env.BROWSERSTACK_TEST_ACCESSIBILITY = 'true'; + } + logger.debug(`BrowserStack Accessibility Automation Test Run ID: ${response.data.data.id}`); + + this.setAccessibilityCypressCapabilities(user_config, response.data); + setAccessibilityEventListeners(); + helper.setBrowserstackCypressCliDependency(user_config); + + } catch (error) { + if (error.response) { + logger.error( + `Exception while creating test run for BrowserStack Accessibility Automation: ${ + error.response.status + } ${error.response.statusText} ${JSON.stringify(error.response.data)}` + ); + } else { + if(error.message === 'Invalid configuration passed.') { + logger.error( + `Exception while creating test run for BrowserStack Accessibility Automation: ${ + error.message || error.stack + }` + ); + for(const errorkey of error.errors){ + logger.error(errorkey.message); + } + + } else { + logger.error( + `Exception while creating test run for BrowserStack Accessibility Automation: ${ + error.message || error.stack + }` + ); + } + // since create accessibility session failed + process.env.BROWSERSTACK_TEST_ACCESSIBILITY = 'false'; + user_config.run_settings.accessibility = false; + } + } +} + +const nodeRequest = (type, url, data, config) => { + return new Promise(async (resolve, reject) => { + const options = {...config,...{ + method: type, + url: `${API_URL}/${url}`, + body: data, + json: config.headers['Content-Type'] === 'application/json', + }}; + + request(options, function callback(error, response, body) { + if(error) { + logger.info("error in nodeRequest", error); + reject(error); + } else if(!(response.statusCode == 201 || response.statusCode == 200)) { + logger.info("response.statusCode in nodeRequest", response.statusCode); + reject(response && response.body ? response.body : `Received response from BrowserStack Server with status : ${response.statusCode}`); + } else { + try { + if(typeof(body) !== 'object') body = JSON.parse(body); + } catch(e) { + if(!url.includes('/stop')) { + reject('Not a JSON response from BrowserStack Server'); + } + } + resolve({ + data: body + }); + } + }); + }); +} + +exports.supportFileCleanup = () => { + logger.debug("Cleaning up support file changes added for accessibility. ") + Object.keys(supportFileContentMap).forEach(file => { + try { + fs.writeFileSync(file, supportFileContentMap[file], {encoding: 'utf-8'}); + } catch(e) { + logger.debug(`Error while replacing file content for ${file} with it's original content with error : ${e}`, true, e); + } + }); +} + +const getAccessibilityCypressCommandEventListener = () => { + return ( + `require('browserstack-cypress-cli/bin/accessibility-automation/cypress');` + ); +} + +const setAccessibilityEventListeners = () => { + try { + const cypressCommandEventListener = getAccessibilityCypressCommandEventListener(); + glob(process.cwd() + '/cypress/support/*.js', {}, (err, files) => { + if(err) return logger.debug('EXCEPTION IN BUILD START EVENT : Unable to parse cypress support files'); + files.forEach(file => { + try { + if(!file.includes('commands.js')) { + const defaultFileContent = fs.readFileSync(file, {encoding: 'utf-8'}); + + if(!defaultFileContent.includes(cypressCommandEventListener)) { + let newFileContent = defaultFileContent + + '\n' + + cypressCommandEventListener + + '\n' + fs.writeFileSync(file, newFileContent, {encoding: 'utf-8'}); + supportFileContentMap[file] = defaultFileContent; + } + } + } catch(e) { + logger.debug(`Unable to modify file contents for ${file} to set event listeners with error ${e}`, true, e); + } + }); + }); + } catch(e) { + logger.debug(`Unable to parse support files to set event listeners with error ${e}`, true, e); + } +} diff --git a/bin/accessibility-automation/plugin/index.js b/bin/accessibility-automation/plugin/index.js new file mode 100644 index 00000000..0aecbf2c --- /dev/null +++ b/bin/accessibility-automation/plugin/index.js @@ -0,0 +1,43 @@ + +const path = require("node:path"); + +const browserstackAccessibility = (on, config) => { + let browser_validation = true; + on('before:browser:launch', (browser = {}, launchOptions) => { + try { + + if (process.env.ACCESSIBILITY_EXTENSION_PATH !== undefined) { + if (browser.name !== 'chrome') { + console.log(`Accessibility Automation will run only on Chrome browsers.`); + browser_validation = false; + } + if (browser.name === 'chrome' && browser.majorVersion <= 94) { + console.log(`Accessibility Automation will run only on Chrome browser version greater than 94.`); + browser_validation = false; + } + if (browser.isHeadless === true) { + console.log(`Accessibility Automation will not run on legacy headless mode. Switch to new headless mode or avoid using headless mode.`); + browser_validation = false; + } + if (browser_validation) { + const ally_path = path.dirname(process.env.ACCESSIBILITY_EXTENSION_PATH) + launchOptions.extensions.push(ally_path); + return launchOptions + } + } + } catch(err) {} + + }) + 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.IS_ACCESSIBILITY_EXTENSION_LOADED = browser_validation.toString() + + config.env.INCLUDE_TAGS_FOR_ACCESSIBILITY = process.env.ACCESSIBILITY_INCLUDETAGSINTESTINGSCOPE + config.env.EXCLUDE_TAGS_FOR_ACCESSIBILITY = process.env.ACCESSIBILITY_EXCLUDETAGSINTESTINGSCOPE + + return config; +} + +module.exports = browserstackAccessibility; diff --git a/bin/commands/runs.js b/bin/commands/runs.js index dc91679e..e198d291 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -30,6 +30,13 @@ const { printBuildLink } = require('../testObservability/helper/helper'); + +const { + createAccessibilityTestRun, + checkAccessibilityPlatform, + supportFileCleanup +} = require('../accessibility-automation/helper'); + module.exports = function run(args, rawArgs) { markBlockStart('preBuild'); @@ -57,6 +64,8 @@ module.exports = function run(args, rawArgs) { Set testObservability & browserstackAutomation flags */ const [isTestObservabilitySession, isBrowserstackInfra] = setTestObservabilityFlags(bsConfig); + const checkAccessibility = checkAccessibilityPlatform(bsConfig); + const isAccessibilitySession = bsConfig.run_settings.accessibility || checkAccessibility; utils.setUsageReportingFlag(bsConfig, args.disableUsageReporting); @@ -131,6 +140,10 @@ module.exports = function run(args, rawArgs) { // add cypress dependency if missing utils.setCypressNpmDependency(bsConfig); + + if (isAccessibilitySession && isBrowserstackInfra) { + await createAccessibilityTestRun(bsConfig); + } } const { packagesInstalled } = !isBrowserstackInfra ? false : await packageInstaller.packageSetupAndInstaller(bsConfig, config.packageDirName, {markBlockStart, markBlockEnd}); @@ -232,6 +245,9 @@ module.exports = function run(args, rawArgs) { markBlockEnd('zip.zipUpload'); markBlockEnd('zip'); + if (process.env.BROWSERSTACK_TEST_ACCESSIBILITY === 'true') { + supportFileCleanup(); + } // Create build //setup Local Testing markBlockStart('localSetup'); diff --git a/bin/helpers/capabilityHelper.js b/bin/helpers/capabilityHelper.js index 6ea88ec9..968605cf 100644 --- a/bin/helpers/capabilityHelper.js +++ b/bin/helpers/capabilityHelper.js @@ -121,6 +121,10 @@ const caps = (bsConfig, zip) => { logger.info(`Running your tests in headless mode. Use --headed arg to run in headful mode.`); } + if (process.env.BROWSERSTACK_TEST_ACCESSIBILITY === 'true') { + bsConfig.run_settings["accessibilityPlatforms"] = getAccessibilityPlatforms(bsConfig); + } + // send run_settings as is for other capabilities obj.run_settings = JSON.stringify(bsConfig.run_settings); } @@ -138,9 +142,22 @@ const caps = (bsConfig, zip) => { if (obj.parallels) logger.info(`Parallels limit specified: ${obj.parallels}`); var data = JSON.stringify(obj); + resolve(data); }) } +const getAccessibilityPlatforms = (bsConfig) => { + const browserList = bsConfig.browsers; + const accessibilityPlatforms = Array(browserList.length).fill(false); + let rootLevelAccessibility = false; + if (!Utils.isUndefined(bsConfig.run_settings.accessibility)) { + rootLevelAccessibility = bsConfig.run_settings.accessibility.toString() === 'true' + } + browserList.forEach((browserDetails, idx) => { + accessibilityPlatforms[idx] = (browserDetails.accessibility === undefined) ? rootLevelAccessibility : browserDetails.accessibility + }); + return accessibilityPlatforms; +} const addCypressZipStartLocation = (runSettings) => { let resolvedHomeDirectoryPath = path.resolve(runSettings.home_directory); diff --git a/bin/helpers/helper.js b/bin/helpers/helper.js new file mode 100644 index 00000000..9f5112b5 --- /dev/null +++ b/bin/helpers/helper.js @@ -0,0 +1,305 @@ +/* Helper methods used by Accessibility and Observability */ + +const logger = require("../helpers/logger").winstonLogger; +const utils = require('../helpers/utils'); +const fs = require('fs'); +const path = require('path'); +const http = require('http'); +const https = require('https'); +const request = require('request'); +const gitLastCommit = require('git-last-commit'); +const { v4: uuidv4 } = require('uuid'); +const os = require('os'); +const { promisify } = require('util'); +const getRepoInfo = require('git-repo-info'); +const gitconfig = require('gitconfiglocal'); +const { spawn, execSync } = require('child_process'); +const glob = require('glob'); +const pGitconfig = promisify(gitconfig); +const CrashReporter = require('../testObservability/crashReporter'); + +exports.debug = (text, shouldReport = false, throwable = null) => { + if (process.env.BROWSERSTACK_OBSERVABILITY_DEBUG === "true" || process.env.BROWSERSTACK_OBSERVABILITY_DEBUG === "1") { + logger.info(`[ OBSERVABILITY ] ${text}`); + } + if(shouldReport) { + CrashReporter.getInstance().uploadCrashReport(text, throwable ? throwable && throwable.stack : null); + } +} + +exports.getFileSeparatorData = () => { + return /^win/.test(process.platform) ? "\\" : "/"; +} + +exports.findGitConfig = (filePath) => { + const fileSeparator = exports.getFileSeparatorData(); + if(filePath == null || filePath == '' || filePath == fileSeparator) { + return null; + } + try { + fs.statSync(filePath + fileSeparator + '.git' + fileSeparator + 'config'); + return filePath; + } catch(e) { + let parentFilePath = filePath.split(fileSeparator); + parentFilePath.pop(); + return exports.findGitConfig(parentFilePath.join(fileSeparator)); + } +} + +let packages = {}; +exports.getPackageVersion = (package_, bsConfig = null) => { + if(packages[package_]) return packages[package_]; + let packageVersion; + /* Try to find version from module path */ + try { + packages[package_] = this.requireModule(`${package_}/package.json`).version; + logger.info(`Getting ${package_} package version from module path = ${packages[package_]}`); + packageVersion = packages[package_]; + } catch(e) { + exports.debug(`Unable to find package ${package_} at module path with error ${e}`); + } + + /* Read package version from npm_dependencies in browserstack.json file if present */ + if(utils.isUndefined(packageVersion) && bsConfig && (process.env.BROWSERSTACK_AUTOMATION == "true" || process.env.BROWSERSTACK_AUTOMATION == "1")) { + const runSettings = bsConfig.run_settings; + if (runSettings && runSettings.npm_dependencies !== undefined && + Object.keys(runSettings.npm_dependencies).length !== 0 && + typeof runSettings.npm_dependencies === 'object') { + if (package_ in runSettings.npm_dependencies) { + packages[package_] = runSettings.npm_dependencies[package_]; + logger.info(`Getting ${package_} package version from browserstack.json = ${packages[package_]}`); + packageVersion = packages[package_]; + } + } + } + + /* Read package version from project's package.json if present */ + const packageJSONPath = path.join(process.cwd(), 'package.json'); + if(utils.isUndefined(packageVersion) && fs.existsSync(packageJSONPath)) { + const packageJSONContents = require(packageJSONPath); + if(packageJSONContents.devDependencies && !utils.isUndefined(packageJSONContents.devDependencies[package_])) packages[package_] = packageJSONContents.devDependencies[package_]; + if(packageJSONContents.dependencies && !utils.isUndefined(packageJSONContents.dependencies[package_])) packages[package_] = packageJSONContents.dependencies[package_]; + logger.info(`Getting ${package_} package version from package.json = ${packages[package_]}`); + packageVersion = packages[package_]; + } + + return packageVersion; +} + +exports.getAgentVersion = () => { + let _path = path.join(__dirname, '../../package.json'); + if(fs.existsSync(_path)) + return require(_path).version; +} + +exports.getGitMetaData = () => { + return new Promise(async (resolve, reject) => { + try { + var info = getRepoInfo(); + if(!info.commonGitDir) { + logger.debug(`Unable to find a Git directory`); + exports.debug(`Unable to find a Git directory`); + resolve({}); + } + if(!info.author && exports.findGitConfig(process.cwd())) { + /* commit objects are packed */ + gitLastCommit.getLastCommit(async (err, commit) => { + if(err) { + logger.debug(`Exception in populating Git Metadata with error : ${err}`, true, err); + exports.debug(`Exception in populating Git Metadata with error : ${err}`, true, err); + return resolve({}); + } + try { + info["author"] = info["author"] || `${commit["author"]["name"].replace(/[“]+/g, '')} <${commit["author"]["email"].replace(/[“]+/g, '')}>`; + info["authorDate"] = info["authorDate"] || commit["authoredOn"]; + info["committer"] = info["committer"] || `${commit["committer"]["name"].replace(/[“]+/g, '')} <${commit["committer"]["email"].replace(/[“]+/g, '')}>`; + info["committerDate"] = info["committerDate"] || commit["committedOn"]; + info["commitMessage"] = info["commitMessage"] || commit["subject"]; + + const { remote } = await pGitconfig(info.commonGitDir); + const remotes = Object.keys(remote).map(remoteName => ({name: remoteName, url: remote[remoteName]['url']})); + resolve({ + "name": "git", + "sha": info["sha"], + "short_sha": info["abbreviatedSha"], + "branch": info["branch"], + "tag": info["tag"], + "committer": info["committer"], + "committer_date": info["committerDate"], + "author": info["author"], + "author_date": info["authorDate"], + "commit_message": info["commitMessage"], + "root": info["root"], + "common_git_dir": info["commonGitDir"], + "worktree_git_dir": info["worktreeGitDir"], + "last_tag": info["lastTag"], + "commits_since_last_tag": info["commitsSinceLastTag"], + "remotes": remotes + }); + } catch(e) { + exports.debug(`Exception in populating Git Metadata with error : ${e}`, true, e); + logger.debug(`Exception in populating Git Metadata with error : ${e}`, true, e); + return resolve({}); + } + }, {dst: exports.findGitConfig(process.cwd())}); + } else { + const { remote } = await pGitconfig(info.commonGitDir); + const remotes = Object.keys(remote).map(remoteName => ({name: remoteName, url: remote[remoteName]['url']})); + resolve({ + "name": "git", + "sha": info["sha"], + "short_sha": info["abbreviatedSha"], + "branch": info["branch"], + "tag": info["tag"], + "committer": info["committer"], + "committer_date": info["committerDate"], + "author": info["author"], + "author_date": info["authorDate"], + "commit_message": info["commitMessage"], + "root": info["root"], + "common_git_dir": info["commonGitDir"], + "worktree_git_dir": info["worktreeGitDir"], + "last_tag": info["lastTag"], + "commits_since_last_tag": info["commitsSinceLastTag"], + "remotes": remotes + }); + } + } catch(err) { + exports.debug(`Exception in populating Git metadata with error : ${err}`, true, err); + logger.debug(`Exception in populating Git metadata with error : ${err}`, true, err); + resolve({}); + } + }) +} +exports.getCiInfo = () => { + var env = process.env; + // Jenkins + if ((typeof env.JENKINS_URL === "string" && env.JENKINS_URL.length > 0) || (typeof env.JENKINS_HOME === "string" && env.JENKINS_HOME.length > 0)) { + return { + name: "Jenkins", + build_url: env.BUILD_URL, + job_name: env.JOB_NAME, + build_number: env.BUILD_NUMBER + } + } + // CircleCI + if (env.CI === "true" && env.CIRCLECI === "true") { + return { + name: "CircleCI", + build_url: env.CIRCLE_BUILD_URL, + job_name: env.CIRCLE_JOB, + build_number: env.CIRCLE_BUILD_NUM + } + } + // Travis CI + if (env.CI === "true" && env.TRAVIS === "true") { + return { + name: "Travis CI", + build_url: env.TRAVIS_BUILD_WEB_URL, + job_name: env.TRAVIS_JOB_NAME, + build_number: env.TRAVIS_BUILD_NUMBER + } + } + // Codeship + if (env.CI === "true" && env.CI_NAME === "codeship") { + return { + name: "Codeship", + build_url: null, + job_name: null, + build_number: null + } + } + // Bitbucket + if (env.BITBUCKET_BRANCH && env.BITBUCKET_COMMIT) { + return { + name: "Bitbucket", + build_url: env.BITBUCKET_GIT_HTTP_ORIGIN, + job_name: null, + build_number: env.BITBUCKET_BUILD_NUMBER + } + } + // Drone + if (env.CI === "true" && env.DRONE === "true") { + return { + name: "Drone", + build_url: env.DRONE_BUILD_LINK, + job_name: null, + build_number: env.DRONE_BUILD_NUMBER + } + } + // Semaphore + if (env.CI === "true" && env.SEMAPHORE === "true") { + return { + name: "Semaphore", + build_url: env.SEMAPHORE_ORGANIZATION_URL, + job_name: env.SEMAPHORE_JOB_NAME, + build_number: env.SEMAPHORE_JOB_ID + } + } + // GitLab + if (env.CI === "true" && env.GITLAB_CI === "true") { + return { + name: "GitLab", + build_url: env.CI_JOB_URL, + job_name: env.CI_JOB_NAME, + build_number: env.CI_JOB_ID + } + } + // Buildkite + if (env.CI === "true" && env.BUILDKITE === "true") { + return { + name: "Buildkite", + build_url: env.BUILDKITE_BUILD_URL, + job_name: env.BUILDKITE_LABEL || env.BUILDKITE_PIPELINE_NAME, + build_number: env.BUILDKITE_BUILD_NUMBER + } + } + // Visual Studio Team Services + if (env.TF_BUILD === "True") { + return { + name: "Visual Studio Team Services", + build_url: `${env.SYSTEM_TEAMFOUNDATIONSERVERURI}${env.SYSTEM_TEAMPROJECTID}`, + job_name: env.SYSTEM_DEFINITIONID, + build_number: env.BUILD_BUILDID + } + } + // if no matches, return null + return null; +} + +exports.getBuildDetails = (bsConfig) => { + let buildName = '', + projectName = '', + buildDescription = '', + buildTags = []; + + /* Pick from environment variables */ + buildName = process.env.BROWSERSTACK_BUILD_NAME || buildName; + projectName = process.env.BROWSERSTACK_PROJECT_NAME || projectName; + + /* Pick from run settings */ + buildName = buildName || bsConfig["run_settings"]["build_name"]; + projectName = projectName || bsConfig["run_settings"]["project_name"]; + if(!utils.isUndefined(bsConfig["run_settings"]["build_tag"])) buildTags = [...buildTags, bsConfig["run_settings"]["build_tag"]]; + + buildName = buildName || path.basename(path.resolve(process.cwd())); + + return { + buildName, + projectName, + buildDescription, + buildTags + }; +} + +exports.setBrowserstackCypressCliDependency = (bsConfig) => { + const runSettings = bsConfig.run_settings; + if (runSettings.npm_dependencies !== undefined && + 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"; + 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 961d220d..a8401418 100644 --- a/bin/helpers/utils.js +++ b/bin/helpers/utils.js @@ -558,6 +558,23 @@ exports.setSystemEnvs = (bsConfig) => { }); } + try { + const accessibilityOptions = bsConfig.run_settings.accessibilityOptions; + if (accessibilityOptions) { + Object.keys(accessibilityOptions).forEach(key => { + const a11y_env_key = `ACCESSIBILITY_${key.toUpperCase()}` + if (["includeTagsInTestingScope", "excludeTagsInTestingScope"].includes(key)) + envKeys[a11y_env_key] = accessibilityOptions[key].join(";") + else if (key === "includeIssueType") + envKeys[a11y_env_key] = JSON.stringify(accessibilityOptions.includeIssueType).replaceAll('"', "") + else + envKeys[a11y_env_key] = accessibilityOptions[key]; + }) + } + } catch (error) { + logger.error(`Error in adding accessibility configs ${error}`) + } + try { OBSERVABILITY_ENV_VARS.forEach(key => { envKeys[key] = process.env[key]; diff --git a/bin/testObservability/helper/helper.js b/bin/testObservability/helper/helper.js index 65494465..2588095d 100644 --- a/bin/testObservability/helper/helper.js +++ b/bin/testObservability/helper/helper.js @@ -3,11 +3,9 @@ const path = require('path'); const http = require('http'); const https = require('https'); const request = require('request'); -var gitLastCommit = require('git-last-commit'); const { v4: uuidv4 } = require('uuid'); const os = require('os'); const { promisify } = require('util'); -const getRepoInfo = require('git-repo-info'); const gitconfig = require('gitconfiglocal'); const { spawn, execSync } = require('child_process'); const glob = require('glob'); @@ -16,6 +14,8 @@ const pGitconfig = promisify(gitconfig); const logger = require("../../helpers/logger").winstonLogger; const utils = require('../../helpers/utils'); +const helper = require('../../helpers/helper'); + const CrashReporter = require('../crashReporter'); // Getting global packages path @@ -181,178 +181,6 @@ exports.findGitConfig = (filePath) => { } } -const getGitMetaData = () => { - return new Promise(async (resolve, reject) => { - try { - var info = getRepoInfo(); - if(!info.commonGitDir) { - exports.debug(`Unable to find a Git directory`); - resolve({}); - } - if(!info.author && exports.findGitConfig(process.cwd())) { - /* commit objects are packed */ - gitLastCommit.getLastCommit(async (err, commit) => { - if(err) { - exports.debug(`Exception in populating Git Metadata with error : ${err}`, true, err); - return resolve({}); - } - try { - info["author"] = info["author"] || `${commit["author"]["name"].replace(/[“]+/g, '')} <${commit["author"]["email"].replace(/[“]+/g, '')}>`; - info["authorDate"] = info["authorDate"] || commit["authoredOn"]; - info["committer"] = info["committer"] || `${commit["committer"]["name"].replace(/[“]+/g, '')} <${commit["committer"]["email"].replace(/[“]+/g, '')}>`; - info["committerDate"] = info["committerDate"] || commit["committedOn"]; - info["commitMessage"] = info["commitMessage"] || commit["subject"]; - - const { remote } = await pGitconfig(info.commonGitDir); - const remotes = Object.keys(remote).map(remoteName => ({name: remoteName, url: remote[remoteName]['url']})); - resolve({ - "name": "git", - "sha": info["sha"], - "short_sha": info["abbreviatedSha"], - "branch": info["branch"], - "tag": info["tag"], - "committer": info["committer"], - "committer_date": info["committerDate"], - "author": info["author"], - "author_date": info["authorDate"], - "commit_message": info["commitMessage"], - "root": info["root"], - "common_git_dir": info["commonGitDir"], - "worktree_git_dir": info["worktreeGitDir"], - "last_tag": info["lastTag"], - "commits_since_last_tag": info["commitsSinceLastTag"], - "remotes": remotes - }); - } catch(e) { - exports.debug(`Exception in populating Git Metadata with error : ${e}`, true, e); - return resolve({}); - } - }, {dst: exports.findGitConfig(process.cwd())}); - } else { - const { remote } = await pGitconfig(info.commonGitDir); - const remotes = Object.keys(remote).map(remoteName => ({name: remoteName, url: remote[remoteName]['url']})); - resolve({ - "name": "git", - "sha": info["sha"], - "short_sha": info["abbreviatedSha"], - "branch": info["branch"], - "tag": info["tag"], - "committer": info["committer"], - "committer_date": info["committerDate"], - "author": info["author"], - "author_date": info["authorDate"], - "commit_message": info["commitMessage"], - "root": info["root"], - "common_git_dir": info["commonGitDir"], - "worktree_git_dir": info["worktreeGitDir"], - "last_tag": info["lastTag"], - "commits_since_last_tag": info["commitsSinceLastTag"], - "remotes": remotes - }); - } - } catch(err) { - exports.debug(`Exception in populating Git metadata with error : ${err}`, true, err); - resolve({}); - } - }) -} - -const getCiInfo = () => { - var env = process.env; - // Jenkins - if ((typeof env.JENKINS_URL === "string" && env.JENKINS_URL.length > 0) || (typeof env.JENKINS_HOME === "string" && env.JENKINS_HOME.length > 0)) { - return { - name: "Jenkins", - build_url: env.BUILD_URL, - job_name: env.JOB_NAME, - build_number: env.BUILD_NUMBER - } - } - // CircleCI - if (env.CI === "true" && env.CIRCLECI === "true") { - return { - name: "CircleCI", - build_url: env.CIRCLE_BUILD_URL, - job_name: env.CIRCLE_JOB, - build_number: env.CIRCLE_BUILD_NUM - } - } - // Travis CI - if (env.CI === "true" && env.TRAVIS === "true") { - return { - name: "Travis CI", - build_url: env.TRAVIS_BUILD_WEB_URL, - job_name: env.TRAVIS_JOB_NAME, - build_number: env.TRAVIS_BUILD_NUMBER - } - } - // Codeship - if (env.CI === "true" && env.CI_NAME === "codeship") { - return { - name: "Codeship", - build_url: null, - job_name: null, - build_number: null - } - } - // Bitbucket - if (env.BITBUCKET_BRANCH && env.BITBUCKET_COMMIT) { - return { - name: "Bitbucket", - build_url: env.BITBUCKET_GIT_HTTP_ORIGIN, - job_name: null, - build_number: env.BITBUCKET_BUILD_NUMBER - } - } - // Drone - if (env.CI === "true" && env.DRONE === "true") { - return { - name: "Drone", - build_url: env.DRONE_BUILD_LINK, - job_name: null, - build_number: env.DRONE_BUILD_NUMBER - } - } - // Semaphore - if (env.CI === "true" && env.SEMAPHORE === "true") { - return { - name: "Semaphore", - build_url: env.SEMAPHORE_ORGANIZATION_URL, - job_name: env.SEMAPHORE_JOB_NAME, - build_number: env.SEMAPHORE_JOB_ID - } - } - // GitLab - if (env.CI === "true" && env.GITLAB_CI === "true") { - return { - name: "GitLab", - build_url: env.CI_JOB_URL, - job_name: env.CI_JOB_NAME, - build_number: env.CI_JOB_ID - } - } - // Buildkite - if (env.CI === "true" && env.BUILDKITE === "true") { - return { - name: "Buildkite", - build_url: env.BUILDKITE_BUILD_URL, - job_name: env.BUILDKITE_LABEL || env.BUILDKITE_PIPELINE_NAME, - build_number: env.BUILDKITE_BUILD_NUMBER - } - } - // Visual Studio Team Services - if (env.TF_BUILD === "True") { - return { - name: "Visual Studio Team Services", - build_url: `${env.SYSTEM_TEAMFOUNDATIONSERVERURI}${env.SYSTEM_TEAMPROJECTID}`, - job_name: env.SYSTEM_DEFINITIONID, - build_number: env.BUILD_BUILDID - } - } - // if no matches, return null - return null; -} - let packages = {}; exports.getPackageVersion = (package_, bsConfig = null) => { @@ -394,12 +222,6 @@ exports.getPackageVersion = (package_, bsConfig = null) => { return packageVersion; } -exports.getAgentVersion = () => { - let _path = path.join(__dirname, '../../../package.json'); - if(fs.existsSync(_path)) - return require(_path).version; -} - const 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; @@ -442,52 +264,6 @@ const setEventListeners = () => { } } -const getBuildDetails = (bsConfig) => { - const isTestObservabilityOptionsPresent = !utils.isUndefined(bsConfig["testObservabilityOptions"]); - let buildName = '', - projectName = '', - buildDescription = '', - buildTags = []; - - /* Pick from environment variables */ - buildName = process.env.BROWSERSTACK_BUILD_NAME || buildName; - projectName = process.env.BROWSERSTACK_PROJECT_NAME || projectName; - - /* Pick from testObservabilityOptions */ - if(isTestObservabilityOptionsPresent) { - buildName = buildName || bsConfig["testObservabilityOptions"]["buildName"]; - projectName = projectName || bsConfig["testObservabilityOptions"]["projectName"]; - if(!utils.isUndefined(bsConfig["testObservabilityOptions"]["buildTag"])) buildTags = [...buildTags, ...bsConfig["testObservabilityOptions"]["buildTag"]]; - buildDescription = buildDescription || bsConfig["testObservabilityOptions"]["buildDescription"]; - } - - /* Pick from run settings */ - buildName = buildName || bsConfig["run_settings"]["build_name"]; - projectName = projectName || bsConfig["run_settings"]["project_name"]; - if(!utils.isUndefined(bsConfig["run_settings"]["build_tag"])) buildTags = [...buildTags, bsConfig["run_settings"]["build_tag"]]; - - buildName = buildName || path.basename(path.resolve(process.cwd())); - - return { - buildName, - projectName, - buildDescription, - buildTags - }; -} - -const setBrowserstackCypressCliDependency = (bsConfig) => { - const runSettings = bsConfig.run_settings; - if (runSettings.npm_dependencies !== undefined && - 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'] = exports.getAgentVersion() || "latest"; - logger.warn(`Adding browserstack-cypress-cli version ${runSettings.npm_dependencies['browserstack-cypress-cli']} in npm_dependencies`); - } - } -} - const getCypressConfigFileContent = (bsConfig, cypressConfigPath) => { try { const cypressConfigFile = require(path.resolve(bsConfig ? bsConfig.run_settings.cypress_config_file : cypressConfigPath)); @@ -551,7 +327,7 @@ exports.launchTestSession = async (user_config, bsConfigPath) => { projectName, buildDescription, buildTags - } = getBuildDetails(user_config); + } = helper.getBuildDetails(user_config); const data = { 'format': 'json', 'project_name': projectName, @@ -566,14 +342,14 @@ exports.launchTestSession = async (user_config, bsConfigPath) => { version: os.version(), arch: os.arch() }, - 'ci_info': getCiInfo(), + 'ci_info': helper.getCiInfo(), 'build_run_identifier': process.env.BROWSERSTACK_BUILD_RUN_IDENTIFIER, 'failed_tests_rerun': process.env.BROWSERSTACK_RERUN || false, - 'version_control': await getGitMetaData(), + 'version_control': await helper.getGitMetaData(), 'observability_version': { frameworkName: "Cypress", frameworkVersion: exports.getPackageVersion('cypress', user_config), - sdkVersion: exports.getAgentVersion() + sdkVersion: helper.getAgentVersion() } }; const config = { @@ -592,7 +368,7 @@ exports.launchTestSession = async (user_config, bsConfigPath) => { process.env.BS_TESTOPS_BUILD_COMPLETED = true; setEnvironmentVariablesForRemoteReporter(response.data.jwt, response.data.build_hashed_id, response.data.allow_screenshots, data.observability_version.sdkVersion); // setEventListeners(); - if(this.isBrowserstackInfra()) setBrowserstackCypressCliDependency(user_config); + if(this.isBrowserstackInfra()) helper.setBrowserstackCypressCliDependency(user_config); } catch(error) { if(!error.errorType) { if (error.response) {