diff --git a/bin/accessibility-automation/helper.js b/bin/accessibility-automation/helper.js index f4fe5921..8a08b674 100644 --- a/bin/accessibility-automation/helper.js +++ b/bin/accessibility-automation/helper.js @@ -107,7 +107,6 @@ exports.createAccessibilityTestRun = async (user_config, framework) => { 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) { @@ -175,41 +174,52 @@ const nodeRequest = (type, url, data, config) => { } exports.supportFileCleanup = () => { - logger.debug("Cleaning up support file changes added for accessibility. ") + logger.debug("Cleaning up support file changes added for accessibility.") Object.keys(supportFileContentMap).forEach(file => { try { - fs.writeFileSync(file, supportFileContentMap[file], {encoding: 'utf-8'}); + if(typeof supportFileContentMap[file] === 'object') { + let fileOrDirpath = file; + if(supportFileContentMap[file].deleteSupportDir) { + fileOrDirpath = path.join(process.cwd(), 'cypress', 'support'); + } + helper.deleteSupportFileOrDir(fileOrDirpath); + } else { + 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 ( +const getAccessibilityCypressCommandEventListener = (extName) => { + return extName == 'js' ? ( `require('browserstack-cypress-cli/bin/accessibility-automation/cypress');` - ); + ) : ( + `import 'browserstack-cypress-cli/bin/accessibility-automation/cypress'` + ) } -const setAccessibilityEventListeners = () => { +exports.setAccessibilityEventListeners = (bsConfig) => { try { - const cypressCommandEventListener = getAccessibilityCypressCommandEventListener(); - // Searching form command.js recursively - glob(process.cwd() + '/**/cypress/support/*.js', {}, (err, files) => { + const supportFilesData = helper.getSupportFiles(bsConfig, true); + if(!supportFilesData.supportFile) return; + glob(process.cwd() + supportFilesData.supportFile, {}, (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')) { + if(!file.includes('commands.js') && !file.includes('commands.ts')) { const defaultFileContent = fs.readFileSync(file, {encoding: 'utf-8'}); + let cypressCommandEventListener = getAccessibilityCypressCommandEventListener(path.extname(file)); if(!defaultFileContent.includes(cypressCommandEventListener)) { let newFileContent = defaultFileContent + '\n' + cypressCommandEventListener + '\n' fs.writeFileSync(file, newFileContent, {encoding: 'utf-8'}); - supportFileContentMap[file] = defaultFileContent; + supportFileContentMap[file] = supportFilesData.cleanupParams ? supportFilesData.cleanupParams : defaultFileContent; } } } catch(e) { diff --git a/bin/commands/runs.js b/bin/commands/runs.js index a7040223..79282b73 100644 --- a/bin/commands/runs.js +++ b/bin/commands/runs.js @@ -25,6 +25,7 @@ const { getStackTraceUrl } = require('../helpers/sync/syncSpecsLogs'); const { launchTestSession, + setEventListeners, setTestObservabilityFlags, runCypressTestsLocally, printBuildLink @@ -33,6 +34,7 @@ const { const { createAccessibilityTestRun, + setAccessibilityEventListeners, checkAccessibilityPlatform, supportFileCleanup } = require('../accessibility-automation/helper'); @@ -146,7 +148,7 @@ module.exports = function run(args, rawArgs) { // add cypress dependency if missing utils.setCypressNpmDependency(bsConfig); - + if (isAccessibilitySession && isBrowserstackInfra) { await createAccessibilityTestRun(bsConfig); } @@ -205,6 +207,12 @@ module.exports = function run(args, rawArgs) { markBlockStart('validateConfig'); logger.debug("Started configs validation"); return capabilityHelper.validate(bsConfig, args).then(function (cypressConfigFile) { + if(process.env.BROWSERSTACK_TEST_ACCESSIBILITY) { + setAccessibilityEventListeners(bsConfig); + } + if(process.env.BS_TESTOPS_BUILD_COMPLETED) { + // setEventListeners(bsConfig); + } markBlockEnd('validateConfig'); logger.debug("Completed configs validation"); markBlockStart('preArchiveSteps'); diff --git a/bin/helpers/helper.js b/bin/helpers/helper.js index 38696cb0..de5e7825 100644 --- a/bin/helpers/helper.js +++ b/bin/helpers/helper.js @@ -16,6 +16,7 @@ const gitconfig = require('gitconfiglocal'); const { spawn, execSync } = require('child_process'); const glob = require('glob'); const pGitconfig = promisify(gitconfig); +const { readCypressConfigFile } = require('./readCypressConfigUtil'); const CrashReporter = require('../testObservability/crashReporter'); exports.debug = (text, shouldReport = false, throwable = null) => { @@ -313,3 +314,76 @@ exports.setBrowserstackCypressCliDependency = (bsConfig) => { } } } + +exports.deleteSupportFileOrDir = (fileOrDirPath) => { + try { + // Sanitize the input to remove any characters that could be used for directory traversal + const sanitizedPath = fileOrDirPath.replace(/(\.\.\/|\.\/|\/\/)/g, ''); + const resolvedPath = path.resolve(sanitizedPath); + if (fs.existsSync(resolvedPath)) { + if (fs.lstatSync(resolvedPath).isDirectory()) { + fs.readdirSync(resolvedPath).forEach((file) => { + const sanitizedFile = file.replace(/(\.\.\/|\.\/|\/\/)/g, ''); + const currentPath = path.join(resolvedPath, sanitizedFile); + fs.unlinkSync(currentPath); + }); + fs.rmdirSync(resolvedPath); + } else { + fs.unlinkSync(resolvedPath); + } + } + } catch(err) {} +} + +exports.getSupportFiles = (bsConfig, isA11y) => { + let extension = null; + try { + extension = bsConfig.run_settings.cypress_config_file.split('.').pop(); + } catch (err) {} + let supportFile = '/**/cypress/support/**/*.{js,ts}'; + let cleanupParams = {}; + let userSupportFile = null; + try { + const completeCypressConfigFile = readCypressConfigFile(bsConfig) + let cypressConfigFile = {}; + if (!utils.isUndefined(completeCypressConfigFile)) { + cypressConfigFile = !utils.isUndefined(completeCypressConfigFile.default) ? completeCypressConfigFile.default : completeCypressConfigFile + } + userSupportFile = cypressConfigFile.e2e?.supportFile !== null ? cypressConfigFile.e2e?.supportFile : cypressConfigFile.component?.supportFile !== null ? cypressConfigFile.component?.supportFile : cypressConfigFile.supportFile; + if(userSupportFile == false && extension) { + const supportFolderPath = path.join(process.cwd(), 'cypress', 'support'); + if (!fs.existsSync(supportFolderPath)) { + fs.mkdirSync(supportFolderPath); + cleanupParams.deleteSupportDir = true; + } + const sanitizedExtension = extension.replace(/(\.\.\/|\.\/|\/\/)/g, ''); + const supportFilePath = path.join(supportFolderPath, `tmpBstackSupportFile.${sanitizedExtension}`); + fs.writeFileSync(supportFilePath, ""); + supportFile = `/cypress/support/tmpBstackSupportFile.${sanitizedExtension}`; + const currEnvVars = bsConfig.run_settings.system_env_vars; + const supportFileEnv = `CYPRESS_SUPPORT_FILE=${supportFile.substring(1)}`; + if(!currEnvVars) { + bsConfig.run_settings.system_env_vars = [supportFileEnv]; + } else { + bsConfig.run_settings.system_env_vars = [...currEnvVars, supportFileEnv]; + } + cleanupParams.deleteSupportFile = true; + } else if(typeof userSupportFile == 'string') { + if (userSupportFile.startsWith('${') && userSupportFile.endsWith('}')) { + /* Template strings to reference environment variables */ + const envVar = userSupportFile.substring(2, userSupportFile.length - 1); + supportFile = process.env[envVar]; + } else { + /* Single file / glob pattern */ + supportFile = userSupportFile; + } + } else if(Array.isArray(userSupportFile)) { + supportFile = userSupportFile[0]; + } + } catch (err) {} + if(supportFile && supportFile[0] != '/') supportFile = '/' + supportFile; + return { + supportFile, + cleanupParams: Object.keys(cleanupParams).length ? cleanupParams : null + }; +} diff --git a/bin/testObservability/helper/helper.js b/bin/testObservability/helper/helper.js index abaeccda..deacff95 100644 --- a/bin/testObservability/helper/helper.js +++ b/bin/testObservability/helper/helper.js @@ -66,7 +66,15 @@ const httpsScreenshotsKeepAliveAgent = new https.Agent({ const supportFileCleanup = () => { Object.keys(supportFileContentMap).forEach(file => { try { - fs.writeFileSync(file, supportFileContentMap[file], {encoding: 'utf-8'}); + if(typeof supportFileContentMap[file] === 'object') { + let fileOrDirpath = file; + if(supportFileContentMap[file].deleteSupportDir) { + fileOrDirpath = path.join(process.cwd(), 'cypress', 'support'); + } + helper.deleteSupportFileOrDir(fileOrDirpath); + } else { + fs.writeFileSync(file, supportFileContentMap[file], {encoding: 'utf-8'}); + } } catch(e) { exports.debug(`Error while replacing file content for ${file} with it's original content with error : ${e}`, true, e); } @@ -241,29 +249,33 @@ const setEnvironmentVariablesForRemoteReporter = (BS_TESTOPS_JWT, BS_TESTOPS_BUI process.env.OBSERVABILITY_LAUNCH_SDK_VERSION = OBSERVABILITY_LAUNCH_SDK_VERSION; } -const getCypressCommandEventListener = () => { - return ( +const getCypressCommandEventListener = (isJS) => { + return isJS ? ( `require('browserstack-cypress-cli/bin/testObservability/cypress');` - ); + ) : ( + `import 'browserstack-cypress-cli/bin/testObservability/cypress'` + ) } -const setEventListeners = () => { +exports.setEventListeners = (bsConfig) => { try { - const cypressCommandEventListener = getCypressCommandEventListener(); - glob(process.cwd() + '/cypress/support/*.js', {}, (err, files) => { + const supportFilesData = helper.getSupportFiles(bsConfig, false); + if(!supportFilesData.supportFile) return; + glob(process.cwd() + supportFilesData.supportFile, {}, (err, files) => { if(err) return exports.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'}); + let cypressCommandEventListener = getCypressCommandEventListener(file.includes('js')); if(!defaultFileContent.includes(cypressCommandEventListener)) { let newFileContent = defaultFileContent + '\n' + cypressCommandEventListener + '\n' fs.writeFileSync(file, newFileContent, {encoding: 'utf-8'}); - supportFileContentMap[file] = defaultFileContent; + supportFileContentMap[file] = supportFilesData.cleanupParams ? supportFilesData.cleanupParams : defaultFileContent; } } } catch(e) { @@ -379,7 +391,6 @@ exports.launchTestSession = async (user_config, bsConfigPath) => { exports.debug('Build creation successfull!'); process.env.BS_TESTOPS_BUILD_COMPLETED = true; setEnvironmentVariablesForRemoteReporter(response.data.jwt, response.data.build_hashed_id, response.data.allow_screenshots, data.observability_version.sdkVersion); - // setEventListeners(); if(this.isBrowserstackInfra()) helper.setBrowserstackCypressCliDependency(user_config); } catch(error) { if(!error.errorType) { @@ -804,6 +815,7 @@ exports.resolveModule = (module) => { }; const getReRunSpecs = (rawArgs) => { + let finalArgs = rawArgs; if (this.isTestObservabilitySession() && this.shouldReRunObservabilityTests()) { let startIdx = -1, numEle = 0; for(let idx=0; idx { } } if(startIdx != -1) rawArgs.splice(startIdx, numEle + 1); - return [...rawArgs, '--spec', process.env.BROWSERSTACK_RERUN_TESTS]; - } else { - return rawArgs; + finalArgs = [...rawArgs, '--spec', process.env.BROWSERSTACK_RERUN_TESTS]; } + return finalArgs.filter(item => item !== '--disable-test-observability' && item !== '--disable-browserstack-automation'); } const getLocalSessionReporter = () => {