Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: browserstack/browserstack-cypress-cli
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v1.24.4
Choose a base ref
...
head repository: browserstack/browserstack-cypress-cli
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref

Commits on Jun 8, 2023

  1. Copy the full SHA
    eaa0ebb View commit details
  2. 🐛 fix for status code

    Archish27 committed Jun 8, 2023
    Copy the full SHA
    75468d8 View commit details
  3. Copy the full SHA
    7313f92 View commit details
  4. Copy the full SHA
    b238df2 View commit details

Commits on Jun 9, 2023

  1. 🐛 fix for zip upload

    Archish27 committed Jun 9, 2023
    Copy the full SHA
    4189e14 View commit details

Commits on Jun 12, 2023

  1. Copy the full SHA
    1c29d31 View commit details
  2. Copy the full SHA
    de1f986 View commit details

Commits on Jun 14, 2023

  1. Copy the full SHA
    0a082f1 View commit details
  2. Copy the full SHA
    3b358a4 View commit details
  3. Copy the full SHA
    c6c2eb0 View commit details
  4. Copy the full SHA
    f5a3b0c View commit details
  5. Copy the full SHA
    e278e43 View commit details
  6. Copy the full SHA
    6a217b6 View commit details
  7. Copy the full SHA
    8e005f8 View commit details
  8. Copy the full SHA
    74e7992 View commit details

Commits on Jul 17, 2023

  1. Copy the full SHA
    8ae164b View commit details

Commits on Jul 18, 2023

  1. Merge pull request #656 from browserstack/deploy

    1.24.4
    Archish Thakkar authored Jul 18, 2023
    Copy the full SHA
    4b46ccb View commit details
  2. Copy the full SHA
    2fefbd2 View commit details

Commits on Jul 26, 2023

  1. added caps

    roshan04 committed Jul 26, 2023
    Copy the full SHA
    3bf7b0f View commit details

Commits on Jul 27, 2023

  1. added validations

    roshan04 committed Jul 27, 2023
    Copy the full SHA
    b45f5b1 View commit details
  2. fixed validation

    roshan04 committed Jul 27, 2023
    Copy the full SHA
    6b5e731 View commit details
  3. added spec for utils

    roshan04 committed Jul 27, 2023
    Copy the full SHA
    544c10c View commit details
  4. added more spec

    roshan04 committed Jul 27, 2023
    Copy the full SHA
    501c9d1 View commit details
  5. removed the comments

    roshan04 committed Jul 27, 2023
    Copy the full SHA
    ee719cd View commit details

Commits on Jul 28, 2023

  1. Resolved comments

    roshan04 committed Jul 28, 2023
    Copy the full SHA
    7d08386 View commit details

Commits on Jul 30, 2023

  1. Copy the full SHA
    3652c3e View commit details

Commits on Jul 31, 2023

  1. minor fix

    amaanbs committed Jul 31, 2023
    Copy the full SHA
    1526747 View commit details
  2. minor fix pt.2

    amaanbs committed Jul 31, 2023
    Copy the full SHA
    c67eea1 View commit details

Commits on Aug 4, 2023

  1. minor fix

    amaanbs committed Aug 4, 2023
    Copy the full SHA
    00c81db View commit details

Commits on Aug 9, 2023

  1. updated spec description

    roshan04 committed Aug 9, 2023
    Copy the full SHA
    df4ab6a View commit details

Commits on Aug 10, 2023

  1. Merge pull request #659 from browserstack/interactive_session_cypress

    Interactive session cypress
    Archish Thakkar authored Aug 10, 2023
    Copy the full SHA
    6ac4c2d View commit details
  2. Merge branch 'master' of github.com:browserstack/browserstack-cypress…

    …-cli into OBS_1871_1867
    amaanbs committed Aug 10, 2023
    Copy the full SHA
    0694b16 View commit details
  3. Merge pull request #660 from browserstack/OBS_1871_1867

    O11y - Handle edge cases for detecting package version
    Archish Thakkar authored Aug 10, 2023
    Copy the full SHA
    7944e13 View commit details
  4. 1.25.0

    Archish Thakkar committed Aug 10, 2023
    Copy the full SHA
    4240b57 View commit details
  5. Merge pull request #669 from browserstack/deploy

    1.25.0
    Archish Thakkar authored Aug 10, 2023
    Copy the full SHA
    a819dab View commit details
  6. Copy the full SHA
    8cf7696 View commit details

Commits on Aug 17, 2023

  1. Copy the full SHA
    57ff8d3 View commit details

Commits on Aug 18, 2023

  1. Copy the full SHA
    29ef187 View commit details

Commits on Aug 21, 2023

  1. Copy the full SHA
    f3aa80d View commit details

Commits on Aug 22, 2023

  1. Merge pull request #670 from browserstack/LOC_4382

    Use proxy and cert flags in local binary download step.
    Archish Thakkar authored Aug 22, 2023
    Copy the full SHA
    17ca3d7 View commit details
  2. 1.25.1

    Archish Thakkar committed Aug 22, 2023
    Copy the full SHA
    18cd19f View commit details
  3. Merge pull request #677 from browserstack/deploy

    1.25.1
    Archish Thakkar authored Aug 22, 2023
    Copy the full SHA
    d781aa6 View commit details

Commits on Aug 23, 2023

  1. Copy the full SHA
    ae01920 View commit details
  2. Copy the full SHA
    4502780 View commit details
  3. Merge pull request #672 from browserstack/OBS_2080_handle_log_exception

    handle exceptions for > v12.7.0
    Archish Thakkar authored Aug 23, 2023
    Copy the full SHA
    466d623 View commit details
  4. Copy the full SHA
    6ab3a7f View commit details
  5. Merge pull request #671 from sauravdas1997/OB-2088-git-linking

    fix: Git mapping for windows
    bstack-security-github authored Aug 23, 2023
    Copy the full SHA
    a8b27f3 View commit details
  6. 1.25.2

    Archish Thakkar committed Aug 23, 2023
    Copy the full SHA
    58a4ed8 View commit details
  7. Merge pull request #678 from browserstack/deploy

    1.25.2
    Archish Thakkar authored Aug 23, 2023
    Copy the full SHA
    b96ba50 View commit details

Commits on Aug 25, 2023

  1. Copy the full SHA
    9448d3b View commit details
Showing with 7,717 additions and 2,047 deletions.
  1. +1 −1 CODEOWNERS
  2. +1 −0 bin/accessibility-automation/constants.js
  3. +433 −0 bin/accessibility-automation/cypress/index.js
  4. +275 −0 bin/accessibility-automation/helper.js
  5. +63 −0 bin/accessibility-automation/plugin/index.js
  6. +7 −8 bin/commands/generateReport.js
  7. +44 −38 bin/commands/info.js
  8. +127 −39 bin/commands/runs.js
  9. +5 −4 bin/commands/stop.js
  10. +23 −2 bin/helpers/archiver.js
  11. +131 −0 bin/helpers/atsHelper.js
  12. +44 −30 bin/helpers/build.js
  13. +134 −85 bin/helpers/buildArtifacts.js
  14. +73 −2 bin/helpers/capabilityHelper.js
  15. +54 −38 bin/helpers/checkUploaded.js
  16. +6 −2 bin/helpers/config.js
  17. +3 −1 bin/helpers/config.json
  18. +53 −4 bin/helpers/constants.js
  19. +23 −14 bin/helpers/downloadBuildStacktrace.js
  20. +33 −22 bin/helpers/getInitialDetails.js
  21. +456 −0 bin/helpers/helper.js
  22. +24 −5 bin/helpers/packageInstaller.js
  23. +133 −11 bin/helpers/readCypressConfigUtil.js
  24. +60 −39 bin/helpers/reporterHTML.js
  25. +203 −0 bin/helpers/runnerArgs.js
  26. +44 −25 bin/helpers/sync/syncSpecsLogs.js
  27. +87 −16 bin/helpers/usageReporting.js
  28. +675 −120 bin/helpers/utils.js
  29. +61 −85 bin/helpers/zipUpload.js
  30. +3 −200 bin/runner.js
  31. +2 −0 bin/templates/configTemplate.js
  32. +63 −19 bin/testObservability/crashReporter/index.js
  33. +193 −95 bin/testObservability/cypress/index.js
  34. +12 −0 bin/testObservability/helper/cleanupQueueSync.js
  35. +10 −0 bin/testObservability/helper/constants.js
  36. +299 −338 bin/testObservability/helper/helper.js
  37. +17 −4 bin/testObservability/helper/requestQueueHandler.js
  38. +6 −0 bin/testObservability/plugin/index.js
  39. +10 −6 bin/testObservability/plugin/ipcServer.js
  40. +100 −31 bin/testObservability/reporter/index.js
  41. +12 −9 package.json
  42. +140 −0 test/test_files/large_commit_message.txt
  43. +22 −28 test/unit/bin/commands/info.js
  44. +591 −85 test/unit/bin/commands/runs.js
  45. +1 −2 test/unit/bin/commands/stop.js
  46. +113 −0 test/unit/bin/helpers/atsHelper.js
  47. +33 −38 test/unit/bin/helpers/build.js
  48. +60 −0 test/unit/bin/helpers/buildArtifacts.js
  49. +123 −0 test/unit/bin/helpers/capabilityHelper.js
  50. +51 −50 test/unit/bin/helpers/checkUploaded.js
  51. +8 −8 test/unit/bin/helpers/getInitialDetails.js
  52. +0 −1 test/unit/bin/helpers/hashUtil.js
  53. +41 −0 test/unit/bin/helpers/helper.js
  54. +286 −17 test/unit/bin/helpers/readCypressConfigUtil.js
  55. +188 −201 test/unit/bin/helpers/reporterHTML.js
  56. +197 −197 test/unit/bin/helpers/sync/syncSpecsLogs.js
  57. +1,627 −49 test/unit/bin/helpers/utils.js
  58. +93 −77 test/unit/bin/helpers/zipUpload.js
  59. +12 −0 test/unit/support/fixtures/package-cypress-deps.json
  60. +9 −0 test/unit/support/fixtures/package-empty-devdeps.json
  61. +14 −0 test/unit/support/fixtures/package-git-deps.json
  62. +8 −0 test/unit/support/fixtures/package-invalid-devdeps.json
  63. +12 −0 test/unit/support/fixtures/package-malformed.json
  64. +9 −0 test/unit/support/fixtures/package-no-devdeps.json
  65. +14 −0 test/unit/support/fixtures/package-scoped-deps.json
  66. +15 −0 test/unit/support/fixtures/package-unicode.json
  67. +15 −0 test/unit/support/fixtures/package-valid.json
  68. +32 −1 test/unit/support/fixtures/testObjects.js
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @browserstack/afd-dev
* @browserstack/automate-dev
1 change: 1 addition & 0 deletions bin/accessibility-automation/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exports.API_URL = 'https://accessibility.browserstack.com/api';
433 changes: 433 additions & 0 deletions bin/accessibility-automation/cypress/index.js

Large diffs are not rendered by default.

275 changes: 275 additions & 0 deletions bin/accessibility-automation/helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
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 axios = require('axios');
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 { consoleHolder } = require("../testObservability/helper/constants");
const supportFileContentMap = {}
const HttpsProxyAgent = require('https-proxy-agent');

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(),
language: 'javascript',
testFramework: 'cypress',
testFrameworkVersion: helper.getPackageVersion('cypress', user_config)
},
'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: {
username: userName,
password: accessKey
},
headers: {
'Content-Type': 'application/json'
}
};

const response = await nodeRequest(
'POST', 'v2/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);
if(user_config.run_settings.auto_import_dev_dependencies != true) helper.setBrowserstackCypressCliDependency(user_config);

} catch (error) {
if (error.response) {
logger.error("Incorrect Cred");
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("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}`,
data: data
};

if(process.env.HTTP_PROXY){
options.proxy = false
options.httpsAgent = new HttpsProxyAgent(process.env.HTTP_PROXY);

} else if (process.env.HTTPS_PROXY){
options.proxy = false
options.httpsAgent = new HttpsProxyAgent(process.env.HTTPS_PROXY);
}

axios(options).then(response => {
if(!(response.status == 201 || response.status == 200)) {
logger.info("response.status in nodeRequest", response.status);
reject(response && response.data ? response.data : `Received response from BrowserStack Server with status : ${response.status}`);
} else {
try {
if(typeof(response.data) !== 'object') body = JSON.parse(response.data);
} catch(e) {
if(!url.includes('/stop')) {
reject('Not a JSON response from BrowserStack Server');
}
}
resolve({
data: response.data
});
}
}).catch(error => {

logger.info("error in nodeRequest", error);
reject(error);
})
});
}

exports.supportFileCleanup = () => {
logger.debug("Cleaning up support file changes added for accessibility.")
Object.keys(supportFileContentMap).forEach(file => {
try {
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 = (extName) => {
return extName == 'js' ? (
`require('browserstack-cypress-cli/bin/accessibility-automation/cypress');`
) : (
`import 'browserstack-cypress-cli/bin/accessibility-automation/cypress'`
)
}

exports.setAccessibilityEventListeners = (bsConfig) => {
try {

const supportFilesData = helper.getSupportFiles(bsConfig, true);
if(!supportFilesData.supportFile) return;

const isPattern = glob.hasMagic(supportFilesData.supportFile);
if(!isPattern) {
logger.debug(`Using user defined support file: ${supportFilesData.supportFile}`);
let file;
try {
file = process.cwd() + supportFilesData.supportFile;
const defaultFileContent = fs.readFileSync(file, {encoding: 'utf-8'});
let cypressCommandEventListener = getAccessibilityCypressCommandEventListener(path.extname(file));
const alreadyIncludes = defaultFileContent.includes(cypressCommandEventListener);
if(!alreadyIncludes) {
let newFileContent = defaultFileContent +
'\n' +
cypressCommandEventListener +
'\n';
fs.writeFileSync(file, newFileContent, {encoding: 'utf-8'});
supportFileContentMap[file] = supportFilesData.cleanupParams ? supportFilesData.cleanupParams : defaultFileContent;
}
} catch(e) {
logger.debug(`Unable to modify file contents for ${file} to set event listeners with error ${e}`, true, e);
}
return;
}

const globPattern = process.cwd() + supportFilesData.supportFile;
glob(globPattern, {}, (err, files) => {
if(err) {
logger.debug('EXCEPTION IN BUILD START EVENT : Unable to parse cypress support files');
return;
}

files.forEach(file => {
try {
const fileName = path.basename(file);
if(['e2e.js', 'e2e.ts', 'component.ts', 'component.js'].includes(fileName) && !file.includes('node_modules')) {

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] = supportFilesData.cleanupParams ? supportFilesData.cleanupParams : 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);
}
}
63 changes: 63 additions & 0 deletions bin/accessibility-automation/plugin/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const path = require("node:path");
const { decodeJWTToken } = require("../../helpers/utils");
const utils = require('../../helpers/utils');

const browserstackAccessibility = (on, config) => {
let browser_validation = true;
if (process.env.BROWSERSTACK_ACCESSIBILITY_DEBUG === 'true') {
config.env.BROWSERSTACK_LOGS = 'true';
process.env.BROWSERSTACK_LOGS = 'true';
}
on('task', {
browserstack_log(message) {
console.log(message)

return null
},
})
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)
const payload = decodeJWTToken(process.env.ACCESSIBILITY_AUTH);
launchOptions.extensions.push(ally_path);
if(!utils.isUndefined(payload) && !utils.isUndefined(payload.a11y_core_config) && payload.a11y_core_config.domForge === true) {
launchOptions.args.push("--auto-open-devtools-for-tabs");
launchOptions.preferences.default["devtools"] = launchOptions.preferences.default["devtools"] || {};
launchOptions.preferences.default["devtools"]["preferences"] = launchOptions.preferences.default["devtools"]["preferences"] || {};
launchOptions.preferences.default["devtools"]["preferences"][
"currentDockState"
] = '"undocked"';
}
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;
15 changes: 7 additions & 8 deletions bin/commands/generateReport.js
Original file line number Diff line number Diff line change
@@ -5,13 +5,13 @@ const logger = require("../helpers/logger").winstonLogger,
utils = require("../helpers/utils"),
reporterHTML = require('../helpers/reporterHTML'),
getInitialDetails = require('../helpers/getInitialDetails').getInitialDetails;

const { isTurboScaleSession } = require('../helpers/atsHelper');

module.exports = function generateReport(args, rawArgs) {
let bsConfigPath = utils.getConfigPath(args.cf);
let reportGenerator = reporterHTML.reportGenerator;

return utils.validateBstackJson(bsConfigPath).then(function (bsConfig) {
return utils.validateBstackJson(bsConfigPath).then(async function (bsConfig) {
// setting setDefaults to {} if not present and set via env variables or via args.
utils.setDefaults(bsConfig, args);

@@ -21,22 +21,21 @@ module.exports = function generateReport(args, rawArgs) {
// accept the access key from command line if provided
utils.setAccessKey(bsConfig, args);

getInitialDetails(bsConfig, args, rawArgs).then((buildReportData) => {

utils.setUsageReportingFlag(bsConfig, args.disableUsageReporting);
try {
let buildReportData = isTurboScaleSession(bsConfig) ? null : await getInitialDetails(bsConfig, args, rawArgs);
utils.setUsageReportingFlag(bsConfig, args.disableUsageReporting);

// set cypress config filename
utils.setCypressConfigFilename(bsConfig, args);

let messageType = Constants.messageTypes.INFO;
let errorCode = null;
let buildId = args._[1];

reportGenerator(bsConfig, buildId, args, rawArgs, buildReportData);
utils.sendUsageReport(bsConfig, args, 'generate-report called', messageType, errorCode, buildReportData, rawArgs);
}).catch((err) => {
} catch(err) {
logger.warn(err);
});
};
}).catch(function (err) {
logger.error(err);
utils.setUsageReportingFlag(null, args.disableUsageReporting);
82 changes: 44 additions & 38 deletions bin/commands/info.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
'use strict';
const request = require('request');

const axios = require('axios').default;
const config = require("../helpers/config"),
logger = require("../helpers/logger").winstonLogger,
Constants = require("../helpers/constants"),
utils = require("../helpers/utils"),
getInitialDetails = require('../helpers/getInitialDetails').getInitialDetails;

const { setAxiosProxy } = require('../helpers/helper');

module.exports = function info(args, rawArgs) {
let bsConfigPath = utils.getConfigPath(args.cf);

@@ -19,7 +20,7 @@ module.exports = function info(args, rawArgs) {
// accept the access key from command line if provided
utils.setAccessKey(bsConfig, args);

getInitialDetails(bsConfig, args, rawArgs).then((buildReportData) => {
getInitialDetails(bsConfig, args, rawArgs).then(async (buildReportData) => {

utils.setUsageReportingFlag(bsConfig, args.disableUsageReporting);

@@ -38,65 +39,70 @@ module.exports = function info(args, rawArgs) {
'User-Agent': utils.getUserAgent(),
},
};
request.get(options, function (err, resp, body) {
let message = null;
let messageType = null;
let errorCode = null;

if (err) {
message = Constants.userMessages.BUILD_INFO_FAILED;
messageType = Constants.messageTypes.ERROR;
errorCode = 'api_failed_build_info';

logger.info(message);
logger.error(utils.formatRequest(err, resp, body));
} else {
let build = null;

if (Constants.turboScaleObj.enabled) {
options.url = `${config.turboScaleBuildsUrl}/${buildId}`;
}

let message = null;
let messageType = null;
let errorCode = null;

options.config = {
auth: {
username: bsConfig.auth.username,
password: bsConfig.auth.access_key
},
headers: options.headers
};
setAxiosProxy(options.config);

try {
const response = await axios.get(options.url, options.config);
let build = null;
try {
build = JSON.parse(body);
build = response.data;
} catch (error) {
build = null;
}

if (resp.statusCode == 299) {
if (response.status == 299) {
messageType = Constants.messageTypes.INFO;
errorCode = 'api_deprecated';

if (build) {
message = build.message;
logger.info(message);
} else {
message = Constants.userMessages.API_DEPRECATED;
logger.info(message);
}
logger.info(utils.formatRequest(err, resp, body));
} else if (resp.statusCode != 200) {
message = build ? build.message : Constants.userMessages.API_DEPRECATED;
logger.info(utils.formatRequest(response.statusText, response, response.data));
} else if (response.status != 200) {
message = Constants.userMessages.BUILD_INFO_FAILED;
messageType = Constants.messageTypes.ERROR;
errorCode = 'api_failed_build_info';

if (build) {
message = `${
Constants.userMessages.BUILD_INFO_FAILED
} with error: \n${JSON.stringify(build, null, 2)}`;
logger.error(message);
if (build.message === 'Unauthorized') errorCode = 'api_auth_failed';
} else {
message = Constants.userMessages.BUILD_INFO_FAILED;
logger.error(message);
}
logger.error(utils.formatRequest(err, resp, body));
logger.error(message);
logger.error(utils.formatRequest(response.statusText, response, response.data));
} else {
messageType = Constants.messageTypes.SUCCESS;
message = `Build info for build id: \n ${JSON.stringify(
build,
null,
2
)}`;
logger.info(message);
}
}
utils.sendUsageReport(bsConfig, args, message, messageType, errorCode, buildReportData, rawArgs);
});
logger.info(message);
} catch (error) {
message = `${
Constants.userMessages.BUILD_INFO_FAILED
} with error: \n${error.response.data.message}`;
messageType = Constants.messageTypes.ERROR;
errorCode = 'api_failed_build_info';
logger.info(message);
logger.error(utils.formatRequest(error.response.statusText, error.response, error.response.data));
}
utils.sendUsageReport(bsConfig, args, message, messageType, errorCode, buildReportData, rawArgs);
}).catch((err) => {
logger.warn(err);
});
166 changes: 127 additions & 39 deletions bin/commands/runs.js
Original file line number Diff line number Diff line change
@@ -18,20 +18,29 @@ const archiver = require("../helpers/archiver"),
{initTimeComponents, instrumentEventTime, markBlockStart, markBlockEnd, getTimeComponents} = require('../helpers/timeComponents'),
downloadBuildArtifacts = require('../helpers/buildArtifacts').downloadBuildArtifacts,
downloadBuildStacktrace = require('../helpers/downloadBuildStacktrace').downloadBuildStacktrace,
updateNotifier = require('update-notifier'),
pkg = require('../../package.json'),
packageDiff = require('../helpers/package-diff');
const { getStackTraceUrl } = require('../helpers/sync/syncSpecsLogs');

const {
launchTestSession,
setEventListeners,
setTestObservabilityFlags,
runCypressTestsLocally,
printBuildLink
} = require('../testObservability/helper/helper');

module.exports = function run(args, rawArgs) {
const {
createAccessibilityTestRun,
setAccessibilityEventListeners,
checkAccessibilityPlatform,
supportFileCleanup
} = require('../accessibility-automation/helper');
const { isTurboScaleSession, getTurboScaleGridDetails, patchCypressConfigFileContent, atsFileCleanup } = require('../helpers/atsHelper');


module.exports = function run(args, rawArgs) {
utils.normalizeTestReportingEnvVars();
markBlockStart('preBuild');
// set debug mode (--cli-debug)
utils.setDebugMode(args);
@@ -53,10 +62,16 @@ module.exports = function run(args, rawArgs) {
markBlockStart('setConfig');
logger.debug('Started setting the configs');

/*
Set testObservability & browserstackAutomation flags
*/
// set cypress config filename
utils.setCypressConfigFilename(bsConfig, args);

/* Set testObservability & browserstackAutomation flags */
const [isTestObservabilitySession, isBrowserstackInfra] = setTestObservabilityFlags(bsConfig);
const checkAccessibility = checkAccessibilityPlatform(bsConfig);
const isAccessibilitySession = bsConfig.run_settings.accessibility || checkAccessibility;
const turboScaleSession = isTurboScaleSession(bsConfig);
Constants.turboScaleObj.enabled = turboScaleSession;


utils.setUsageReportingFlag(bsConfig, args.disableUsageReporting);

@@ -68,14 +83,11 @@ module.exports = function run(args, rawArgs) {
// accept the access key from command line or env variable if provided
utils.setAccessKey(bsConfig, args);

let buildReportData = !isBrowserstackInfra ? null : await getInitialDetails(bsConfig, args, rawArgs);
let buildReportData = (turboScaleSession || !isBrowserstackInfra) ? null : await getInitialDetails(bsConfig, args, rawArgs);

// accept the build name from command line if provided
utils.setBuildName(bsConfig, args);

// set cypress config filename
utils.setCypressConfigFilename(bsConfig, args);

if(isBrowserstackInfra) {
// set cypress test suite type
utils.setCypressTestSuiteType(bsConfig);
@@ -89,6 +101,7 @@ module.exports = function run(args, rawArgs) {
// set spec timeout
utils.setSpecTimeout(bsConfig, args);
}


// accept the specs list from command line if provided
utils.setUserSpecs(bsConfig, args);
@@ -99,10 +112,11 @@ module.exports = function run(args, rawArgs) {
// set build tag caps
utils.setBuildTags(bsConfig, args);

/*
Send build start to Observability
*/
if(isTestObservabilitySession) await launchTestSession(bsConfig, bsConfigPath);
// Send build start to TEST REPORTING AND ANALYTICS
if(isTestObservabilitySession) {
await launchTestSession(bsConfig, bsConfigPath);
utils.setO11yProcessHooks(null, bsConfig, args, null, buildReportData);
}

// accept the system env list from bsconf and set it
utils.setSystemEnvs(bsConfig);
@@ -129,8 +143,33 @@ module.exports = function run(args, rawArgs) {
// set the no-wrap
utils.setNoWrap(bsConfig, args);

// process auto-import dev dependencies
utils.processAutoImportDependencies(bsConfig.run_settings);

// add cypress dependency if missing
utils.setCypressNpmDependency(bsConfig);

if (isAccessibilitySession && isBrowserstackInfra) {
await createAccessibilityTestRun(bsConfig);
}

if (turboScaleSession) {
const gridDetails = await getTurboScaleGridDetails(bsConfig, args, rawArgs);

if (gridDetails && Object.keys(gridDetails).length > 0) {
Constants.turboScaleObj.gridDetails = gridDetails;
Constants.turboScaleObj.gridUrl = gridDetails.cypressUrl;
Constants.turboScaleObj.uploadUrl = gridDetails.cypressUrl + '/upload';
Constants.turboScaleObj.buildUrl = gridDetails.cypressUrl + '/build';

logger.debug(`Automate TurboScale Grid URL set to ${gridDetails.url}`);

patchCypressConfigFileContent(bsConfig);
} else {
process.exitCode = Constants.ERROR_EXIT_CODE;
return;
}
}
}

const { packagesInstalled } = !isBrowserstackInfra ? false : await packageInstaller.packageSetupAndInstaller(bsConfig, config.packageDirName, {markBlockStart, markBlockEnd});
@@ -156,24 +195,34 @@ module.exports = function run(args, rawArgs) {
logger.debug("Completed setting the configs");

if(!isBrowserstackInfra) {
if(process.env.BS_TESTOPS_BUILD_COMPLETED) {
setEventListeners(bsConfig);
}

return runCypressTestsLocally(bsConfig, args, rawArgs);
}

// Validate browserstack.json values and parallels specified via arguments
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');
logger.debug("Started pre-archive steps");

//get the number of spec files
markBlockStart('getNumberOfSpecFiles');
let specFiles = utils.getNumberOfSpecFiles(bsConfig, args, cypressConfigFile);
let specFiles = utils.getNumberOfSpecFiles(bsConfig, args, cypressConfigFile, turboScaleSession);
markBlockEnd('getNumberOfSpecFiles');

bsConfig['run_settings']['video_config'] = utils.getVideoConfig(cypressConfigFile);
bsConfig['run_settings']['video_config'] = utils.getVideoConfig(cypressConfigFile, bsConfig);

// return the number of parallels user specified
let userSpecifiedParallels = utils.getParallels(bsConfig, args);
@@ -184,6 +233,9 @@ module.exports = function run(args, rawArgs) {
// set record feature caps
utils.setRecordCaps(bsConfig, args, cypressConfigFile);

// set the interactive debugging capability
utils.setInteractiveCapability(bsConfig);

// warn if specFiles cross our limit
utils.warnSpecLimit(bsConfig, args, specFiles, rawArgs, buildReportData);
markBlockEnd('preArchiveSteps');
@@ -210,7 +262,15 @@ module.exports = function run(args, rawArgs) {

let test_zip_size = utils.fetchZipSize(path.join(process.cwd(), config.fileName));
let npm_zip_size = utils.fetchZipSize(path.join(process.cwd(), config.packageFileName));
let node_modules_size = await utils.fetchFolderSize(path.join(process.cwd(), "node_modules"))
let node_modules_size = await utils.fetchFolderSize(path.join(process.cwd(), "node_modules"));

if (Constants.turboScaleObj.enabled) {
// Note: Calculating md5 here for turboscale force-upload so that we don't need to re-calculate at hub
let zip_md5sum = await checkUploaded.checkSpecsMd5(bsConfig.run_settings, args, {markBlockStart, markBlockEnd});
let npm_package_md5sum = await checkUploaded.checkPackageMd5(bsConfig.run_settings);
Object.assign(md5data, { npm_package_md5sum });
Object.assign(md5data, { zip_md5sum });
}

//Package diff
let isPackageDiff = false;
@@ -229,6 +289,22 @@ module.exports = function run(args, rawArgs) {
markBlockEnd('zip.zipUpload');
markBlockEnd('zip');

if (process.env.BROWSERSTACK_TEST_ACCESSIBILITY === 'true') {
supportFileCleanup();
}

if (turboScaleSession) {
atsFileCleanup(bsConfig);
}

// Set config args for enforce_settings
if ( !utils.isUndefinedOrFalse(bsConfig.run_settings.enforce_settings) ) {
markBlockStart('setEnforceSettingsConfig');
logger.debug('Started setting the configs');
utils.setEnforceSettingsConfig(bsConfig, args);
logger.debug('Completed setting the configs');
markBlockEnd('setEnforceSettingsConfig');
}
// Create build
//setup Local Testing
markBlockStart('localSetup');
@@ -245,8 +321,14 @@ module.exports = function run(args, rawArgs) {
markBlockEnd('createBuild');
markBlockEnd('total');
utils.setProcessHooks(data.build_id, bsConfig, bs_local, args, buildReportData);
if(isTestObservabilitySession) {
utils.setO11yProcessHooks(data.build_id, bsConfig, bs_local, args, buildReportData);
}
let message = `${data.message}! ${Constants.userMessages.BUILD_CREATED} with build id: ${data.build_id}`;
let dashboardLink = `${Constants.userMessages.VISIT_DASHBOARD} ${data.dashboard_url}`;
if (turboScaleSession) {
dashboardLink = `${Constants.userMessages.VISIT_ATS_DASHBOARD} ${data.dashboard_url}`;
}
buildReportData = { 'build_id': data.build_id, 'parallels': userSpecifiedParallels, ...buildReportData }
utils.exportResults(data.build_id, `${config.dashboardUrl}${data.build_id}`);
if ((utils.isUndefined(bsConfig.run_settings.parallels) && utils.isUndefined(args.parallels)) || (!utils.isUndefined(bsConfig.run_settings.parallels) && bsConfig.run_settings.parallels == Constants.cliMessages.RUN.DEFAULT_PARALLEL_MESSAGE)) {
@@ -277,7 +359,7 @@ module.exports = function run(args, rawArgs) {
logger.debug("Completed polling of build status");

// stop the Local instance
await utils.stopLocalBinary(bsConfig, bs_local, args, rawArgs, buildReportData);
if (!turboScaleSession) await utils.stopLocalBinary(bsConfig, bs_local, args, rawArgs, buildReportData);

// waiting for 5 secs for upload to complete (as a safety measure)
await new Promise(resolve => setTimeout(resolve, 5000));
@@ -286,18 +368,18 @@ module.exports = function run(args, rawArgs) {
if (exitCode != Constants.BUILD_FAILED_EXIT_CODE) {
if (utils.nonEmptyArray(bsConfig.run_settings.downloads)) {
logger.debug("Downloading build artifacts");
await downloadBuildArtifacts(bsConfig, data.build_id, args, rawArgs, buildReportData);
await downloadBuildArtifacts(bsConfig, data.build_id, args, rawArgs, buildReportData, turboScaleSession);
}

// Generate custom report!
reportGenerator(bsConfig, data.build_id, args, rawArgs, buildReportData, function(){
reportGenerator(bsConfig, data.build_id, args, rawArgs, buildReportData, function(modifiedExitCode=exitCode){
utils.sendUsageReport(bsConfig, args, `${message}\n${dashboardLink}`, Constants.messageTypes.SUCCESS, null, buildReportData, rawArgs);
markBlockEnd('postBuild');
utils.handleSyncExit(exitCode, data.dashboard_url);
utils.handleSyncExit(modifiedExitCode, data.dashboard_url);
});
} else {
} else if(!turboScaleSession){
let stacktraceUrl = getStackTraceUrl();
downloadBuildStacktrace(stacktraceUrl).then((message) => {
downloadBuildStacktrace(stacktraceUrl, bsConfig).then((message) => {
utils.sendUsageReport(bsConfig, args, message, Constants.messageTypes.SUCCESS, null, buildReportData, rawArgs);
}).catch((err) => {
let message = `Downloading build stacktrace failed with statuscode: ${err}. Please visit ${data.dashboard_url} for additional details.`;
@@ -309,9 +391,11 @@ module.exports = function run(args, rawArgs) {
logger.info(Constants.userMessages.BUILD_FAILED_ERROR)
process.exitCode = Constants.BUILD_FAILED_EXIT_CODE;
});
} else {
utils.handleSyncExit(exitCode, data.dashboard_url);
}
});
} else if (utils.nonEmptyArray(bsConfig.run_settings.downloads)) {
} else if (utils.nonEmptyArray(bsConfig.run_settings.downloads && !turboScaleSession)) {
logger.info(Constants.userMessages.ASYNC_DOWNLOADS.replace('<build-id>', data.build_id));
}

@@ -451,23 +535,27 @@ module.exports = function run(args, rawArgs) {
utils.sendUsageReport(bsJsonData, args, err.message, Constants.messageTypes.ERROR, utils.getErrorCodeFromErr(err), null, rawArgs);
process.exitCode = Constants.ERROR_EXIT_CODE;
}).finally(function(){
const notifier = updateNotifier({
pkg,
updateCheckInterval: 1000 * 60 * 60 * 24 * 7,
});
import('update-notifier').then(({ default: updateNotifier } ) => {
const notifier = updateNotifier({
pkg,
updateCheckInterval: 1000 * 60 * 60 * 24 * 7,
});

// Checks for update on first run.
// Set lastUpdateCheck to 0 to spawn the check update process as notifier sets this to Date.now() for preventing
// the check untill one interval period. It runs once.
if (!notifier.disabled && Date.now() - notifier.config.get('lastUpdateCheck') < 50) {
notifier.config.set('lastUpdateCheck', 0);
notifier.check();
}
// Checks for update on first run.
// Set lastUpdateCheck to 0 to spawn the check update process as notifier sets this to Date.now() for preventing
// the check untill one interval period. It runs once.
if (!notifier.disabled && Date.now() - notifier.config.get('lastUpdateCheck') < 50) {
notifier.config.set('lastUpdateCheck', 0);
notifier.check();
}

// Set the config update as notifier clears this after reading.
if (notifier.update && notifier.update.current !== notifier.update.latest) {
notifier.config.set('update', notifier.update);
notifier.notify({isGlobal: true});
}
// Set the config update as notifier clears this after reading.
if (notifier.update && notifier.update.current !== notifier.update.latest) {
notifier.config.set('update', notifier.update);
notifier.notify({isGlobal: true});
}
}).catch((error) => {
logger.debug('Got error loading update-notifier: ', error);
});
});
}
9 changes: 5 additions & 4 deletions bin/commands/stop.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
'use strict';
const request = require('request');

const config = require("../helpers/config"),
logger = require("../helpers/logger").winstonLogger,
@@ -19,17 +18,19 @@ module.exports = function stop(args, rawArgs) {
// accept the access key from command line if provided
utils.setAccessKey(bsConfig, args);

let buildReportData = await getInitialDetails(bsConfig, args, rawArgs);

utils.setUsageReportingFlag(bsConfig, args.disableUsageReporting);

// set cypress config filename
utils.setCypressConfigFilename(bsConfig, args);

let buildId = args._[1];
let buildReportData = null;

await utils.stopBrowserStackBuild(bsConfig, args, buildId, rawArgs, buildReportData);
if (!Constants.turboScaleObj.enabled) {
buildReportData = await getInitialDetails(bsConfig, args, rawArgs);
}

await utils.stopBrowserStackBuild(bsConfig, args, buildId, rawArgs, buildReportData);
}).catch(function (err) {
logger.error(err);
utils.setUsageReportingFlag(null, args.disableUsageReporting);
25 changes: 23 additions & 2 deletions bin/helpers/archiver.js
Original file line number Diff line number Diff line change
@@ -72,9 +72,30 @@ const archiveSpecs = (runSettings, filePath, excludeFiles, md5data) => {
});
}

// Split mac and win configs
let macPackageJSON = {};
let winPackageJSON = {};
Object.assign(macPackageJSON, packageJSON);
Object.assign(winPackageJSON, packageJSON);

if (typeof runSettings.npm_dependencies === 'object') {
let macNpmDependencies = Object.assign({}, runSettings.npm_dependencies, runSettings.mac_npm_dependencies || {});
let winNpmDependencies = Object.assign({}, runSettings.npm_dependencies, runSettings.win_npm_dependencies || {});

Object.assign(macPackageJSON, {
devDependencies: macNpmDependencies,
});

Object.assign(winPackageJSON, {
devDependencies: winNpmDependencies,
});
}

if (Object.keys(packageJSON).length > 0) {
let packageJSONString = JSON.stringify(packageJSON, null, 4);
archive.append(packageJSONString, {name: `${cypressAppendFilesZipLocation}browserstack-package.json`});
const macPackageJSONString = JSON.stringify(macPackageJSON, null, 4);
const winPackageJSONString = JSON.stringify(winPackageJSON, null, 4);
archive.append(macPackageJSONString, {name: `${cypressAppendFilesZipLocation}browserstack-mac-package.json`});
archive.append(winPackageJSONString, {name: `${cypressAppendFilesZipLocation}browserstack-win-package.json`});
}

//Create copy of package.json
131 changes: 131 additions & 0 deletions bin/helpers/atsHelper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
const path = require('path');
const fs = require('fs');
const { consoleHolder } = require('../testObservability/helper/constants');
const HttpsProxyAgent = require('https-proxy-agent');
const { v4: uuidv4 } = require('uuid');
const axios = require('axios'),
logger = require('./logger').winstonLogger,
utils = require('./utils'),
config = require('./config');
Constants = require('./constants');

exports.isTurboScaleSession = (bsConfig) => {
// env var will override config
if (process.env.BROWSERSTACK_TURBOSCALE && process.env.BROWSERSTACK_TURBOSCALE === 'true') {
return true;
}

if (utils.isNotUndefined(bsConfig) && bsConfig.run_settings && bsConfig.run_settings.turboScale) {
return true;
}

return false;
};

exports.getTurboScaleOptions = (bsConfig) => {
if (bsConfig.run_settings && bsConfig.run_settings.turboScaleOptions) {
return bsConfig.run_settings.turboScaleOptions;
}

return {};
};

exports.getTurboScaleGridName = (bsConfig) => {
// env var will override config
if (process.env.BROWSERSTACK_TURBOSCALE_GRID_NAME) {
return process.env.BROWSERSTACK_TURBOSCALE_GRID_NAME;
}

if (bsConfig.run_settings && bsConfig.run_settings.turboScaleOptions && bsConfig.run_settings.turboScaleOptions.gridName) {
return bsConfig.run_settings.turboScaleOptions.gridName;
}

return 'NO_GRID_NAME_PASSED';
};

exports.getTurboScaleGridDetails = async (bsConfig, args, rawArgs) => {
try {
const gridName = this.getTurboScaleGridName(bsConfig);

return new Promise((resolve, reject) => {
let options = {
url: `${config.turboScaleAPIUrl}/grids/${gridName}`,
auth: {
username: bsConfig.auth.username,
password: bsConfig.auth.access_key,
},
headers: {
'User-Agent': utils.getUserAgent(),
}
};

if (process.env.HTTP_PROXY) {
options.proxy = false;
options.httpsAgent = new HttpsProxyAgent(process.env.HTTP_PROXY);
} else if (process.env.HTTPS_PROXY) {
options.proxy = false;
options.httpsAgent = new HttpsProxyAgent(process.env.HTTPS_PROXY);
}

let responseData = {};

axios(options).then(response => {
try {
responseData = response.data;
} catch (e) {
responseData = {};
}
if(response.status != 200) {
logger.warn(`Warn: Get Automate TurboScale Details Request failed with status code ${response.status}`);
utils.sendUsageReport(bsConfig, args, responseData["error"], Constants.messageTypes.ERROR, 'get_ats_details_failed', null, rawArgs);
resolve({});
}
resolve(responseData);
}).catch(error => {
logger.warn(`Grid with name - ${gridName} not found`);
logger.warn(utils.formatRequest(error, null, null));
utils.sendUsageReport(bsConfig, args, error, Constants.messageTypes.ERROR, 'get_ats_details_failed', null, rawArgs);
resolve({});
});
});
} catch (err) {
logger.error(`Failed to find TurboScale Grid: ${err}: ${err.stack}`);
}
};

exports.patchCypressConfigFileContent = (bsConfig) => {
try {
let cypressConfigFileData = fs.readFileSync(path.resolve(bsConfig.run_settings.cypress_config_file)).toString();
const patchedConfigFileData = cypressConfigFileData + '\n\n' + `
let originalFunction = module.exports.e2e.setupNodeEvents;
module.exports.e2e.setupNodeEvents = (on, config) => {
const bstackOn = require("./cypressPatch.js")(on);
if (originalFunction !== null && originalFunction !== undefined) {
originalFunction(bstackOn, config);
}
return config;
}
`

let confPath = bsConfig.run_settings.cypress_config_file;
let patchedConfPathList = confPath.split(path.sep);
const uniqueNamePatchFileName = `patched_ats_config_file_${uuidv4()}.js`;
patchedConfPathList[patchedConfPathList.length - 1] = uniqueNamePatchFileName;
logger.debug("Patch file name is " + uniqueNamePatchFileName);
const patchedConfPath = patchedConfPathList.join(path.sep);

bsConfig.run_settings.patched_cypress_config_file = patchedConfPath;

fs.writeFileSync(path.resolve(bsConfig.run_settings.patched_cypress_config_file), patchedConfigFileData);
} catch(e) {
logger.error(`Encountered an error when trying to patch ATS Cypress Config File ${e}`);
return {};
}
}

exports.atsFileCleanup = (bsConfig) => {
const filePath = path.resolve(bsConfig.run_settings.patched_cypress_config_file);
if(fs.existsSync(filePath)){
fs.unlinkSync(filePath);
}
}
74 changes: 44 additions & 30 deletions bin/helpers/build.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
'use strict';
const request = require('request');
const axios = require('axios').default;

const config = require('./config'),
capabilityHelper = require("../helpers/capabilityHelper"),
Constants = require('../helpers/constants'),
utils = require('../helpers/utils'),
logger = require('../helpers/logger').winstonLogger;

const { setAxiosProxy } = require('./helper');

const createBuild = (bsConfig, zip) => {
return new Promise(function (resolve, reject) {
capabilityHelper.caps(bsConfig, zip).then(function(data){
capabilityHelper.caps(bsConfig, zip).then(async function(data){
let options = {
url: config.buildUrl,
auth: {
@@ -22,39 +24,51 @@ const createBuild = (bsConfig, zip) => {
},
body: data
}
if (Constants.turboScaleObj.enabled) {
options.url = Constants.turboScaleObj.buildUrl;
}

request.post(options, function (err, resp, body) {
if (err) {
logger.error(utils.formatRequest(err, resp, body));
reject(err);
} else {
let build = null;
try {
build = JSON.parse(body);
} catch (error) {
build = null;
}
const axiosConfig = {
auth: {
username: options.auth.user,
password: options.auth.password
},
headers: options.headers
}
setAxiosProxy(axiosConfig);

if (resp.statusCode == 299) {
if (build) {
resolve(build.message);
} else {
logger.error(utils.formatRequest(err, resp, body));
reject(Constants.userMessages.API_DEPRECATED);
}
} else if (resp.statusCode != 201) {
logger.error(utils.formatRequest(err, resp, body));
if (build) {
reject(`${Constants.userMessages.BUILD_FAILED} Error: ${build.message}`);
} else {
reject(Constants.userMessages.BUILD_FAILED);
}
try {
const response = await axios.post(options.url, data, axiosConfig);
let build = null;
try {
build = response.data;
} catch (error) {
build = null;
}
if (response.status == 299) {
if (build) {
resolve(build.message);
} else {
logger.error(utils.formatRequest(response.statusText, response, response.data));
reject(Constants.userMessages.API_DEPRECATED);
}
} else if (response.status != 201) {
logger.error(utils.formatRequest(response.statusText, response, response.data));
if (build) {
reject(`${Constants.userMessages.BUILD_FAILED} Error: ${build.message}`);
} else {
resolve(build);
reject(Constants.userMessages.BUILD_FAILED);
}
resolve(build);
}
})
resolve(build);
} catch (error) {
if(error.response) {
logger.error(utils.formatRequest(error.response.statusText, error.response, error.response.data));
reject(`${Constants.userMessages.BUILD_FAILED} Error: ${error.response.data.message}`);
} else {
reject(error);
}
}
}).catch(function(err){
reject(err);
});
219 changes: 134 additions & 85 deletions bin/helpers/buildArtifacts.js
Original file line number Diff line number Diff line change
@@ -8,14 +8,17 @@ const logger = require('./logger').winstonLogger,
Constants = require("./constants"),
config = require("./config");

const request = require('request');
const { default: axios } = require('axios');
const HttpsProxyAgent = require('https-proxy-agent');
const FormData = require('form-data');
const decompress = require('decompress');

const unzipper = require("unzipper");
const { setAxiosProxy } = require('./helper');

let BUILD_ARTIFACTS_TOTAL_COUNT = 0;
let BUILD_ARTIFACTS_FAIL_COUNT = 0;

const parseAndDownloadArtifacts = async (buildId, data) => {
const parseAndDownloadArtifacts = async (buildId, data, bsConfig, args, rawArgs, buildReportData) => {
return new Promise(async (resolve, reject) => {
let all_promises = [];
let combs = Object.keys(data);
@@ -28,7 +31,17 @@ const parseAndDownloadArtifacts = async (buildId, data) => {
let fileName = 'build_artifacts.zip';
BUILD_ARTIFACTS_TOTAL_COUNT += 1;
all_promises.push(downloadAndUnzip(filePath, fileName, data[comb][sessionId]).catch((error) => {
BUILD_ARTIFACTS_FAIL_COUNT += 1;
if (error === Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_NOT_FOUND) {
// Don't consider build artifact 404 error as a failure
let warningMessage = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_NOT_FOUND.replace('<session-id>', sessionId);
logger.warn(warningMessage);
utils.sendUsageReport(bsConfig, args, warningMessage, Constants.messageTypes.ERROR, 'build_artifacts_not_found', buildReportData, rawArgs);
} else {
BUILD_ARTIFACTS_FAIL_COUNT += 1;
const errorMsg = `Error downloading build artifacts for ${sessionId} with error: ${error}`;
logger.debug(errorMsg);
utils.sendUsageReport(bsConfig, args, errorMsg, Constants.messageTypes.ERROR, 'build_artifacts_parse_error', buildReportData, rawArgs);
}
// delete malformed zip if present
let tmpFilePath = path.join(filePath, fileName);
if(fs.existsSync(tmpFilePath)){
@@ -95,44 +108,70 @@ const downloadAndUnzip = async (filePath, fileName, url) => {
let tmpFilePath = path.join(filePath, fileName);
const writer = fs.createWriteStream(tmpFilePath);

logger.debug(`Downloading build artifact for: ${filePath}`)
return new Promise(async (resolve, reject) => {
request.get(url).on('response', function(response) {

if(response.statusCode != 200) {
reject();
try {
const axiosConfig = {
responseType: 'stream',
validateStatus: status => (status >= 200 && status < 300) || status === 404
};
setAxiosProxy(axiosConfig);
const response = await axios.get(url, axiosConfig);
if(response.status != 200) {
if (response.status === 404) {
reject(Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_NOT_FOUND);
}
const errorMsg = `Non 200 status code, got status code: ${response.status}`;
reject(errorMsg);
} else {
//ensure that the user can call `then()` only when the file has
//been downloaded entirely.
response.pipe(writer);
response.data.pipe(writer);
let error = null;
writer.on('error', err => {
error = err;
writer.close();
reject(err);
});
writer.on('close', async () => {
if (!error) {
await unzipFile(filePath, fileName);
fs.unlinkSync(tmpFilePath);
resolve(true);
try {
if (!error) {
await unzipFile(filePath, fileName);
fs.unlinkSync(tmpFilePath);
}
} catch (error) {
reject(error);
}
//no need to call the reject here, as it will have been called in the
//'error' stream;
resolve(true);
});
}
});
} catch (error) {
reject(error);
}
});
}

const unzipFile = async (filePath, fileName) => {
return new Promise( async (resolve, reject) => {
await decompress(path.join(filePath, fileName), filePath)
.then((files) => {
try {
await decompress(path.join(filePath, fileName), filePath);
resolve();
})
.catch((error) => {
reject(error);
});
} catch (error) {
logger.debug(`Error unzipping with decompress, trying with unzipper. Stacktrace: ${error}.`);
try {
fs.createReadStream(path.join(filePath, fileName))
.pipe(unzipper.Extract({ path: filePath }))
.on("close", () => {
resolve();
})
.on("error", (err) => {
reject(err);
});
} catch (unzipperError) {
logger.debug(`Unzipper package error: ${unzipperError}`);
reject(Constants.userMessages.BUILD_ARTIFACTS_UNZIP_FAILURE);
}
}
});
}

@@ -159,38 +198,50 @@ const sendUpdatesToBstack = async (bsConfig, buildId, args, options, rawArgs, bu
}

options.formData = data.toString();
const axiosConfig = {
auth: {
username: options.auth.username,
password: options.auth.password
},
headers: options.headers
};
setAxiosProxy(axiosConfig);

let responseData = null;
return new Promise (async (resolve, reject) => {
request.post(options, function (err, resp, data) {
if(err) {
utils.sendUsageReport(bsConfig, args, err, Constants.messageTypes.ERROR, 'api_failed_build_artifacts_status_update', buildReportData, rawArgs);
logger.error(utils.formatRequest(err, resp, data));
reject(err);
} else {
try {
responseData = JSON.parse(data);
} catch(e) {
responseData = {};
}
if (resp.statusCode != 200) {
if (responseData && responseData["error"]) {
utils.sendUsageReport(bsConfig, args, responseData["error"], Constants.messageTypes.ERROR, 'api_failed_build_artifacts_status_update', buildReportData, rawArgs);
reject(responseData["error"])
}
try {
const response = await axios.post(options.url, data, axiosConfig);
try {
responseData = response.data;
} catch(e) {
responseData = {};
}
if (response.status != 200) {
if (responseData && responseData["error"]) {
utils.sendUsageReport(bsConfig, args, responseData["error"], Constants.messageTypes.ERROR, 'api_failed_build_artifacts_status_update', buildReportData, rawArgs);
reject(responseData["error"])
}
}
resolve()
});
resolve();
} catch (error) {
if(error.response) {
utils.sendUsageReport(bsConfig, args, error.response, Constants.messageTypes.ERROR, 'api_failed_build_artifacts_status_update', buildReportData, rawArgs);
logger.error(utils.formatRequest(error.response.statusText, error.response, error.response.data));
reject(errror.response.data.message);
}
}
});
}

exports.downloadBuildArtifacts = async (bsConfig, buildId, args, rawArgs, buildReportData = null) => {
exports.downloadBuildArtifacts = async (bsConfig, buildId, args, rawArgs, buildReportData = null, isTurboScaleSession = false) => {
return new Promise ( async (resolve, reject) => {
BUILD_ARTIFACTS_FAIL_COUNT = 0;
BUILD_ARTIFACTS_TOTAL_COUNT = 0;

let options = {
url: `${config.buildUrl}${buildId}/build_artifacts`,
url: isTurboScaleSession
? `${config.turboScaleBuildsUrl}/${buildId}/build_artifacts`
: `${config.buildUrl}${buildId}/build_artifacts`,
auth: {
username: bsConfig.auth.username,
password: bsConfig.auth.access_key,
@@ -204,53 +255,51 @@ exports.downloadBuildArtifacts = async (bsConfig, buildId, args, rawArgs, buildR
let messageType = null;
let errorCode = null;
let buildDetails = null;
request.get(options, async function (err, resp, body) {
if(err) {
logger.error(utils.formatRequest(err, resp, body));
utils.sendUsageReport(bsConfig, args, err, Constants.messageTypes.ERROR, 'api_failed_build_artifacts', buildReportData, rawArgs);
options.config = {
auth: options.auth,
headers: options.headers
}
setAxiosProxy(options.config);
let response;
try {
response = await axios.get(options.url, options.config);
buildDetails = response.data;
await createDirectories(buildId, buildDetails);
await parseAndDownloadArtifacts(buildId, buildDetails, bsConfig, args, rawArgs, buildReportData);
if (BUILD_ARTIFACTS_FAIL_COUNT > 0) {
messageType = Constants.messageTypes.ERROR;
message = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_FAILED.replace('<build-id>', buildId).replace('<machine-count>', BUILD_ARTIFACTS_FAIL_COUNT);
logger.error(message);
process.exitCode = Constants.ERROR_EXIT_CODE;
} else {
try {
buildDetails = JSON.parse(body);
if(resp.statusCode != 200) {
logger.error('Downloading the build artifacts failed.');
logger.error(`Error: Request failed with status code ${resp.statusCode}`)
logger.error(utils.formatRequest(err, resp, body));
utils.sendUsageReport(bsConfig, args, buildDetails, Constants.messageTypes.ERROR, 'api_failed_build_artifacts', buildReportData, rawArgs);
process.exitCode = Constants.ERROR_EXIT_CODE;
} else {
await createDirectories(buildId, buildDetails);
await parseAndDownloadArtifacts(buildId, buildDetails);
if (BUILD_ARTIFACTS_FAIL_COUNT > 0) {
messageType = Constants.messageTypes.ERROR;
message = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_FAILED.replace('<build-id>', buildId).replace('<machine-count>', BUILD_ARTIFACTS_FAIL_COUNT);
logger.error(message);
process.exitCode = Constants.ERROR_EXIT_CODE;
} else {
messageType = Constants.messageTypes.SUCCESS;
message = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_SUCCESS.replace('<build-id>', buildId).replace('<user-path>', process.cwd());
logger.info(message);
}
await sendUpdatesToBstack(bsConfig, buildId, args, options, rawArgs, buildReportData)
utils.sendUsageReport(bsConfig, args, message, messageType, null, buildReportData, rawArgs);
}
} catch (err) {
messageType = Constants.messageTypes.SUCCESS;
message = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_SUCCESS.replace('<build-id>', buildId).replace('<user-path>', process.cwd());
logger.info(message);
}
await sendUpdatesToBstack(bsConfig, buildId, args, options, rawArgs, buildReportData)
utils.sendUsageReport(bsConfig, args, message, messageType, null, buildReportData, rawArgs);
} catch (err) {
messageType = Constants.messageTypes.ERROR;
errorCode = 'api_failed_build_artifacts';
if(err.response && err.response.status !== 200) {
logger.error('Downloading the build artifacts failed.');
logger.error(`Error: Request failed with status code ${err.response.status}`)
logger.error(utils.formatRequest(err.response.statusText, err.response, err.response.data));
utils.sendUsageReport(bsConfig, args, JSON.stringify(buildDetails), Constants.messageTypes.ERROR, 'api_failed_build_artifacts', buildReportData, rawArgs);
} else {
if (BUILD_ARTIFACTS_FAIL_COUNT > 0) {
messageType = Constants.messageTypes.ERROR;
errorCode = 'api_failed_build_artifacts';
if (BUILD_ARTIFACTS_FAIL_COUNT > 0) {
messageType = Constants.messageTypes.ERROR;
message = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_FAILED.replace('<build-id>', buildId).replace('<machine-count>', BUILD_ARTIFACTS_FAIL_COUNT);
logger.error(message);
} else {
logger.error('Downloading the build artifacts failed.');
}
utils.sendUsageReport(bsConfig, args, err, messageType, errorCode, buildReportData, rawArgs);
logger.error(`Error: Request failed with status code ${resp.statusCode}`)
logger.error(utils.formatRequest(err, resp, body));
process.exitCode = Constants.ERROR_EXIT_CODE;
message = Constants.userMessages.DOWNLOAD_BUILD_ARTIFACTS_FAILED.replace('<build-id>', buildId).replace('<machine-count>', BUILD_ARTIFACTS_FAIL_COUNT);
logger.error(message);
} else {
logger.error('Downloading the build artifacts failed.');
}
utils.sendUsageReport(bsConfig, args, err, messageType, errorCode, buildReportData, rawArgs);
logger.error(`Error: Request failed with status code ${resp.status}`)
logger.error(utils.formatRequest(err, resp, body));
}
resolve();
});
process.exitCode = Constants.ERROR_EXIT_CODE;
}
resolve();
});
};
75 changes: 73 additions & 2 deletions bin/helpers/capabilityHelper.js
Original file line number Diff line number Diff line change
@@ -121,10 +121,20 @@ 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') {
// If any of the platform has accessibility true, make it true
bsConfig.run_settings["accessibility"] = true;
bsConfig.run_settings["accessibilityPlatforms"] = getAccessibilityPlatforms(bsConfig);
}

// send run_settings as is for other capabilities
obj.run_settings = JSON.stringify(bsConfig.run_settings);
}

obj.cypress_cli_user_agent = Utils.getUserAgent();

logger.info(`Cypress CLI User Agent: ${obj.cypress_cli_user_agent}`);

if(obj.parallels === Constants.cliMessages.RUN.DEFAULT_PARALLEL_MESSAGE) obj.parallels = undefined

if (obj.project) logger.info(`Project name is: ${obj.project}`);
@@ -138,18 +148,52 @@ 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 = [];
if (bsConfig.browsers) {
bsConfig.browsers.forEach((element) => {
element.versions.forEach((version) => {
browserList.push({...element, version, platform: element.os + "-" + element.browser});
});
});
}

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;
if (Utils.isUndefined(bsConfig.run_settings.headless) || !(String(bsConfig.run_settings.headless) === "false")) {
logger.warn(`Accessibility Automation will not run on legacy headless mode. Switch to new headless mode or avoid using headless mode for ${browserDetails.platform}.`);
} else if (browserDetails.browser && browserDetails.browser.toLowerCase() !== 'chrome') {
logger.warn(`Accessibility Automation will run only on Chrome browsers for ${browserDetails.platform}.`);
} else if (browserDetails.version && !browserDetails.version.includes('latest') && browserDetails.version <= 94) {
logger.warn(`Accessibility Automation will run only on Chrome browser version greater than 94 for ${browserDetails.platform}.`);
}
});
return accessibilityPlatforms;
}

const addCypressZipStartLocation = (runSettings) => {
let resolvedHomeDirectoryPath = path.resolve(runSettings.home_directory);
let resolvedCypressConfigFilePath = path.resolve(runSettings.cypressConfigFilePath);
runSettings.cypressZipStartLocation = path.dirname(resolvedCypressConfigFilePath.split(resolvedHomeDirectoryPath)[1]);

// Convert to POSIX style paths for consistent behavior
let posixHomePath = resolvedHomeDirectoryPath.split(path.sep).join(path.posix.sep);
let posixConfigPath = resolvedCypressConfigFilePath.split(path.sep).join(path.posix.sep);

runSettings.cypressZipStartLocation = path.posix.dirname(posixConfigPath.split(posixHomePath)[1]);
runSettings.cypressZipStartLocation = runSettings.cypressZipStartLocation.substring(1);
logger.debug(`Setting cypress zip start location = ${runSettings.cypressZipStartLocation}`);
}


const validate = (bsConfig, args) => {
return new Promise(function (resolve, reject) {
logger.info(Constants.userMessages.VALIDATING_CONFIG);
@@ -167,6 +211,10 @@ const validate = (bsConfig, args) => {
reject(Constants.validationMessages.EMPTY_CYPRESS_CONFIG_FILE);
}

if ( bsConfig && bsConfig.run_settings && bsConfig.run_settings.enforce_settings && bsConfig.run_settings.enforce_settings.toString() === 'true' && Utils.isUndefined(bsConfig.run_settings.specs) ) {
reject(Constants.validationMessages.EMPTY_SPECS_IN_BROWSERSTACK_JSON);
}

// validate parallels specified in browserstack.json if parallels are not specified via arguments
if (!Utils.isUndefined(args) && Utils.isUndefined(args.parallels) && !Utils.isParallelValid(bsConfig.run_settings.parallels)) reject(Constants.validationMessages.INVALID_PARALLELS_CONFIGURATION);

@@ -197,7 +245,8 @@ const validate = (bsConfig, args) => {

logger.debug(`Validating ${bsConfig.run_settings.cypress_config_filename}`);
try {
if (bsConfig.run_settings.cypress_config_filename !== 'false') {
// Not reading cypress config file upon enforce_settings
if (Utils.isUndefinedOrFalse(bsConfig.run_settings.enforce_settings) && bsConfig.run_settings.cypress_config_filename !== 'false') {
if (bsConfig.run_settings.cypressTestSuiteType === Constants.CYPRESS_V10_AND_ABOVE_TYPE) {
const completeCypressConfigFile = readCypressConfigFile(bsConfig)
if (!Utils.isUndefined(completeCypressConfigFile)) {
@@ -217,6 +266,11 @@ const validate = (bsConfig, args) => {
// Detect if the user is not using the right directory structure, and throw an error
if (!Utils.isUndefined(cypressConfigFile.integrationFolder) && !Utils.isCypressProjDirValid(bsConfig.run_settings.cypressProjectDir,cypressConfigFile.integrationFolder)) reject(Constants.validationMessages.INCORRECT_DIRECTORY_STRUCTURE);
}
else {
logger.debug("Validating baseurl and integrationFolder in browserstack.json");
if (!Utils.isUndefined(bsConfig.run_settings.baseUrl) && bsConfig.run_settings.baseUrl.includes("localhost") && !Utils.getLocalFlag(bsConfig.connection_settings)) reject(Constants.validationMessages.LOCAL_NOT_SET.replace("<baseUrlValue>", bsConfig.run_settings.baseUrl));
if (!Utils.isUndefined(bsConfig.run_settings.integrationFolder) && !Utils.isCypressProjDirValid(bsConfig.run_settings.cypressProjectDir,bsConfig.run_settings.integrationFolder)) reject(Constants.validationMessages.INCORRECT_DIRECTORY_STRUCTURE);
}
} catch(error){
reject(Constants.validationMessages.INVALID_CYPRESS_JSON)
}
@@ -242,6 +296,23 @@ const validate = (bsConfig, args) => {
addCypressZipStartLocation(bsConfig.run_settings);
}

// check if Interactive Capabilities Caps passed is correct or not
if(!Utils.isUndefined(bsConfig.run_settings.interactive_debugging) && !Utils.isUndefined(bsConfig.run_settings.interactiveDebugging)) {
if(Utils.isConflictingBooleanValues(bsConfig.run_settings.interactive_debugging, bsConfig.run_settings.interactiveDebugging)) {
reject(Constants.userMessages.CYPRESS_INTERACTIVE_SESSION_CONFLICT_VALUES);
} else if(Utils.isNonBooleanValue(bsConfig.run_settings.interactive_debugging) && Utils.isNonBooleanValue(bsConfig.run_settings.interactiveDebugging)) {
logger.warn('You have passed an invalid value to the interactive_debugging capability. Proceeding with the default value (True).');
}
} else if(!Utils.isUndefined(bsConfig.run_settings.interactive_debugging)) {
if(Utils.isNonBooleanValue(bsConfig.run_settings.interactive_debugging)) {
logger.warn('You have passed an invalid value to the interactive_debugging capability. Proceeding with the default value (True).');
}
} else if(!Utils.isUndefined(bsConfig.run_settings.interactiveDebugging)) {
if(Utils.isNonBooleanValue(bsConfig.run_settings.interactiveDebugging)) {
logger.warn('You have passed an invalid value to the interactive_debugging capability. Proceeding with the default value (True).');
}
}

// check if two config files are present at the same location
let cypressFileDirectory = path.dirname(path.resolve(bsConfig.run_settings.cypressConfigFilePath));
let listOfFiles = fs.readdirSync(cypressFileDirectory);
92 changes: 54 additions & 38 deletions bin/helpers/checkUploaded.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use strict';
const request = require('request');
const { default: axios } = require('axios');
const { combineMacWinNpmDependencies } = require('./helper');

const crypto = require('crypto'),
Constants = require('./constants'),
@@ -10,12 +11,10 @@ const crypto = require('crypto'),
utils = require('./utils'),
logger = require('./logger').winstonLogger;

const { setAxiosProxy } = require('./helper');

const checkSpecsMd5 = (runSettings, args, instrumentBlocks) => {
return new Promise(function (resolve, reject) {
if (args["force-upload"]) {
return resolve("force-upload");
}
let cypressFolderPath = undefined;
if (runSettings.home_directory) {
cypressFolderPath = runSettings.home_directory;
@@ -62,7 +61,7 @@ const checkPackageMd5 = (runSettings) => {

if (typeof runSettings.npm_dependencies === 'object') {
Object.assign(packageJSON, {
devDependencies: utils.sortJsonKeys(runSettings.npm_dependencies),
devDependencies: utils.sortJsonKeys(combineMacWinNpmDependencies(runSettings)),
});
}

@@ -93,7 +92,7 @@ const checkUploadedMd5 = (bsConfig, args, instrumentBlocks) => {
}

instrumentBlocks.markBlockStart("checkAlreadyUploaded.md5Total");
checkSpecsMd5(bsConfig.run_settings, args, instrumentBlocks).then(function (zip_md5sum) {
checkSpecsMd5(bsConfig.run_settings, args, instrumentBlocks).then(async function (zip_md5sum) {
instrumentBlocks.markBlockStart("checkAlreadyUploaded.md5Package");
let npm_package_md5sum = checkPackageMd5(bsConfig.run_settings);
instrumentBlocks.markBlockEnd("checkAlreadyUploaded.md5Package");
@@ -118,48 +117,65 @@ const checkUploadedMd5 = (bsConfig, args, instrumentBlocks) => {
'Content-Type': 'application/json',
"User-Agent": utils.getUserAgent(),
},
body: JSON.stringify(data)
body: data
};

if (Constants.turboScaleObj.enabled) {
options.url = config.turboScaleMd5Sum;
}

instrumentBlocks.markBlockStart("checkAlreadyUploaded.railsCheck");
request.post(options, function (err, resp, body) {
if (err) {
instrumentBlocks.markBlockEnd("checkAlreadyUploaded.railsCheck");
resolve(obj);
} else {
let zipData = null;
try {
zipData = JSON.parse(body);
} catch (error) {
zipData = {};
}
if (resp.statusCode === 200) {
if (!utils.isUndefined(zipData.zipUrl)) {
Object.assign(obj, zipData, {zipUrlPresent: true});
}
if (!utils.isUndefined(zipData.npmPackageUrl)) {
Object.assign(obj, zipData, {packageUrlPresent: true});
}
}
if (utils.isTrueString(zipData.disableNpmSuiteCache)) {
bsConfig.run_settings.cache_dependencies = false;
Object.assign(obj, {packageUrlPresent: false});
delete obj.npm_package_md5sum;

const axiosConfig = {
auth: {
username: options.auth.user,
password: options.auth.password
},
headers: options.headers
};
setAxiosProxy(axiosConfig);

try {
const response = await axios.post(options.url, options.body, axiosConfig);
let zipData = null;
try {
zipData = response.data;
} catch (error) {
zipData = {};
}
if (response.status === 200) {
if (!utils.isUndefined(zipData.zipUrl)) {
Object.assign(obj, zipData, {zipUrlPresent: true});
}
if (utils.isTrueString(zipData.disableTestSuiteCache)) {
args["force-upload"] = true;
Object.assign(obj, {zipUrlPresent: false});
delete obj.zip_md5sum;
if (!utils.isUndefined(zipData.npmPackageUrl)) {
Object.assign(obj, zipData, {packageUrlPresent: true});
}
instrumentBlocks.markBlockEnd("checkAlreadyUploaded.railsCheck");
resolve(obj);
}
});
if (utils.isTrueString(zipData.disableNpmSuiteCache)) {
bsConfig.run_settings.cache_dependencies = false;
Object.assign(obj, {packageUrlPresent: false});
delete obj.npm_package_md5sum;
}
if (utils.isTrueString(zipData.disableTestSuiteCache)) {
args["force-upload"] = true;
Object.assign(obj, {zipUrlPresent: false});
delete obj.zip_md5sum;
}
instrumentBlocks.markBlockEnd("checkAlreadyUploaded.railsCheck");
resolve(obj);
} catch (error) {
instrumentBlocks.markBlockEnd("checkAlreadyUploaded.railsCheck");
resolve(obj);
}
}).catch((err) => {
let errString = err.stack ? err.stack.toString().substring(0,100) : err.toString().substring(0,100);
resolve({zipUrlPresent: false, packageUrlPresent: false, error: errString});
});
});
};

exports.checkUploadedMd5 = checkUploadedMd5;
module.exports = {
checkSpecsMd5,
checkPackageMd5,
checkUploadedMd5
};
8 changes: 6 additions & 2 deletions bin/helpers/config.js
Original file line number Diff line number Diff line change
@@ -24,7 +24,11 @@ config.packageFileName = "bstackPackages.tar.gz";
config.packageDirName = "tmpBstackPackages";
config.retries = 5;
config.networkErrorExitCode = 2;
config.compiledConfigJsDirName = 'tmpBstackCompiledJs'
config.configJsonFileName = 'tmpCypressConfig.json'
config.compiledConfigJsDirName = 'tmpBstackCompiledJs';
config.configJsonFileName = 'tmpCypressConfig.json';

// turboScale
config.turboScaleMd5Sum = `${config.turboScaleUrl}/md5sumcheck`;
config.turboScaleBuildsUrl = `${config.turboScaleUrl}/builds`;

module.exports = config;
4 changes: 3 additions & 1 deletion bin/helpers/config.json
Original file line number Diff line number Diff line change
@@ -3,5 +3,7 @@
"rails_host": "https://api.browserstack.com",
"dashboardUrl": "https://automate.browserstack.com/dashboard/v2/builds/",
"usageReportingUrl": "https://eds.browserstack.com:443/send_event_cy_internal",
"localTestingListUrl": "https://www.browserstack.com/local/v1/list"
"localTestingListUrl": "https://www.browserstack.com/local/v1/list",
"turboScaleUrl": "https://grid.browserstack.com/packages/cypress",
"turboScaleAPIUrl": "https://api.browserstack.com/automate-turboscale/v1"
}
57 changes: 53 additions & 4 deletions bin/helpers/constants.js
Original file line number Diff line number Diff line change
@@ -57,6 +57,7 @@ const userMessages = {
FAILED_MD5_CHECK:
"Something went wrong - you can retry running browserstack-cypress with ‘--force-upload’ parameter, or contact BrowserStack Support.",
VISIT_DASHBOARD: "Visit the Automate dashboard for real-time test reporting:",
VISIT_ATS_DASHBOARD: "Visit the Automate TurboScale dashboard for real-time test reporting:",
CONFLICTING_INIT_ARGUMENTS:
"Conflicting arguments given. You can use --path only with a file name, and not with a file path.",
NO_PARALLELS:
@@ -72,6 +73,8 @@ const userMessages = {
UPLOADING_NPM_PACKAGES_SUCCESS: "Uploaded node_modules successfully",
SKIP_UPLOADING_TESTS:
"Skipping zip upload since BrowserStack already has your test suite that has not changed since the last run.",
SKIP_NPM_INSTALL:
"Skipping NPM Install as the enforce_settings has been passed.",
SKIP_UPLOADING_NPM_PACKAGES:
"Skipping the upload of node_modules since BrowserStack has already cached your npm dependencies that have not changed since the last run.",
LOCAL_TRUE: "you will now be able to test localhost / private URLs",
@@ -94,7 +97,9 @@ const userMessages = {
SPEC_LIMIT_WARNING:
"You might not see all your results on the dashboard because of high spec count, please consider reducing the number of spec files in this folder.",
DOWNLOAD_BUILD_ARTIFACTS_FAILED:
"Downloading build artifacts for the build <build-id> failed for <machine-count> machines.",
"Downloading build artifact(s) for the build <build-id> failed for <machine-count> machines.",
DOWNLOAD_BUILD_ARTIFACTS_NOT_FOUND:
"Build artifact(s) for the session <session-id> was either not generated or not uploaded.",
ASYNC_DOWNLOADS:
"Test artifacts as specified under 'downloads' can be downloaded after the build has completed its run, using 'browserstack-cypress generate-downloads <build-id>'",
DOWNLOAD_BUILD_ARTIFACTS_SUCCESS:
@@ -117,7 +122,10 @@ const userMessages = {
NO_CONNECTION_WHILE_UPDATING_UPLOAD_PROGRESS_BAR:
"Unable to determine zip upload progress due to undefined/null connection request",
CYPRESS_PORT_WARNING:
"The requested port number <x> is ignored. The default BrowserStack port will be used for this execution"
"The requested port number <x> is ignored. The default BrowserStack port will be used for this execution",
CYPRESS_INTERACTIVE_SESSION_CONFLICT_VALUES:
"Conflicting values (True & False) were found for the interactive_debugging capability. Please resolve this issue to proceed further.",
BUILD_ARTIFACTS_UNZIP_FAILURE: "Failed to unzip build artifacts.",
};

const validationMessages = {
@@ -130,8 +138,10 @@ const validationMessages = {
"cypress_proj_dir is not set in run_settings. See https://www.browserstack.com/docs/automate/cypress/sample-tutorial to learn more.",
EMPTY_CYPRESS_CONFIG_FILE:
"cypress_config_file is not set in run_settings. See https://www.browserstack.com/docs/automate/cypress/configuration-file to learn more.",
EMPTY_SPECS_IN_BROWSERSTACK_JSON:
"specs is required when enforce_settings is true in run_settings of browserstack.json",
VALIDATED: "browserstack.json file is validated",
NOT_VALID: "browerstack.json is not valid",
NOT_VALID: "browserstack.json is not valid",
NOT_VALID_JSON: "browerstack.json is not a valid json",
INVALID_EXTENSION: "Invalid files, please remove these files and try again.",
INVALID_PARALLELS_CONFIGURATION:
@@ -186,6 +196,28 @@ const validationMessages = {
"You have specified '--record' flag but you've not provided the '--record-key' and we could not find any value in 'CYPRESS_RECORD_KEY' environment variable. Your record functionality on cypress.io dashboard might not work as it needs the key and projectId",
NODE_VERSION_PARSING_ERROR:
"We weren't able to successfully parse the specified nodeVersion. We will be using the default nodeVersion to run your tests.",
AUTO_IMPORT_CONFLICT_ERROR:
"Cannot use both 'auto_import_dev_dependencies' and manual npm dependency configuration. Please either set 'auto_import_dev_dependencies' to false or remove manual 'npm_dependencies', 'win_npm_dependencies', and 'mac_npm_dependencies' configurations.",
AUTO_IMPORT_INVALID_TYPE:
"'auto_import_dev_dependencies' must be a boolean value (true or false).",
PACKAGE_JSON_NOT_FOUND:
"package.json not found in project directory. Cannot auto-import devDependencies.",
PACKAGE_JSON_PERMISSION_DENIED:
"Cannot read package.json due to permission issues. Please check file permissions.",
PACKAGE_JSON_MALFORMED:
"package.json contains invalid JSON syntax. Please fix the JSON format.",
PACKAGE_JSON_NOT_OBJECT:
"package.json must contain a JSON object, not an array or other type.",
DEVDEPS_INVALID_FORMAT:
"devDependencies field in package.json must be an object, not an array or other type.",
EXCLUDE_DEPS_INVALID_TYPE:
"'exclude_dependencies' must be an array of strings.",
EXCLUDE_DEPS_INVALID_PATTERNS:
"'exclude_dependencies' must contain only string values representing regex patterns.",
INVALID_REGEX_PATTERN:
"Invalid regex pattern found in 'exclude_dependencies': {pattern}. Please provide valid regex patterns.",
DEPENDENCIES_PARAM_INVALID:
"Dependencies parameter must be an object.",
};

const cliMessages = {
@@ -334,7 +366,11 @@ const filesToIgnoreWhileUploading = [
"package.json",
"**/package.json",
"browserstack-package.json",
"browserstack-mac-package.json",
"browserstack-win-package.json",
"**/browserstack-package.json",
"**/browserstack-mac-package.json",
"**/browserstack-win-package.json",
"tests.zip",
"**/tests.zip",
"cypress.json",
@@ -433,6 +469,16 @@ const CYPRESS_CONFIG_FILE_NAMES = Object.keys(CYPRESS_CONFIG_FILE_MAPPING);

const CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS = ['js', 'ts', 'cjs', 'mjs']

// Maximum size of VCS info which is allowed
const MAX_GIT_META_DATA_SIZE_IN_BYTES = 64 * 1024;

/* The value to be appended at the end if git metadata is larger than
MAX_GIT_META_DATA_SIZE_IN_BYTES
*/
const GIT_META_DATA_TRUNCATED = '...[TRUNCATED]';

const turboScaleObj = {};

module.exports = Object.freeze({
syncCLI,
userMessages,
@@ -445,6 +491,7 @@ module.exports = Object.freeze({
hashingOptions,
packageInstallerOptions,
specFileTypes,
turboScaleObj,
DEFAULT_CYPRESS_SPEC_PATH,
DEFAULT_CYPRESS_10_SPEC_PATH,
SPEC_TOTAL_CHAR_LIMIT,
@@ -464,5 +511,7 @@ module.exports = Object.freeze({
CYPRESS_V10_AND_ABOVE_TYPE,
CYPRESS_CONFIG_FILE_MAPPING,
CYPRESS_CONFIG_FILE_NAMES,
CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS
CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS,
MAX_GIT_META_DATA_SIZE_IN_BYTES,
GIT_META_DATA_TRUNCATED
});
37 changes: 23 additions & 14 deletions bin/helpers/downloadBuildStacktrace.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
'use strict'
const request = require('request');
'use strict';
const { default: axios } = require('axios');
const { setAxiosProxy } = require('./helper');

const downloadBuildStacktrace = async (url, bsConfig) => {
const axiosConfig = {
auth: {
username: bsConfig.auth.username,
password: bsConfig.auth.access_key
},
responseType: 'stream',
};
setAxiosProxy(axiosConfig);

const downloadBuildStacktrace = async (url) => {
return new Promise(async (resolve, reject) => {
request.get(url).on('response', function (response) {
if(response.statusCode == 200) {
response.pipe(process.stdout);
try {
const response = await axios.get(url, axiosConfig);
if (response.status === 200) {
response.data.pipe(process.stdout);
let error = null;
process.stdout.on('error', (err) => {
error = err;
process.stdout.close();
reject(response.statusCode);
reject(response.status);
});
process.stdout.on('close', async () => {
if(!error) {
resolve("Build stacktrace downloaded successfully");
if (!error) {
resolve('Build stacktrace downloaded successfully');
}
});
} else {
reject(response.statusCode);
}
}).on('end', () => {
resolve("Build stacktrace downloaded successfully");
});
} catch (error) {
reject(error.response.status);
}
});
};

55 changes: 33 additions & 22 deletions bin/helpers/getInitialDetails.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
const request = require('request'),
logger = require('./logger').winstonLogger,
const { default: axios } = require('axios');

const logger = require('./logger').winstonLogger,
utils = require('./utils'),
config = require("./config"),
Constants = require('./constants');

const { setAxiosProxy } = require('./helper');

exports.getInitialDetails = (bsConfig, args, rawArgs) => {
return new Promise((resolve, reject) => {
return new Promise(async (resolve, reject) => {
let options = {
url: config.getInitialDetails,
auth: {
@@ -17,28 +20,36 @@ exports.getInitialDetails = (bsConfig, args, rawArgs) => {
}
};
let responseData = {};
request.get(options, function (err, resp, data) {
if(err) {
logger.warn(utils.formatRequest(err, resp, data));
utils.sendUsageReport(bsConfig, args, err, Constants.messageTypes.ERROR, 'get_initial_details_failed', null, rawArgs);

const axiosConfig = {
auth: options.auth,
headers: options.headers,
}
setAxiosProxy(axiosConfig);

try {
const response = await axios.get(options.url, axiosConfig);
try {
responseData = response.data;
} catch (e) {
responseData = {};
}
if(response.status != 200) {
logger.warn(`Warn: Get Initial Details Request failed with status code ${response.status}`);
utils.sendUsageReport(bsConfig, args, responseData["error"], Constants.messageTypes.ERROR, 'get_initial_details_failed', null, rawArgs);
resolve({});
} else {
try {
responseData = JSON.parse(data);
} catch (e) {
responseData = {};
}
if(resp.statusCode != 200) {
logger.warn(`Warn: Get Initial Details Request failed with status code ${resp.statusCode}`);
utils.sendUsageReport(bsConfig, args, responseData["error"], Constants.messageTypes.ERROR, 'get_initial_details_failed', null, rawArgs);
resolve({});
} else {
if (!utils.isUndefined(responseData.grr) && responseData.grr.enabled && !utils.isUndefined(responseData.grr.urls)) {
config.uploadUrl = responseData.grr.urls.upload_url;
}
resolve(responseData);
if (!utils.isUndefined(responseData.grr) && responseData.grr.enabled && !utils.isUndefined(responseData.grr.urls)) {
config.uploadUrl = responseData.grr.urls.upload_url;
}
resolve(responseData);
}
} catch (error) {
if(error.response && error.response.status !== 200) {
logger.warn(`Warn: Get Initial Details Request failed with status code ${error.response.status}`);
utils.sendUsageReport(bsConfig, args, error.response.data["error"], Constants.messageTypes.ERROR, 'get_initial_details_failed', null, rawArgs);
}
});
resolve({});
}
});
};
456 changes: 456 additions & 0 deletions bin/helpers/helper.js

Large diffs are not rendered by default.

29 changes: 24 additions & 5 deletions bin/helpers/packageInstaller.js
Original file line number Diff line number Diff line change
@@ -9,8 +9,11 @@
{ get_version } = require('./usageReporting'),
process = require('process'),
{ spawn } = require('child_process'),
cliUtils = require("./utils"),
util = require('util');

const { combineMacWinNpmDependencies } = require("./helper");

let nodeProcess;

const setupPackageFolder = (runSettings, directoryPath) => {
@@ -28,9 +31,11 @@ const setupPackageFolder = (runSettings, directoryPath) => {
Object.assign(packageJSON, runSettings.package_config_options);
}

if (typeof runSettings.npm_dependencies === 'object') {
// Combine win and mac specific dependencies if present
const combinedDependencies = combineMacWinNpmDependencies(runSettings);
if (combinedDependencies && Object.keys(combinedDependencies).length > 0) {
Object.assign(packageJSON, {
devDependencies: runSettings.npm_dependencies,
devDependencies: combinedDependencies,
});
}

@@ -58,7 +63,7 @@ const setupPackageFolder = (runSettings, directoryPath) => {
})
};

const packageInstall = (packageDir) => {
const packageInstall = (packageDir, bsConfig) => {
return new Promise(function (resolve, reject) {
const nodeProcessCloseCallback = (code) => {
if(code == 0) {
@@ -74,6 +79,16 @@ const packageInstall = (packageDir) => {
reject(`Packages were not installed successfully. Error Description ${util.format('%j', error)}`);
};

// Moving .npmrc to tmpBstackPackages
try {
logger.debug(`Copying .npmrc file to temporary package directory`);
const npmrcRootPath = path.join(cliUtils.isNotUndefined(bsConfig.run_settings.home_directory) ? path.resolve(bsConfig.run_settings.home_directory) : './', '.npmrc');
const npmrcTmpPath = path.join(path.resolve(packageDir), '.npmrc');
fs.copyFileSync(npmrcRootPath, npmrcTmpPath);
} catch (error) {
logger.debug(`Failed copying .npmrc to ${packageDir}: ${error}`)
}

let nodeProcess;
logger.debug(`Fetching npm version and its major version`);
const npm_version = get_version('npm')
@@ -133,7 +148,11 @@ const packageSetupAndInstaller = (bsConfig, packageDir, instrumentBlocks) => {
let obj = {
packagesInstalled: false
};

if (bsConfig && bsConfig.run_settings && bsConfig.run_settings.enforce_settings && bsConfig.run_settings.enforce_settings.toString() === 'true' ) {
logger.info("Enforce_settings is enabled in run_settings");
logger.debug(Constants.userMessages.SKIP_NPM_INSTALL);
return resolve(obj);
}
logger.info(Constants.userMessages.NPM_INSTALL);
instrumentBlocks.markBlockStart("packageInstaller.folderSetup");
logger.debug("Started setting up package folder");
@@ -143,7 +162,7 @@ const packageSetupAndInstaller = (bsConfig, packageDir, instrumentBlocks) => {
instrumentBlocks.markBlockEnd("packageInstaller.folderSetup");
instrumentBlocks.markBlockStart("packageInstaller.packageInstall");
logger.debug("Started installing dependencies specified in browserstack.json");
return packageInstall(packageDir);
return packageInstall(packageDir, bsConfig);
}).then((_result) => {
logger.debug("Completed installing dependencies");
instrumentBlocks.markBlockEnd("packageInstaller.packageInstall");
144 changes: 133 additions & 11 deletions bin/helpers/readCypressConfigUtil.js
Original file line number Diff line number Diff line change
@@ -13,6 +13,120 @@ exports.detectLanguage = (cypress_config_filename) => {
return constants.CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS.includes(extension) ? extension : 'js'
}

function resolveTsConfigPath(bsConfig, cypress_config_filepath) {
const working_dir = path.dirname(cypress_config_filepath);

// Priority order for finding tsconfig
const candidates = [
bsConfig.run_settings && bsConfig.run_settings.ts_config_file_path, // User specified
path.join(working_dir, 'tsconfig.json'), // Same directory as cypress config
path.join(working_dir, '..', 'tsconfig.json'), // Parent directory
path.join(process.cwd(), 'tsconfig.json') // Project root
].filter(Boolean).map(p => path.resolve(p));

for (const candidate of candidates) {
if (fs.existsSync(candidate)) {
logger.debug(`Found tsconfig at: ${candidate}`);
return candidate;
}
}

return null;
}

function generateTscCommandAndTempTsConfig(bsConfig, bstack_node_modules_path, complied_js_dir, cypress_config_filepath) {
const working_dir = path.dirname(cypress_config_filepath);
const typescript_path = path.join(bstack_node_modules_path, 'typescript', 'bin', 'tsc');
const tsc_alias_path = require.resolve('tsc-alias/dist/bin/index.js');

// Smart tsconfig detection and validation
const resolvedTsConfigPath = resolveTsConfigPath(bsConfig, cypress_config_filepath);
let hasValidTsConfig = false;

if (resolvedTsConfigPath) {
try {
// Validate the tsconfig is readable and valid JSON
const tsConfigContent = fs.readFileSync(resolvedTsConfigPath, 'utf8');
JSON.parse(tsConfigContent);
hasValidTsConfig = true;
logger.info(`Using existing tsconfig: ${resolvedTsConfigPath}`);
} catch (error) {
logger.warn(`Invalid tsconfig file: ${resolvedTsConfigPath}, falling back to default configuration. Error: ${error.message}`);
hasValidTsConfig = false;
}
} else {
logger.info('No tsconfig found, using default TypeScript configuration');
}

let tempTsConfig;

if (hasValidTsConfig) {
// Scenario 1: User has valid tsconfig - use extends approach
tempTsConfig = {
extends: resolvedTsConfigPath,
compilerOptions: {
// Force override critical parameters for BrowserStack compatibility
"outDir": path.basename(complied_js_dir),
"listEmittedFiles": true,
// Ensure these are always set regardless of base tsconfig
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
include: [cypress_config_filepath]
};
} else {
// Scenario 2: No tsconfig or invalid tsconfig - create standalone with all basic parameters
tempTsConfig = {
compilerOptions: {
// Preserve old command-line parameters for backwards compatibility
"outDir": path.basename(complied_js_dir),
"listEmittedFiles": true,
"allowSyntheticDefaultImports": true,
"module": "commonjs",
"declaration": false,

// Add essential missing parameters for robust compilation
"target": "es2017",
"moduleResolution": "node",
"esModuleInterop": true,
"allowJs": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"strict": false, // Avoid breaking existing code
"noEmitOnError": false // Continue compilation even with errors
},
include: [cypress_config_filepath],
exclude: ["node_modules", "dist", "build"]
};
}

// Write the temporary tsconfig
const tempTsConfigPath = path.join(working_dir, 'tsconfig.singlefile.tmp.json');
fs.writeFileSync(tempTsConfigPath, JSON.stringify(tempTsConfig, null, 2));
logger.info(`Temporary tsconfig created at: ${tempTsConfigPath}`);

// Platform-specific command generation
const isWindows = /^win/.test(process.platform);

if (isWindows) {
// Windows: Use && to chain commands, no space after SET
const setNodePath = isWindows
? `set NODE_PATH=${bstack_node_modules_path}`
: `NODE_PATH="${bstack_node_modules_path}"`;

const tscCommand = `${setNodePath} && node "${typescript_path}" --project "${tempTsConfigPath}" && ${setNodePath} && node "${tsc_alias_path}" --project "${tempTsConfigPath}" --verbose`;
logger.info(`TypeScript compilation command: ${tscCommand}`);
return { tscCommand, tempTsConfigPath };
} else {
// Unix/Linux/macOS: Use ; to separate commands or && to chain
const nodePathPrefix = `NODE_PATH=${bstack_node_modules_path}`;
const tscCommand = `${nodePathPrefix} node "${typescript_path}" --project "${tempTsConfigPath}" && ${nodePathPrefix} node "${tsc_alias_path}" --project "${tempTsConfigPath}" --verbose`;
logger.info(`TypeScript compilation command: ${tscCommand}`);
return { tscCommand, tempTsConfigPath };
}
}

exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_modules_path) => {
const cypress_config_filename = bsConfig.run_settings.cypress_config_filename
const working_dir = path.dirname(cypress_config_filepath);
@@ -22,19 +136,12 @@ exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_module
}
fs.mkdirSync(complied_js_dir, { recursive: true })

const typescript_path = path.join(bstack_node_modules_path, 'typescript', 'bin', 'tsc')

let tsc_command = `NODE_PATH=${bstack_node_modules_path} node "${typescript_path}" --outDir "${complied_js_dir}" --listEmittedFiles true --allowSyntheticDefaultImports --module commonjs --declaration false "${cypress_config_filepath}"`
const { tscCommand, tempTsConfigPath } = generateTscCommandAndTempTsConfig(bsConfig, bstack_node_modules_path, complied_js_dir, cypress_config_filepath);

if (/^win/.test(process.platform)) {
tsc_command = `set NODE_PATH=${bstack_node_modules_path}&& node "${typescript_path}" --outDir "${complied_js_dir}" --listEmittedFiles true --allowSyntheticDefaultImports --module commonjs --declaration false "${cypress_config_filepath}"`
}


let tsc_output
try {
logger.debug(`Running: ${tsc_command}`)
tsc_output = cp.execSync(tsc_command, { cwd: working_dir })
logger.debug(`Running: ${tscCommand}`)
tsc_output = cp.execSync(tscCommand, { cwd: working_dir })
} catch (err) {
// error while compiling ts files
logger.debug(err.message);
@@ -44,6 +151,21 @@ exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_module
logger.debug(`Saved compiled js output at: ${complied_js_dir}`);
logger.debug(`Finding compiled cypress config file in: ${complied_js_dir}`);

// Clean up the temporary tsconfig file
if (fs.existsSync(tempTsConfigPath)) {
fs.unlinkSync(tempTsConfigPath);
logger.debug(`Temporary tsconfig file removed: ${tempTsConfigPath}`);
}

if (tsc_output) {
logger.debug(tsc_output.toString());
}

if (!tsc_output) {
logger.error('No TypeScript compilation output available');
return null;
}

const lines = tsc_output.toString().split('\n');
let foundLine = null;
for (let i = 0; i < lines.length; i++) {
@@ -53,7 +175,7 @@ exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_module
}
}
if (foundLine === null) {
logger.error(`No compiled cypress config found. There might some error running ${tsc_command} command`)
logger.error(`No compiled cypress config found. There might some error running ${tscCommand} command`)
return null
} else {
const compiled_cypress_config_filepath = foundLine.split('TSFILE: ').pop()
Loading