├── packages ├── cyphfell │ ├── .npmrc │ ├── test │ │ ├── helper.js │ │ ├── test.sh │ │ ├── unit │ │ │ ├── util │ │ │ │ ├── ArrayUtilTests.js │ │ │ │ ├── InitializeBrowserTests.js │ │ │ │ ├── PluginsUtilTests.js │ │ │ │ ├── FileParserTests.js │ │ │ │ ├── FunctionBodyTransformerTests.js │ │ │ │ ├── RegexUtilTests.js │ │ │ │ ├── DirectoryUtilsTests.js │ │ │ │ └── FilePathUtilTests.js │ │ │ ├── regex │ │ │ │ ├── TemporaryRegexTests.js │ │ │ │ └── CommandsToWrapTests.js │ │ │ ├── plugins │ │ │ │ ├── ElementIdTests.js │ │ │ │ ├── LambdaExpressionsTests.js │ │ │ │ ├── LoopWarningTests.js │ │ │ │ ├── LongWaitWarningTests.js │ │ │ │ ├── ArgumentSeparationTests.js │ │ │ │ ├── WrapCommandsTests.js │ │ │ │ └── ElementsIteratorsTests.js │ │ │ └── converters │ │ │ │ └── ActiveConverterTests.js │ │ └── e2e │ │ │ ├── noImports │ │ │ ├── SelectByConversionTests.js │ │ │ └── FixErrorTests.js │ │ │ └── IndexTests.js │ ├── src │ │ ├── constants │ │ │ ├── WarningConstants.js │ │ │ ├── FrameworkConstants.js │ │ │ ├── EslintConstants.js │ │ │ ├── OptionalCommandsToWrap.js │ │ │ └── CommandsToWrap.js │ │ ├── handlers │ │ │ ├── FileHanderList.js │ │ │ ├── JSONFileHandler.js │ │ │ └── AbstractFileHandler.js │ │ ├── regex │ │ │ ├── ReverseReturnRegex.js │ │ │ ├── ReplaceTemporaryRegex.js │ │ │ └── PostParseRegex.js │ │ ├── converters │ │ │ ├── ActiveConverter.js │ │ │ ├── nightwatch │ │ │ │ └── NightwatchStrategy.js │ │ │ ├── BaseStrategy.js │ │ │ └── wdio │ │ │ │ ├── TemporaryRegexReplacements.js │ │ │ │ └── WDIOStrategy.js │ │ ├── plugins │ │ │ ├── LambdaExpressionsPlugin.js │ │ │ ├── ElementIdPlugin.js │ │ │ ├── LoopWarningsPlugin.js │ │ │ ├── LongWaitWarningPlugin.js │ │ │ ├── ElementsIteratorsPlugin.js │ │ │ ├── ShouldAssertionsPlugin.js │ │ │ ├── TernaryOperatorPlugin.js │ │ │ ├── ArgumentSeparationPlugin.js │ │ │ ├── WrapReturnsPlugin.js │ │ │ └── WrapElementActionsPlugin.js │ │ ├── BaseWarningPlugin.js │ │ ├── util │ │ │ ├── PluginsUtil.js │ │ │ ├── FileParser.js │ │ │ ├── FilePathUtil.js │ │ │ ├── FunctionBodyTransformer.js │ │ │ ├── RegexUtil.js │ │ │ └── DirectoryUtils.js │ │ ├── CypressArrayUtil.js │ │ ├── BasePlugin.js │ │ └── reports │ │ │ └── ReportGenerator.js │ ├── cli.js │ ├── test-automation │ │ ├── initSupport.js │ │ ├── cypress │ │ │ ├── integration │ │ │ │ ├── DataCaptureHookTests.spec.js │ │ │ │ ├── ReturnWrapperTests.spec.js │ │ │ │ ├── BrowserLogTests.spec.js │ │ │ │ ├── TestsWrapSimplification.spec.js │ │ │ │ ├── BrowserWaitTests.spec.js │ │ │ │ ├── BrowserActionTests.spec.js │ │ │ │ ├── BrowserUtilTests.spec.js │ │ │ │ ├── ArrayIteratorTests.spec.js │ │ │ │ ├── BrowserEventTests.spec.js │ │ │ │ └── BrowserIdAttributeTests.spec.js │ │ │ ├── plugins │ │ │ │ └── index.js │ │ │ └── support │ │ │ │ ├── commands.js │ │ │ │ └── index.js │ │ ├── run.sh │ │ └── wdioConfigInitData.js │ ├── defaultFiles │ │ ├── defaultConfigFile.json │ │ ├── defaultPluginFile.js │ │ ├── defaultSupportFile.js │ │ └── defaultCommandsFile.js │ ├── cypress.json │ ├── cliOptions.js │ ├── package.json │ └── index.js ├── cyphfell-test-app │ ├── .npmrc │ ├── README.md │ ├── src │ │ ├── index.css │ │ ├── index.js │ │ ├── App.test.js │ │ ├── App.css │ │ ├── logo.svg │ │ ├── App.js │ │ └── registerServiceWorker.js │ ├── .gitignore │ ├── public │ │ ├── manifest.json │ │ └── index.html │ └── package.json └── cyphfell-debug-instrumentation │ ├── .npmrc │ ├── index.js │ ├── src │ ├── Instrumentation.js │ └── FunctionCallStack.js │ └── package.json ├── docs ├── Cyphfell Architecture.png └── Cyphfell Architecture.vsdx ├── lerna.json ├── .travis.yml ├── Cyphfell.iml ├── package.json ├── readme.md └── os-project-logo.svg /packages/cyphfell/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.yarnpkg.com -------------------------------------------------------------------------------- /packages/cyphfell-test-app/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /packages/cyphfell-debug-instrumentation/.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /packages/cyphfell-test-app/README.md: -------------------------------------------------------------------------------- 1 | Test application for Cyphfell automation tests -------------------------------------------------------------------------------- /packages/cyphfell/test/helper.js: -------------------------------------------------------------------------------- 1 | const chai = require("chai"); 2 | global.expect = chai.expect; 3 | -------------------------------------------------------------------------------- /packages/cyphfell/src/constants/WarningConstants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | MAX_WAIT_MILLISECONDS: 500 3 | }; -------------------------------------------------------------------------------- /docs/Cyphfell Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/cyphfell/HEAD/docs/Cyphfell Architecture.png -------------------------------------------------------------------------------- /docs/Cyphfell Architecture.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intuit/cyphfell/HEAD/docs/Cyphfell Architecture.vsdx -------------------------------------------------------------------------------- /packages/cyphfell-test-app/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.9.0", 3 | "packages": [ 4 | "packages/*" 5 | ], 6 | "version": "independent" 7 | } 8 | -------------------------------------------------------------------------------- /packages/cyphfell/src/constants/FrameworkConstants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | WebdriverIO: "wdio", 3 | NightwatchJS: "njs" 4 | }; -------------------------------------------------------------------------------- /packages/cyphfell/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const options = require("./cliOptions"), 3 | conversion = require("./index"); 4 | conversion(options, []); 5 | -------------------------------------------------------------------------------- /packages/cyphfell/test/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export TESTING_LOCALLY=true 4 | export CYPHFELL_TEST_FRAMEWORK=wdio 5 | ./node_modules/.bin/mocha "./test/**/*.js" -------------------------------------------------------------------------------- /packages/cyphfell-debug-instrumentation/index.js: -------------------------------------------------------------------------------- 1 | const FunctionCallStack = require("./src/FunctionCallStack"); 2 | 3 | module.exports = () => { 4 | global.cyphfellCallStack = FunctionCallStack; 5 | }; -------------------------------------------------------------------------------- /packages/cyphfell-debug-instrumentation/src/Instrumentation.js: -------------------------------------------------------------------------------- 1 | export default function({types: t}) { 2 | return { 3 | visitor: { 4 | FunctionDeclaration(path) { 5 | console.error(path); 6 | } 7 | } 8 | }; 9 | } -------------------------------------------------------------------------------- /packages/cyphfell/src/constants/EslintConstants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | DISABLED: 0, // don't use eslint autofix 3 | LOCAL: 1, // use locally installed eslint package 4 | GLOBAL: 2 // use globally installed eslint 5 | }; -------------------------------------------------------------------------------- /packages/cyphfell/src/handlers/FileHanderList.js: -------------------------------------------------------------------------------- 1 | const JavaScriptFileHandler = require("./JavaScriptFileHandler"); 2 | const JSONFileHandler = require("./JSONFileHandler"); 3 | 4 | module.exports = [ 5 | new JavaScriptFileHandler(), 6 | new JSONFileHandler() 7 | ]; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8.11.2" 4 | before_script: 5 | - cd packages/cyphfell 6 | - yarn install 7 | - cd ../cyphfell-test-app 8 | - yarn install 9 | - cd ../cyphfell 10 | script: 11 | - sh ./test/test.sh 12 | - sh ./test-automation/run.sh -------------------------------------------------------------------------------- /packages/cyphfell/src/regex/ReverseReturnRegex.js: -------------------------------------------------------------------------------- 1 | const arrays = require("../CypressArrayUtil"); 2 | 3 | module.exports = [ 4 | ".its(", 5 | ".title()", 6 | ".url()", 7 | ".invoke(", 8 | "cy.getAll", 9 | ".findAll(" 10 | ].concat(arrays.modifiableIterators.map((it) => `${it}(`)); -------------------------------------------------------------------------------- /packages/cyphfell-test-app/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import registerServiceWorker from './registerServiceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | registerServiceWorker(); 9 | -------------------------------------------------------------------------------- /packages/cyphfell-test-app/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /Cyphfell.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /packages/cyphfell-debug-instrumentation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cyphfell-debug-instrumentation", 3 | "version": "1.0.1", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "Apache License 2.0", 12 | "private": true 13 | } 14 | -------------------------------------------------------------------------------- /packages/cyphfell-test-app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /packages/cyphfell/src/regex/ReplaceTemporaryRegex.js: -------------------------------------------------------------------------------- 1 | const replacements = require("../converters/wdio/TemporaryRegexReplacements"); 2 | 3 | module.exports = (str) => { 4 | Object.keys(replacements).forEach((replacement) => { 5 | replacement = replacements[replacement]; 6 | const regex = new RegExp(replacement.temporary, "g"); 7 | str = str.replace(regex, replacement.real); 8 | }); 9 | return str; 10 | }; -------------------------------------------------------------------------------- /packages/cyphfell-test-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/cyphfell/test-automation/initSupport.js: -------------------------------------------------------------------------------- 1 | const converter = require("../src/converters/ActiveConverter"); 2 | const fs = require("fs"); 3 | converter.init(process.argv[2]); 4 | 5 | const contents = fs.readFileSync(process.argv[3], "utf8"); 6 | fs.writeFileSync(process.argv[3], `${contents} \n ${converter.getStrategy().getSupportAppendText()}`); 7 | fs.writeFileSync(process.argv[4], converter.getStrategy().getCommandsFileContents()); -------------------------------------------------------------------------------- /packages/cyphfell/defaultFiles/defaultConfigFile.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "4b7341", 3 | "chromeWebSecurity": false, 4 | "fixturesFolder": "test/cypress/fixtures", 5 | "integrationFolder": "test/cypress/integration", 6 | "pluginsFile": "test/cypress/plugins/index.js", 7 | "screenshotsFolder": "test/cypress/screenshots", 8 | "supportFile": "test/cypress/support/index.js", 9 | "videosFolder": "test/cypress/videos", 10 | "defaultCommandTimeout": 15000, 11 | "pageLoadTimeout": 60000 12 | } -------------------------------------------------------------------------------- /packages/cyphfell/test-automation/cypress/integration/DataCaptureHookTests.spec.js: -------------------------------------------------------------------------------- 1 | describe("Tests the data capture hooks defined in a config", function() { 2 | 3 | beforeEach(() => { 4 | cy.visit("http://localhost:3000/"); 5 | }); 6 | 7 | it("Random test 1", () => { 8 | 9 | }); 10 | 11 | it("Random test 2", () => { 12 | 13 | }); 14 | 15 | }); 16 | 17 | describe("Tests another test suite", function() { 18 | 19 | it("XYZ", () => { 20 | 21 | }); 22 | 23 | }); -------------------------------------------------------------------------------- /packages/cyphfell/src/handlers/JSONFileHandler.js: -------------------------------------------------------------------------------- 1 | const AbstractFileHandler = require("./AbstractFileHandler"); 2 | 3 | class JSONFileHandler extends AbstractFileHandler { 4 | 5 | /** 6 | * Gets whether this handler can convert the specified file 7 | * @param {String} dir - the path to the file 8 | * @return {boolean} 9 | */ 10 | canHandle(dir) { 11 | return dir.endsWith(".json"); 12 | } 13 | 14 | parseImpl(plugins) { 15 | return this.lines; 16 | } 17 | } 18 | 19 | module.exports = JSONFileHandler; -------------------------------------------------------------------------------- /packages/cyphfell-test-app/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | -------------------------------------------------------------------------------- /packages/cyphfell/test/unit/util/ArrayUtilTests.js: -------------------------------------------------------------------------------- 1 | const arrays = require("../../../src/CypressArrayUtil"); 2 | 3 | describe("Tests Cypress array util functions", function () { 4 | 5 | it("Tests getCypressFunction", () => { 6 | expect(arrays.getCypressFunction("NOT_AN_ARRAY_FUNCTION")).to.be.null; 7 | expect(arrays.getCypressFunction("find")).to.be.equal("findCypress"); 8 | expect(arrays.getCypressFunction("map")).to.be.equal("mapCypress"); 9 | expect(arrays.getCypressFunction("")).to.be.null; 10 | }); 11 | }); -------------------------------------------------------------------------------- /packages/cyphfell/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "4b7341", 3 | "chromeWebSecurity": false, 4 | "fixturesFolder": "test-automation/cypress/fixtures", 5 | "integrationFolder": "test-automation/cypress/integration", 6 | "pluginsFile": "test-automation/cypress/plugins/index.js", 7 | "screenshotsFolder": "test-automation/cypress/screenshots", 8 | "supportFile": "test-automation/cypress/support/index.js", 9 | "videosFolder": "test-automation/cypress/videos", 10 | "defaultCommandTimeout": 15000, 11 | "pageLoadTimeout": 60000, 12 | "baseUrl": "http://localhost:3000/" 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cyphfell-core", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git@github.com:intuit/cyphfell.git" 12 | }, 13 | "publishConfig": { 14 | "registry": "https://registry.npmjs.org/" 15 | }, 16 | "keywords": [], 17 | "author": "", 18 | "license": "Apache License 2.0", 19 | "dependencies": { 20 | "eslint": "^5.6.1", 21 | "eslint-plugin-react": "^7.11.1", 22 | "lerna": "^2.9.0" 23 | }, 24 | "private": true 25 | } 26 | -------------------------------------------------------------------------------- /packages/cyphfell-test-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cyphfell-test-app", 3 | "version": "0.1.1", 4 | "dependencies": { 5 | "react": "^16.5.0", 6 | "react-dom": "^16.5.0", 7 | "react-scripts": "1.1.5" 8 | }, 9 | "scripts": { 10 | "start": "react-scripts start", 11 | "build": "react-scripts build", 12 | "test": "react-scripts test --env=jsdom", 13 | "eject": "react-scripts eject" 14 | }, 15 | "license": "Apache License 2.0", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/intuit/cyphfell.git#master" 19 | }, 20 | "publishConfig": { 21 | "registry": "https://registry.npmjs.org/" 22 | }, 23 | "private": true 24 | } 25 | -------------------------------------------------------------------------------- /packages/cyphfell/test-automation/cypress/integration/ReturnWrapperTests.spec.js: -------------------------------------------------------------------------------- 1 | describe("Tests the Cyphfell WrapReturns plugin", function() { 2 | 3 | beforeEach(() => { 4 | cy.visit("http://localhost:3000/"); 5 | }); 6 | 7 | it("Tests the WrapReturns plugin", () => { 8 | const promise = () => { 9 | return new Promise((resolve) => { 10 | setTimeout(() => { 11 | resolve("success"); 12 | }, 99); 13 | }); 14 | }; 15 | browser.call(promise).then((val) => { 16 | return cy.wrap(val); 17 | }).should((res) => { 18 | expect(res).to.be.equal("success"); 19 | }); 20 | }); 21 | }); -------------------------------------------------------------------------------- /packages/cyphfell/defaultFiles/defaultPluginFile.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /packages/cyphfell/test-automation/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example plugins/index.js can be used to load plugins 3 | // 4 | // You can change the location of this file or turn off loading 5 | // the plugins file with the 'pluginsFile' configuration option. 6 | // 7 | // You can read more here: 8 | // https://on.cypress.io/plugins-guide 9 | // *********************************************************** 10 | 11 | // This function is called when a project is opened or re-opened (e.g. due to 12 | // the project's config changing) 13 | 14 | module.exports = (on, config) => { 15 | // `on` is used to hook into various events Cypress emits 16 | // `config` is the resolved Cypress config 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /packages/cyphfell-debug-instrumentation/src/FunctionCallStack.js: -------------------------------------------------------------------------------- 1 | const stack = []; 2 | 3 | module.exports = { 4 | getStack: () => stack, 5 | emptyStack: () => { 6 | stack.length = 0; 7 | }, 8 | push: (filePath, functionName = "&anonymous") => { 9 | stack.push({ 10 | filePath: filePath, 11 | functionName: functionName, 12 | calledLines: [] 13 | }); 14 | }, 15 | callLine: (lineNumber) => { 16 | stack[stack.length - 1].calledLines.push(lineNumber); 17 | }, 18 | pop: () => { 19 | stack.pop(); 20 | }, 21 | printDebugMessage: () => { 22 | stack.forEach((call) => { 23 | console.error(`In ${call.filePath}: function: ${call.functionName} was called`); 24 | console.error(`${call.calledLines[call.calledLines.length - 1]} was the last successfully executed line.`); 25 | }); 26 | } 27 | }; -------------------------------------------------------------------------------- /packages/cyphfell/src/converters/ActiveConverter.js: -------------------------------------------------------------------------------- 1 | const wdio = require("./wdio/WDIOStrategy"); 2 | const nightwatch = require("./nightwatch/NightwatchStrategy"); 3 | const frameworks = require("../constants/FrameworkConstants"); 4 | 5 | let strategy; 6 | class ActiveConverter { 7 | 8 | static init(framework) { 9 | // reset for testing purposes 10 | strategy = null; 11 | if (framework === frameworks.NightwatchJS) { 12 | strategy = new nightwatch(); 13 | } else if (framework === frameworks.WebdriverIO) { 14 | strategy = new wdio(); 15 | } 16 | 17 | strategy.init(); 18 | } 19 | 20 | static getStrategy() { 21 | if (!strategy && process.env.CYPHFELL_TEST_FRAMEWORK) { 22 | this.init(process.env.CYPHFELL_TEST_FRAMEWORK); 23 | } 24 | return strategy; 25 | } 26 | } 27 | 28 | module.exports = ActiveConverter; -------------------------------------------------------------------------------- /packages/cyphfell/src/plugins/LambdaExpressionsPlugin.js: -------------------------------------------------------------------------------- 1 | const BasePlugin = require("../BasePlugin"); 2 | const estraverse = require("estraverse"); 3 | 4 | /** 5 | * This plugin transforms lambda arrow function expressions into a normal arrow function call 6 | * ex: 7 | * 8 | * abc.xyz(() => true) turns into 9 | * abc.xyz(() => { 10 | * return true; 11 | * }); 12 | */ 13 | class LambdaExpressionsPlugin extends BasePlugin { 14 | 15 | beforeParseLines(ast) { 16 | estraverse.traverse(ast, { 17 | enter: (node) => { 18 | if (node.type === "ArrowFunctionExpression" && !node.body.body) { 19 | node.body = { 20 | type: "BlockStatement", 21 | body: [ 22 | { 23 | type: "ReturnStatement", 24 | argument: node.body 25 | } 26 | ] 27 | }; 28 | node.expression = false; 29 | } 30 | } 31 | }); 32 | } 33 | 34 | getName() { 35 | return "LambdaExpressions"; 36 | } 37 | } 38 | 39 | module.exports = LambdaExpressionsPlugin; -------------------------------------------------------------------------------- /packages/cyphfell/test-automation/cypress/integration/BrowserLogTests.spec.js: -------------------------------------------------------------------------------- 1 | describe("Tests the browser log function", function() { 2 | 3 | const test = () => { 4 | browser.log().then((logs) => { 5 | const testInclusion = (level, message) => { 6 | expect(logs.value).to.deep.include({ 7 | level: level, 8 | message: message 9 | }); 10 | }; 11 | 12 | testInclusion("INFO", "%cDownload the React DevTools for a better development experience: https://fb.me/react-devtools"); 13 | testInclusion("ERROR", "constructor error"); 14 | expect(logs.value.length).to.be.equal(2); 15 | }); 16 | }; 17 | 18 | beforeEach(() => { 19 | cy.visit("http://localhost:3000/"); 20 | }); 21 | 22 | it("Tests browser.log() without reloading the page", () => { 23 | console.error("random log to make sure this console does not get spied on"); 24 | test(); 25 | }); 26 | 27 | it("Tests browser.log() with a page reload", () => { 28 | test(); 29 | cy.reload(); 30 | test(); 31 | }); 32 | }); -------------------------------------------------------------------------------- /packages/cyphfell/cliOptions.js: -------------------------------------------------------------------------------- 1 | const commandLineArgs = require("command-line-args"), 2 | frameworks = require("./src/constants/FrameworkConstants"), 3 | eslint = require("./src/constants/EslintConstants"), 4 | optionDefinitions = [ 5 | {name: "cypressFolder", alias: "c", type: String, defaultValue: "test/cypress/"}, 6 | {name: "baseNormalFolder", alias: "b", type: String, defaultValue: "test/"}, 7 | {name: "enableAssertions", alias: "a", type: Boolean, defaultValue: false}, 8 | {name: "glob", alias: "g", type: String, defaultValue: "${CWD}/test/!(unit|ui-perf|cypress)/**/*.+(js|json)"}, 9 | {name: "transpile", alias: "t", type: Boolean, defaultValue: false}, 10 | {name: "validateCypressDir", alias: "v", type: Boolean, defaultValue: false}, 11 | {name: "framework", alias: "f", type: String, defaultValue: frameworks.WebdriverIO}, 12 | {name: "eslint", alias: "e", type: Number, defaultValue: eslint.DISABLED} 13 | 14 | ], 15 | options = commandLineArgs(optionDefinitions); 16 | 17 | module.exports = options; -------------------------------------------------------------------------------- /packages/cyphfell/src/converters/nightwatch/NightwatchStrategy.js: -------------------------------------------------------------------------------- 1 | const BaseStrategy = require("../BaseStrategy"); 2 | const name = require("../../constants/FrameworkConstants").NightwatchJS; 3 | 4 | class NightwatchStrategy extends BaseStrategy { 5 | 6 | /** 7 | * Gets a list of all regular expressions to replace, and what to replace them with 8 | * @return {Array} - list of all regular expressions to replace, and what to replace them with 9 | */ 10 | getReplacementRegex() { 11 | return []; 12 | } 13 | 14 | /** 15 | * Gets a list of all regular expressions that have been transformed and need a .then() chained on to them 16 | * @return {Array} - list of regex representing transformed code 17 | */ 18 | getReplacedReturnRegex() { 19 | // TODO: do this 20 | return []; 21 | } 22 | 23 | /** 24 | * Gets the name of the framework being used 25 | * @return {String} - the framework name 26 | */ 27 | getName() { 28 | return name; 29 | } 30 | } 31 | 32 | module.exports = NightwatchStrategy; -------------------------------------------------------------------------------- /packages/cyphfell/test/unit/regex/TemporaryRegexTests.js: -------------------------------------------------------------------------------- 1 | const util = require("../../../src/regex/ReplaceTemporaryRegex.js"); 2 | const replacements = require("../../../src/converters/wdio/TemporaryRegexReplacements"); 3 | 4 | describe("Tests ReplaceTemporaryRegex utility", function() { 5 | 6 | it("Tests to make sure all temporary replacements are made", () => { 7 | Object.keys(replacements).forEach((replacement) => { 8 | replacement = replacements[replacement]; 9 | expect(util(replacement.temporary)).to.be.equal(replacement.real); 10 | }); 11 | 12 | expect(util(`${replacements.IS_SELECTED.temporary}.${replacements.IS_SELECTED.temporary}`)).to.be. 13 | equal(`${replacements.IS_SELECTED.real}.${replacements.IS_SELECTED.real}`, "Failed to replace multiple instances of same temporary string"); 14 | }); 15 | 16 | it("Tests to make sure no replacement is made in some cases", () => { 17 | expect(util("ABCDEFGHIJKLMNOPQRSTUV AIA A A A A A A")).to.be.equal("ABCDEFGHIJKLMNOPQRSTUV AIA A A A A A A", "Failed when no string should be replaced"); 18 | expect(util("")).to.be.equal(""); 19 | }); 20 | }); -------------------------------------------------------------------------------- /packages/cyphfell/test-automation/cypress/integration/TestsWrapSimplification.spec.js: -------------------------------------------------------------------------------- 1 | const direct = () => { 2 | return cy.get("#textInput"); 3 | }; 4 | 5 | const indirect = () => { 6 | return cy.get("#textInput").then((res) => { 7 | return res; 8 | }); 9 | }; 10 | 11 | describe("Tests the Cyphfell wrap simplifcation concepts used", function() { 12 | 13 | beforeEach(() => { 14 | cy.visit("http://localhost:3000/"); 15 | cy.get("#textInput").type("abc"); 16 | }); 17 | 18 | it("Tests wrap simplification concept with getText() with a direct element return", () => { 19 | direct().getText().then((value) => { 20 | expect(value).to.be.equal("abc"); 21 | }); 22 | }); 23 | 24 | it("Tests wrap simplification concept with getText() with an indirect element return", () => { 25 | indirect().getText().then((value) => { 26 | expect(value).to.be.equal("abc"); 27 | }); 28 | }); 29 | 30 | it("Tests non-wrap simplification result", () => { 31 | cy.get("#textInput").then((res) => { 32 | return cy.wrap(res).getText(); 33 | }).then((res) => { 34 | expect(res).to.be.equal("abc"); 35 | }); 36 | }); 37 | }); -------------------------------------------------------------------------------- /packages/cyphfell/test/unit/plugins/ElementIdTests.js: -------------------------------------------------------------------------------- 1 | const esprima = require("../../../src/util/EsprimaUtils"); 2 | const _ = require("lodash"); 3 | let plugin = require("../../../src/plugins/ElementIdPlugin"); 4 | plugin = new plugin(); 5 | 6 | describe("Tests ElementId plugin", function() { 7 | before(() => { 8 | global.options = { 9 | transpile: false 10 | }; 11 | }); 12 | 13 | it(`Tests to make sure .ELEMENT attributes are changed`, () => { 14 | const ast = esprima.generateAST(`const x = () => { let x = xyz.ELEMENT; cy.doSomething(xyz2.something.else.ELEMENT); return test.something.ELEMENT.something.ELEMENT; };`); 15 | plugin.afterTransformAfterParsing(ast); 16 | expect(ast).to.deep.equal(esprima.generateAST(` 17 | const x = () => { 18 | let x = xyz; 19 | cy.doSomething(xyz2.something.else); 20 | return test.something.something; 21 | }; 22 | `)); 23 | }); 24 | 25 | it("Tests plugin name", () => { 26 | expect(plugin.getName()).to.be.equal("ElementId"); 27 | }); 28 | 29 | }); -------------------------------------------------------------------------------- /packages/cyphfell/test/unit/converters/ActiveConverterTests.js: -------------------------------------------------------------------------------- 1 | const activeConverter = require("../../../src/converters/ActiveConverter"); 2 | const frameworks = require("../../../src/constants/FrameworkConstants"); 3 | const wdio = require("../../../src/converters/wdio/WDIOStrategy"); 4 | const njs = require("../../../src/converters/nightwatch/NightwatchStrategy"); 5 | 6 | describe("Tests ActiveConverter", function() { 7 | 8 | it("Tests init() with WDIO", () => { 9 | activeConverter.init(frameworks.WebdriverIO); 10 | expect(activeConverter.getStrategy().getName()).to.be.equal(new wdio().getName()); 11 | }); 12 | 13 | it("Tests init() with Nightwatch", () => { 14 | activeConverter.init(frameworks.NightwatchJS); 15 | expect(activeConverter.getStrategy().getName()).to.be.equal(new njs().getName()); 16 | }); 17 | 18 | it("Tests init() with invalid selection", () => { 19 | let errThrown = false; 20 | try { 21 | activeConverter.init("some invalid selection"); 22 | } catch (ex) { 23 | errThrown = true; 24 | } 25 | expect(errThrown).to.be.true; 26 | }); 27 | }); -------------------------------------------------------------------------------- /packages/cyphfell/src/handlers/AbstractFileHandler.js: -------------------------------------------------------------------------------- 1 | const dirUtil = require("../util/DirectoryUtils"); 2 | 3 | class AbstractFileHandler { 4 | 5 | /** 6 | * Gets whether this handler can convert the specified file 7 | * @param {String} dir - the path to the file 8 | * @return {boolean} 9 | */ 10 | canHandle(dir) { 11 | return false; 12 | } 13 | 14 | parseImpl(plugins) { 15 | return this.lines; 16 | } 17 | 18 | /** 19 | * Parses a test file and converts it into Cypress format if necessary 20 | * @param {String} lines - all of the lines in the file 21 | * @param {String} dir - the path to the file 22 | * @param {Array} plugins - all plugins that will modify the AST 23 | * @return {String?} - the lines in the file after modifications are made, or null if a modification could not be made 24 | */ 25 | handleParseAttempt(lines, dir, plugins) { 26 | if (!this.canHandle(dir)) { 27 | return null; 28 | } 29 | 30 | this.lines = lines; 31 | this.fileDir = dir; 32 | this.newFileDir = dirUtil.getNewFilePath(dir, lines, options); 33 | return this.parseImpl(plugins); 34 | } 35 | 36 | } 37 | 38 | module.exports = AbstractFileHandler; -------------------------------------------------------------------------------- /packages/cyphfell/test-automation/cypress/integration/BrowserWaitTests.spec.js: -------------------------------------------------------------------------------- 1 | describe("Tests browser wait functions", function() { 2 | 3 | beforeEach(() => { 4 | cy.visit("http://localhost:3000/"); 5 | }); 6 | 7 | it("Tests browser.waitUntil() with a promise", () => { 8 | let finished = false; 9 | const promise = new Promise((resolve) => { 10 | setTimeout(() => { 11 | finished = true; 12 | resolve(true); 13 | }, 3000); 14 | }); 15 | browser.waitUntil(promise).then(() => { 16 | expect(finished).to.be.true; 17 | }); 18 | }); 19 | 20 | it("Tests browser.waitUntil() with a function", () => { 21 | let finished = false; 22 | let timesCalled = 0; 23 | let fn = () => { 24 | if (++timesCalled === 10) { 25 | finished = true; 26 | // test truthy value 27 | return 25; 28 | } 29 | return false; 30 | }; 31 | browser.waitUntil(fn).then(() => { 32 | expect(finished).to.be.true; 33 | expect(timesCalled).to.be.equal(10); 34 | }); 35 | }); 36 | }); -------------------------------------------------------------------------------- /packages/cyphfell/src/constants/OptionalCommandsToWrap.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const esprima = require("../util/EsprimaUtils"); 3 | const estraverse = require("estraverse"); 4 | const fp = require("../util/FilePathUtil"); 5 | const converter = require("../converters/ActiveConverter"); 6 | 7 | const customCommands = () => { 8 | const fileContents = [ 9 | fs.readFileSync(fp.findLocalFilePath("defaultFiles/defaultCommandsFile.js"), "utf8"), 10 | converter.getStrategy().getCommandsFileContents() 11 | ]; 12 | const commands = []; 13 | 14 | fileContents.forEach((lines) => { 15 | const ast = esprima.generateAST(lines); 16 | estraverse.traverse(ast, { 17 | enter: (node) => { 18 | if (node.type === "CallExpression" && node.arguments[1] && node.arguments[1].type === "ObjectExpression" && 19 | !(node.arguments[1].properties[0] && node.arguments[1].properties[0].kind === "init" && node.arguments[1].properties[0].key.name === "prevSubject" && 20 | node.arguments[1].properties[0].value.value === "element") && node.arguments[0].value) { 21 | commands.push(node.arguments[0].value); 22 | } 23 | } 24 | }); 25 | }); 26 | 27 | return commands; 28 | }; 29 | 30 | module.exports = customCommands(); -------------------------------------------------------------------------------- /packages/cyphfell/test/unit/util/InitializeBrowserTests.js: -------------------------------------------------------------------------------- 1 | const util = require("../../../src/converters/wdio/InitializeBrowserFunctions.js"); 2 | 3 | describe("Tests InitializeBrowserTests functions", function () { 4 | 5 | before(() => { 6 | global.Cypress = { on: () => {}}; 7 | }); 8 | 9 | it("Tests initializing global browser object", () => { 10 | util.init("browser"); 11 | expect(global.browser).to.not.be.undefined; 12 | expect(browser.desiredCapabilities.browserName).to.be.equal("chrome"); 13 | expect(typeof browser.options).to.be.equal("object"); 14 | 15 | util.init("clientNameTest"); 16 | expect(global.clientNameTest).to.not.be.undefined; 17 | }); 18 | 19 | it("Tests initializing config object", () => { 20 | util.init("browser"); 21 | const config = { 22 | testKey1: "a", 23 | testKey2: () => {}, 24 | testKey3: [25, 3] 25 | }; 26 | util.initConfig(browser, config); 27 | expect(browser.options.testKey1).to.be.equal(config.testKey1); 28 | expect(browser.options.testKey2).to.deep.equal([config.testKey2]); 29 | expect(browser.options.testKey3).to.be.equal(config.testKey3); 30 | }); 31 | }); -------------------------------------------------------------------------------- /packages/cyphfell/src/BaseWarningPlugin.js: -------------------------------------------------------------------------------- 1 | const BasePlugin = require("./BasePlugin"); 2 | const report = require("./reports/ReportGenerator"); 3 | const esprima = require("./util/EsprimaUtils"); 4 | 5 | /** 6 | * Represents a base class that any warning implementation must override. Warnings scan the converted Cypress code 7 | * and highlight lines where the user may have to take certain actions 8 | */ 9 | class BaseWarningPlugin extends BasePlugin { 10 | 11 | /** 12 | * Walks the given AST and adds any warnings to the output report 13 | * @param {Object} ast - the AST to walk 14 | * @param {String} newFileDir - the new directory of the file 15 | */ 16 | run(ast, newFileDir) { 17 | } 18 | 19 | /** 20 | * Gets the warning message to write to the report 21 | * @return {String} 22 | */ 23 | getMessage() { 24 | return ""; 25 | } 26 | 27 | afterComplete(ast, newFileDir) { 28 | this.run(ast, newFileDir); 29 | } 30 | 31 | /** 32 | * Adds a warning to the generated report for a node 33 | * @param {Object} node - the node that triggered the warning 34 | */ 35 | reportWarning(node) { 36 | report.onWarning(this.getMessage(), esprima.generateCodeFromAST(node), node.loc.start.line); 37 | } 38 | } 39 | 40 | module.exports = BaseWarningPlugin; -------------------------------------------------------------------------------- /packages/cyphfell/src/plugins/ElementIdPlugin.js: -------------------------------------------------------------------------------- 1 | const BasePlugin = require("../BasePlugin"); 2 | const esprima = require("../util/EsprimaUtils"); 3 | 4 | /** 5 | * This plugin removes all occurrences of the .ELEMENT property from an AST. 6 | * Used for elementId* functions, because they expect to have the value of the .ELEMENT property passed into them, but in Cypress 7 | * the elements can be passed in directly instead 8 | */ 9 | class ElementIdPlugin extends BasePlugin { 10 | 11 | /** 12 | * Makes modifications to the AST after transforming the code after the initial regular expression conversion is finished 13 | * @param {Object} ast - the AST representing the code in a javascript file 14 | */ 15 | afterTransformAfterParsing(ast) { 16 | // TODO: prefer to delete this plugin and replace it with RegEx in RegexReplacements.js 17 | const newAST = esprima.generateAST(esprima.generateCodeFromAST(ast).replace(/\.ELEMENT/g, "")); 18 | Object.keys(ast).forEach((key) => { 19 | delete ast[key]; 20 | }); 21 | Object.assign(ast, newAST); 22 | } 23 | 24 | /** 25 | * Gets the unique name of this plugin 26 | * @return {String} - the unique name of this plugin 27 | */ 28 | getName() { 29 | return "ElementId"; 30 | } 31 | 32 | } 33 | 34 | module.exports = ElementIdPlugin; -------------------------------------------------------------------------------- /packages/cyphfell/test-automation/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | copy_support_file() { 4 | cp ./defaultFiles/defaultSupportFile.js ./test-automation/cypress/support/index.js 5 | # replace the package name because that should only be used for modules that are dependent upon this one 6 | export TESTING_LOCALLY=true 7 | node ./test-automation/initSupport.js wdio test-automation/cypress/support/index.js test-automation/cypress/support/frameworkCommands.js 8 | sed -i '' -e 's/cyphfell/\.\.\/\.\.\/\.\./g' ./test-automation/cypress/support/index.js 9 | } 10 | 11 | set -e 12 | 13 | # copy the necessary files for running automation 14 | cp ./defaultFiles/defaultPluginFile.js ./test-automation/cypress/plugins/index.js 15 | cp ./defaultFiles/defaultCommandsFile.js ./test-automation/cypress/support/commands.js 16 | sed -i '' -e 's/cyphfell/\.\.\/\.\.\/\.\./g' ./test-automation/cypress/support/commands.js 17 | copy_support_file 18 | 19 | # run automation 20 | # TODO: potentially use github static page instead of starting a local server 21 | yarn start & 22 | sleep 7 23 | 24 | cypress run -b chrome --spec "**/*/!(DataCaptureHookTests).spec.js" 25 | 26 | cat test-automation/wdioConfigInitData.js >> ./test-automation/cypress/support/index.js 27 | cypress run -b chrome --spec **/*/DataCaptureHookTests.spec.js -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | # Cyphfell 3 | 4 |

5 | drawing 6 |

7 | 8 | ### Converts WDIO automation tests to Cypress 9 | ##### Usage instructions can be found here in the ["Cyphfell" package readme](https://github.com/intuit/cyphfell/tree/master/packages/cyphfell#usage-steps) 10 | 11 | ### Packages: 12 | #### [Cyphfell](https://github.com/intuit/cyphfell/tree/master/packages/cyphfell) 13 | The core conversion module. Add this package as a package.json dependency 14 | 15 | #### [Cyphfell Test Application](https://github.com/intuit/cyphfell/tree/master/packages/cyphfell-test-app) 16 | A React test application used to perform Cyphfell automation tests (this is only for the developers of the Cyphfell package to run automation) 17 | 18 | ### Contribution Guidelines 19 | 1. Pull requests are welcome, and should be made to the master branch 20 | 2. To highlight bugs, create an issue and add the **bug** label. 21 | 3. For feature requests, create an issue and add the **feature request** label 22 | 4. For framework support requests (e.g. adding in support for another automation testing framework that can be converted to Cypress), create an issue and add the **framework request** label 23 | 24 | ### Architecture (UML Activity Diagram): 25 | ![](https://github.com/intuit/cyphfell/blob/master/docs/Cyphfell%20Architecture.png?raw=true) 26 | -------------------------------------------------------------------------------- /packages/cyphfell/src/plugins/LoopWarningsPlugin.js: -------------------------------------------------------------------------------- 1 | const BaseWarning = require("../BaseWarningPlugin"); 2 | const estraverse = require("estraverse"); 3 | const FunctionBodyTransformer = require("../util/FunctionBodyTransformer"); 4 | const esprima = require("../util/EsprimaUtils"); 5 | 6 | /** 7 | * Detects when the user has loops with Cypress commands inside of them 8 | */ 9 | class LoopWarningsPlugin extends BaseWarning { 10 | 11 | /** 12 | * Walks the given AST and adds any warnings to the output report 13 | * @param {Object} ast - the AST to walk 14 | * @param {String} newFileDir - the new directory of the file 15 | */ 16 | run(ast, newFileDir) { 17 | estraverse.traverse(ast, { 18 | enter: (node) => { 19 | if ((node.type === "ForStatement" || node.type === "WhileStatement" || node.type === "DoWhileStatement") && 20 | FunctionBodyTransformer.isPromiseChain("", newFileDir)(esprima.generateCodeFromAST(node))) { 21 | this.reportWarning(node); 22 | } 23 | } 24 | }); 25 | } 26 | 27 | /** 28 | * Gets the unique name of this warning 29 | * @return {String} - the unique name of this warning 30 | */ 31 | getName() { 32 | return "LoopWarning"; 33 | } 34 | 35 | getMessage() { 36 | return "You have Cypress commands inside of a loop here. This may need to be done in a different way to be compatible with Cypress."; 37 | } 38 | } 39 | 40 | module.exports = LoopWarningsPlugin; -------------------------------------------------------------------------------- /packages/cyphfell/src/plugins/LongWaitWarningPlugin.js: -------------------------------------------------------------------------------- 1 | const BaseWarning = require("../BaseWarningPlugin"); 2 | const estraverse = require("estraverse"); 3 | const MAX_WAIT_MILLISECONDS = require("../constants/WarningConstants").MAX_WAIT_MILLISECONDS; 4 | 5 | /** 6 | * Detects when the user is waiting for a long time in their tests (this is an anti-pattern in Cypress) 7 | */ 8 | class LongWaitWarning extends BaseWarning { 9 | 10 | /** 11 | * Walks the given AST and adds any warnings to the output report 12 | * @param {Object} ast - the AST to walk 13 | */ 14 | run(ast) { 15 | estraverse.traverse(ast, { 16 | enter: (node) => { 17 | if (node.type === "CallExpression" && node.callee.object && node.callee.object.name === "cy" && node.callee.property && node.callee.property.name === "wait" && 18 | node.arguments[0] && node.arguments[0].type === "Literal" && !isNaN(node.arguments[0].value) && node.arguments[0].value > MAX_WAIT_MILLISECONDS) { 19 | this.reportWarning(node); 20 | } 21 | } 22 | }); 23 | } 24 | 25 | /** 26 | * Gets the unique name of this warning 27 | * @return {String} - the unique name of this warning 28 | */ 29 | getName() { 30 | return "LongWaitWarning"; 31 | } 32 | 33 | getMessage() { 34 | return `You are waiting for more than ${MAX_WAIT_MILLISECONDS} milliseconds here. This is an anti-pattern in Cypress, and should be avoided.`; 35 | } 36 | } 37 | 38 | module.exports = LongWaitWarning; -------------------------------------------------------------------------------- /packages/cyphfell/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cyphfell", 3 | "version": "0.2.8", 4 | "description": "Converts automation tests into Cypress format", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha --recursive --g \"./test/**/*.js\" test", 8 | "start": "cd ../cyphfell-test-app && export BROWSER='none' && yarn start && cd ../Cyphfell" 9 | }, 10 | "bin": { 11 | "cyphfell": "./cli.js" 12 | }, 13 | "keywords": [], 14 | "author": "", 15 | "license": "Apache License 2.0", 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/intuit/cyphfell.git#master" 19 | }, 20 | "dependencies": { 21 | "@babel/core": "^7.0.0-rc.1", 22 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0-rc.1", 23 | "babel-plugin-syntax-object-rest-spread": "^6.13.0", 24 | "babel-plugin-transform-class-properties": "^6.24.1", 25 | "babel-polyfill": "^6.26.0", 26 | "command-line-args": "^5.0.2", 27 | "css-what": "^2.1.0", 28 | "cypress": "^3.1.0", 29 | "escodegen": "^1.11.0", 30 | "eslint": "^5.6.1", 31 | "esprima": "^4.0.1", 32 | "estraverse": "^4.2.0", 33 | "fs-extra": "^7.0.0", 34 | "glob": "^7.1.3", 35 | "lodash": "^4.17.10", 36 | "relative": "^3.0.2", 37 | "stack-trace": "^0.0.10", 38 | "url": "^0.11.0" 39 | }, 40 | "devDependencies": { 41 | "chai": "^4.1.2", 42 | "mocha": "^5.2.0", 43 | "rewire": "^4.0.1", 44 | "sinon": "6.1.2" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/cyphfell/test-automation/cypress/integration/BrowserActionTests.spec.js: -------------------------------------------------------------------------------- 1 | describe("Tests browser util functions", function() { 2 | 3 | beforeEach(() => { 4 | cy.visit("http://localhost:3000/"); 5 | }); 6 | 7 | it("Tests browser.getAttribute()", () => { 8 | browser.getAttribute("#textInput", "id").should((id) => { 9 | expect(id).to.be.equal("textInput"); 10 | }); 11 | }); 12 | 13 | it("Tests browser.setValue()", () => { 14 | browser.setValue("#textInput", "abcdefghijk"); 15 | cy.get("#textInput").invoke("val").should((val) => { 16 | expect(val).to.be.equal("abcdefghijk"); 17 | }); 18 | 19 | browser.setValue("#textInput", "df"); 20 | cy.get("#textInput").invoke("val").should((val) => { 21 | expect(val).to.be.equal("df"); 22 | }); 23 | }); 24 | 25 | // TODO: uncomment after adding support for this command 26 | /*it("Tests browser.windowHandleSize()", () => { 27 | const res = browser.windowHandleSize(); 28 | expect(res).to.deep.equal({ 29 | width: 1000, 30 | height: 660 31 | }); 32 | 33 | browser.windowHandleSize({width: 800, height: 600}); 34 | const res2 = browser.windowHandleSize(); 35 | expect(res2).to.deep.equal({ 36 | width: 800, 37 | height: 600 38 | }); 39 | 40 | browser.windowHandleSize({width: 1000, height: 660}); 41 | });*/ 42 | }); -------------------------------------------------------------------------------- /packages/cyphfell/src/regex/PostParseRegex.js: -------------------------------------------------------------------------------- 1 | const regexUtil = require("../util/RegexUtil"); 2 | 3 | const removeUnnecessaryClosures = (str) => { 4 | const regex = new RegExp(`${regexUtil.formatRegex(".then(")}(.*?) => {\\s*\\1;\\s*${regexUtil.formatRegex("});")}`, "g"); 5 | const regex2 = new RegExp(`${regexUtil.formatRegex(".then(")}(.*?) => {\\s*${regexUtil.formatRegex("});")}`, "g"); 6 | const regex3 = new RegExp(`${regexUtil.formatRegex(".then(")}(.*?) => {\\s*return \\1;\\s*${regexUtil.formatRegex("});")}`, "g"); 7 | return str.replace(regex, ";").replace(regex2, ";").replace(regex3, ";"); 8 | }; 9 | 10 | const removeUnnecessaryExpressions = (str) => { 11 | const regex = new RegExp(`${regexUtil.formatRegex(".then(")}(.*?) => {\\s*\\1;`, "g"); 12 | let result; 13 | do { 14 | result = regex.exec(str); 15 | if (result) { 16 | str = str.replace(result[0], `.then(${result[1]} => {`); 17 | } 18 | 19 | } while (result); 20 | return str; 21 | }; 22 | 23 | const removeUnnecessaryVariables = (str) => { 24 | const regex = new RegExp(`${regexUtil.formatRegex(".then(")}(.*?) => {\\s*const ([^=]*?) = \\1;`, "g"); 25 | let result; 26 | do { 27 | result = regex.exec(str); 28 | if (result) { 29 | str = str.replace(result[0], `.then(${result[2]} => {`); 30 | } 31 | } while (result); 32 | return str; 33 | }; 34 | 35 | module.exports = (str) => { 36 | str = removeUnnecessaryClosures(str); 37 | str = removeUnnecessaryExpressions(str); 38 | str = removeUnnecessaryVariables(str); 39 | return str; 40 | }; -------------------------------------------------------------------------------- /packages/cyphfell/src/plugins/ElementsIteratorsPlugin.js: -------------------------------------------------------------------------------- 1 | const BasePlugin = require("../BasePlugin"); 2 | const estraverse = require("estraverse"); 3 | const arrays = require("../CypressArrayUtil"); 4 | const FunctionBodyTransformer = require("../util/FunctionBodyTransformer"); 5 | const esprima = require("../util/EsprimaUtils"); 6 | 7 | /** 8 | * This plugin transforms array iterator functions that have asynchronous logic performed in them into an async equivalent of that function 9 | */ 10 | class ElementsIteratorsPlugin extends BasePlugin { 11 | 12 | beforeTransformAfterParsing(ast, newFileDir) { 13 | const filter = FunctionBodyTransformer.isPromiseChain(esprima.generateCodeFromAST(ast), newFileDir); 14 | estraverse.traverse(ast, { 15 | enter: (node) => { 16 | if (node.type === "CallExpression" && node.callee.type === "MemberExpression" && node.callee.property && 17 | node.arguments[0] && (node.arguments[0].type === "ArrowFunctionExpression" || node.arguments[0].type === "FunctionExpression") && 18 | filter(esprima.generateCodeFromAST(node.arguments[0]), null, null, new Map())) { 19 | const cyFunc = arrays.getCypressFunction(node.callee.property.name); 20 | if (cyFunc) { 21 | node.callee.property.name = cyFunc; 22 | } 23 | } 24 | } 25 | }); 26 | } 27 | 28 | /** 29 | * Gets the unique name of this plugin 30 | * @return {String} - the unique name of this plugin 31 | */ 32 | getName() { 33 | return "ElementsIterators"; 34 | } 35 | 36 | } 37 | 38 | module.exports = ElementsIteratorsPlugin; -------------------------------------------------------------------------------- /packages/cyphfell/defaultFiles/defaultSupportFile.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // define custom commands 17 | import "./commands"; 18 | import "./frameworkCommands"; 19 | 20 | const arrays = require("cyphfell/src/CypressArrayUtil.js"); 21 | arrays.init(); 22 | 23 | // keep local storage data after each test 24 | Cypress.LocalStorage.clear = function() {}; 25 | 26 | // keep cookies after each test 27 | Cypress.Cookies.defaults({ 28 | whitelist: function() { 29 | return true; 30 | } 31 | }); 32 | 33 | // don't crash automation when an application error happens 34 | Cypress.on("uncaught:exception", () => { 35 | return false; 36 | }); 37 | 38 | const consoleSpies = []; 39 | const initSpy = (method, win) => { 40 | consoleSpies.push({level: method.toUpperCase(), spy: cy.spy(win.console, method)}); 41 | }; 42 | 43 | Cypress.on("window:before:load", (win) => { 44 | consoleSpies.length = 0; 45 | initSpy("info", win); 46 | initSpy("error", win); 47 | initSpy("warn", win); 48 | initSpy("log", win); 49 | initSpy("debug", win); 50 | }); -------------------------------------------------------------------------------- /packages/cyphfell/src/constants/CommandsToWrap.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const esprima = require("../util/EsprimaUtils"); 3 | const estraverse = require("estraverse"); 4 | const fp = require("../util/FilePathUtil"); 5 | const tempSelected = require("../converters/wdio/TemporaryRegexReplacements").IS_SELECTED.temporary; 6 | const converter = require("../converters/ActiveConverter"); 7 | 8 | const customCommands = () => { 9 | const fileContents = [ 10 | fs.readFileSync(fp.findLocalFilePath("defaultFiles/defaultCommandsFile.js"), "utf8"), 11 | converter.getStrategy().getCommandsFileContents() 12 | ]; 13 | const commands = []; 14 | 15 | fileContents.forEach((lines) => { 16 | const ast = esprima.generateAST(lines); 17 | estraverse.traverse(ast, { 18 | enter: (node) => { 19 | if (node.type === "CallExpression" && node.arguments[1] && node.arguments[1].type === "ObjectExpression" && 20 | node.arguments[1].properties[0] && node.arguments[1].properties[0].kind === "init" && node.arguments[1].properties[0].key.name === "prevSubject" && 21 | node.arguments[1].properties[0].value.value === "element") { 22 | commands.push(node.arguments[0].value); 23 | } 24 | } 25 | }); 26 | }); 27 | 28 | return commands; 29 | }; 30 | 31 | module.exports = [ 32 | "click", 33 | "type", 34 | "invoke", 35 | "its", 36 | "focus", 37 | "blur", 38 | "check", 39 | "clear", 40 | "children", 41 | "closest", 42 | //"contains", 43 | "dblclick", 44 | //"filter", 45 | "parent", 46 | "scrollIntoView", 47 | "scrollTo", 48 | "select", 49 | "siblings", 50 | "submit", 51 | "trigger", 52 | "uncheck", 53 | tempSelected 54 | ].concat(customCommands()); -------------------------------------------------------------------------------- /packages/cyphfell-test-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 21 | Cyphfell Test App 22 | 23 | 24 | 27 |
28 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /packages/cyphfell/src/plugins/ShouldAssertionsPlugin.js: -------------------------------------------------------------------------------- 1 | const BasePlugin = require("../BasePlugin"); 2 | const estraverse = require("estraverse"); 3 | const esprima = require("../util/EsprimaUtils"); 4 | 5 | /** 6 | * This plugin transforms Cypress .then() closures which only have assertions performed in them into .should() blocks instead 7 | */ 8 | // TODO: make this plugin execute after WrapElementActions 9 | class ShouldAssertionsPlugin extends BasePlugin { 10 | 11 | afterTransformAfterParsing(ast) { 12 | estraverse.traverse(ast, { 13 | enter: (node) => { 14 | // TODO: direct function calls like xyz().then(() => { ... }) instead of something.somethingElse().then(() => { ... }); 15 | if (node.callee && node.callee.object && node.callee.object.type === "CallExpression" && 16 | node.callee.property && node.callee.object.callee.property && node.callee.property.name === "then" && 17 | (node.arguments[0].type === "ArrowFunctionExpression" || node.arguments[0].type === "FunctionExpression") && 18 | node.arguments[0].body.body) { 19 | let allAssertions = true; 20 | for (const child of node.arguments[0].body.body) { 21 | const code = esprima.generateCodeFromAST(child); 22 | if (!code.includes("expect(") && !code.includes("assert(") && !code.includes(".should(") && !code.includes(".should.") 23 | && !code.includes("assert.")) { 24 | allAssertions = false; 25 | break; 26 | } 27 | } 28 | if (allAssertions) { 29 | node.callee.property.name = "should"; 30 | } 31 | } 32 | } 33 | }); 34 | } 35 | 36 | getName() { 37 | return "ShouldAssertions"; 38 | } 39 | } 40 | 41 | module.exports = ShouldAssertionsPlugin; -------------------------------------------------------------------------------- /packages/cyphfell/test/e2e/noImports/SelectByConversionTests.js: -------------------------------------------------------------------------------- 1 | const fp = require("../../../src/util/FileParser"); 2 | const plugins = require("../../../src/util/PluginsUtil"); 3 | const esprima = require("../../../src/util/EsprimaUtils"); 4 | 5 | describe("Tests SelectBy function conversions", function() { 6 | 7 | let pluginsList = null; 8 | before(() => { 9 | global.options = { 10 | cypressFolder: "test/cypress/", 11 | baseNormalFolder: "test/" 12 | }; 13 | pluginsList = plugins.loadPlugins("./src/plugins/*"); 14 | }); 15 | 16 | it("Tests conversion of the selectByAttribute command", () => { 17 | const res = fp(` 18 | class X { 19 | testMethod() { 20 | browser.selectByAttribute(".class", "attrb", "val"); 21 | } 22 | } 23 | `, `${process.cwd()}/test/test.js`, global.options, pluginsList); 24 | expect(esprima.generateAST(res)).to.deep.equal(esprima.generateAST(` 25 | class X { 26 | testMethod() { 27 | cy.get(".class").selectByAttribute("attrb", "val"); 28 | } 29 | } 30 | `)); 31 | }); 32 | 33 | it("Tests conversion of the selectByIndex command", () => { 34 | const res = fp(` 35 | class X { 36 | testMethod() { 37 | browser.selectByIndex(".class", 4); 38 | } 39 | } 40 | `, `${process.cwd()}/test/test.js`, global.options, pluginsList); 41 | expect(esprima.generateAST(res)).to.deep.equal(esprima.generateAST(` 42 | class X { 43 | testMethod() { 44 | cy.get(".class").selectByIndex(4); 45 | } 46 | } 47 | `)); 48 | }); 49 | }); -------------------------------------------------------------------------------- /packages/cyphfell/src/util/PluginsUtil.js: -------------------------------------------------------------------------------- 1 | const glob = require("glob"), 2 | path = require("path"); 3 | 4 | class PluginsUtil { 5 | 6 | static loadPlugins(pattern) { 7 | const allPlugins = []; 8 | glob.sync(pattern).forEach((file) => { 9 | const pluginImport = require(path.resolve(file)); 10 | allPlugins.push(new pluginImport()); 11 | }); 12 | return allPlugins; 13 | } 14 | 15 | /** 16 | * Gets all activated plugins 17 | * @param {Array} extraPlugins - external plugins that should be activated 18 | * @param {Array} disabledDefaultPlugins - the unique ID of each default plugin to disable 19 | * @param {String} pattern - the glob pattern to use to load default plugins 20 | * @return {Array} - all activated plugins 21 | */ 22 | static getActivatedPlugins(extraPlugins, disabledDefaultPlugins, pattern = "./node_modules/cyphfell/src/plugins/*") { 23 | const framework = require("../converters/ActiveConverter").getStrategy().getName(); 24 | return this.loadPlugins(pattern).filter((plugin) => { 25 | return !disabledDefaultPlugins.some((id) => plugin.getName() === id); 26 | }).concat(extraPlugins).filter((plugin) => plugin.getSupportedFrameworks().includes(framework)); 27 | } 28 | 29 | /** 30 | * Invokes the given method across all plugins 31 | * @param {Array} plugins - all plugins that are active 32 | * @param {Object} ast - the AST to pass into the method 33 | * @param {String} methodName - the name of the method to invoke 34 | * @param {String} newFileDir - the new path to the file that is currently being modified 35 | */ 36 | static invokePlugins(plugins, ast, methodName, newFileDir) { 37 | plugins.forEach((plugin) => { 38 | plugin[methodName](ast, newFileDir); 39 | }); 40 | } 41 | } 42 | 43 | module.exports = PluginsUtil; 44 | 45 | -------------------------------------------------------------------------------- /packages/cyphfell/test/unit/plugins/LambdaExpressionsTests.js: -------------------------------------------------------------------------------- 1 | const esprima = require("../../../src/util/EsprimaUtils"); 2 | const _ = require("lodash"); 3 | let plugin = require("../../../src/plugins/LambdaExpressionsPlugin"); 4 | plugin = new plugin(); 5 | 6 | describe("Tests LambdaExpressions plugin", function() { 7 | before(() => { 8 | global.options = { 9 | transpile: false 10 | }; 11 | }); 12 | 13 | it("Tests to make sure Lambda expressions are changed", () => { 14 | const ast = esprima.generateAST("xyz.randomFuncName(() => true)"); 15 | plugin.beforeParseLines(ast); 16 | expect(ast.body[0]).to.deep.equal({ 17 | "type": "ExpressionStatement", 18 | "expression": { 19 | "type": "CallExpression", 20 | "callee": { 21 | "type": "MemberExpression", 22 | "computed": false, 23 | "object": { 24 | "type": "Identifier", 25 | "name": "xyz" 26 | }, 27 | "property": { 28 | "type": "Identifier", 29 | "name": "randomFuncName" 30 | } 31 | }, 32 | "arguments": [ 33 | { 34 | "type": "ArrowFunctionExpression", 35 | "id": null, 36 | "params": [], 37 | "body": { 38 | "type": "BlockStatement", 39 | "body": [ 40 | { 41 | "type": "ReturnStatement", 42 | "argument": { 43 | "type": "Literal", 44 | "value": true, 45 | "raw": "true" 46 | } 47 | } 48 | ] 49 | }, 50 | "generator": false, 51 | "expression": false, 52 | "async": false 53 | } 54 | ] 55 | } 56 | }); 57 | }); 58 | 59 | it("Tests to make sure non-expression Lambda functions are not changed", () => { 60 | const ast = esprima.generateAST("xyz.randomFuncName(() => {return true;})"); 61 | const copy = _.cloneDeep(ast); 62 | plugin.beforeParseLines(ast); 63 | expect(ast).to.deep.equal(copy); 64 | }); 65 | 66 | it("Tests plugin name", () => { 67 | expect(plugin.getName()).to.be.equal("LambdaExpressions"); 68 | }); 69 | 70 | }); -------------------------------------------------------------------------------- /packages/cyphfell/test/e2e/IndexTests.js: -------------------------------------------------------------------------------- 1 | const index = require("../../index"); 2 | const sinon = require("sinon"); 3 | const fs = require("fs-extra"); 4 | const converter = require("../../src/converters/ActiveConverter"); 5 | const wdio = require("../../src/converters/wdio/WDIOStrategy"); 6 | const report = require("../../src/reports/ReportGenerator"); 7 | const dir = require("../../src/util/DirectoryUtils"); 8 | const handlers = require("../../src/handlers/FileHanderList"); 9 | 10 | describe("Tests the complete conversion process", function () { 11 | 12 | let sandbox = null; 13 | 14 | beforeEach(() => { 15 | sandbox = sinon.createSandbox(); 16 | sandbox.stub(fs, "mkdirsSync"); 17 | sandbox.stub(fs, "writeFileSync"); 18 | sandbox.stub(fs, "mkdirSync"); 19 | handlers.forEach((handler) => { 20 | sandbox.stub(handler, "parseImpl").returns("const x;"); 21 | }); 22 | }); 23 | 24 | it("Tests to make sure the correct converter is selected by default", () => { 25 | sandbox.spy(converter, "init"); 26 | sandbox.stub(dir, "getAllTestFiles").returns([]); 27 | 28 | index({validateCypressDir: false}); 29 | expect(converter.init.calledOnce).to.be.true; 30 | expect(converter.getStrategy().getName()).to.be.equal(new wdio().getName(), "The WDIO strategy was not selected by default"); 31 | }); 32 | 33 | it("Tests to make sure a report is generated", () => { 34 | sandbox.spy(report, "generateReport"); 35 | sandbox.stub(dir, "getAllTestFiles").returns([]); 36 | 37 | index({validateCypressDir: false}); 38 | expect(report.generateReport.calledOnce).to.be.true; 39 | }); 40 | 41 | it("Tests to make sure this file was converted", () => { 42 | index({validateCypressDir: false}); 43 | 44 | let foundFile = false; 45 | for (let i = 0; i < fs.writeFileSync.callCount; ++i) { 46 | if (fs.writeFileSync.getCall(i).args[0] === `${process.cwd()}/test/cypress/integration/e2e/IndexTests.spec.js`) { 47 | foundFile = true; 48 | break; 49 | } 50 | } 51 | expect(foundFile).to.be.true; 52 | }); 53 | 54 | afterEach(() => { 55 | sandbox.restore(); 56 | }); 57 | }); -------------------------------------------------------------------------------- /packages/cyphfell/defaultFiles/defaultCommandsFile.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | 27 | const validator = require("css-what"); 28 | 29 | Cypress.Commands.overwrite("get", (originalFn, selector, options) => { 30 | try { 31 | validator(selector); 32 | } catch (ex) { 33 | return cy.document().then((document) => { 34 | return cy.wrap(document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).should("not.be.null").then((el) => { 35 | return cy.wrap(el); 36 | }); 37 | }); 38 | } 39 | 40 | if (options && options.multiple) { 41 | const array = []; 42 | return cy.wrap(originalFn(selector, options)).each((res) => { 43 | array.push(res); 44 | }).then(() => { 45 | return array; 46 | }); 47 | } else { 48 | return cy.wrap(originalFn(selector, options)).first(); 49 | } 50 | }); 51 | 52 | Cypress.Commands.add("getAll", (selector, options = {}) => { 53 | return cy.get(selector, Object.assign(options, {multiple: true})); 54 | }); 55 | 56 | Cypress.Commands.add("findFirst", {prevSubject: "element"}, (subject, selector) => { 57 | return cy.wrap(subject).find(selector).then((res) => { 58 | return cy.wrap(res[0]); 59 | }); 60 | }); -------------------------------------------------------------------------------- /packages/cyphfell/test-automation/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | 27 | const validator = require("css-what"); 28 | 29 | Cypress.Commands.overwrite("get", (originalFn, selector, options) => { 30 | try { 31 | validator(selector); 32 | } catch (ex) { 33 | return cy.document().then((document) => { 34 | return cy.wrap(document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue).should("not.be.null").then((el) => { 35 | return cy.wrap(el); 36 | }); 37 | }); 38 | } 39 | 40 | if (options && options.multiple) { 41 | const array = []; 42 | return cy.wrap(originalFn(selector, options)).each((res) => { 43 | array.push(res); 44 | }).then(() => { 45 | return array; 46 | }); 47 | } else { 48 | return cy.wrap(originalFn(selector, options)).first(); 49 | } 50 | }); 51 | 52 | Cypress.Commands.add("getAll", (selector, options = {}) => { 53 | return cy.get(selector, Object.assign(options, {multiple: true})); 54 | }); 55 | 56 | Cypress.Commands.add("findFirst", {prevSubject: "element"}, (subject, selector) => { 57 | return cy.wrap(subject).find(selector).then((res) => { 58 | return cy.wrap(res[0]); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/cyphfell/src/converters/BaseStrategy.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | 3 | class BaseStrategy { 4 | 5 | /** 6 | * Initializes this strategy after construction 7 | */ 8 | init() { 9 | const path = this.getCommandsFilePath(); 10 | try { 11 | if (path !== "") { 12 | this.commandsFileContents = fs.readFileSync(path, "utf8"); 13 | } else { 14 | this.commandsFileContents = ""; 15 | } 16 | } catch (ex) { 17 | console.error(ex); 18 | this.commandsFileContents = ""; 19 | } 20 | } 21 | 22 | /** 23 | * Gets a list of all regular expressions to replace, and what to replace them with 24 | * @return {Array} - list of all regular expressions to replace, and what to replace them with 25 | */ 26 | getReplacementRegex() { 27 | return []; 28 | } 29 | 30 | /** 31 | * Gets a list of all regular expressions that have been transformed and need a .then() chained on to them 32 | * @return {Array} - list of regex representing transformed code 33 | */ 34 | getReplacedReturnRegex() { 35 | return []; 36 | } 37 | 38 | /** 39 | * Gets the name of the framework being used 40 | * @return {String} - the framework name 41 | */ 42 | getName() { 43 | return ""; 44 | } 45 | 46 | /** 47 | * Gets text that this strategy needs to append to the default support file 48 | * @return {String} - the text that this strategy needs to append to the default support file 49 | */ 50 | getSupportAppendText() { 51 | return ""; 52 | } 53 | 54 | /** 55 | * Gets the absolute path to the custom commands file for this strategy 56 | * @return {String} - the absolute path to the custom commands file for this strategy if there is one, or an empty string otherwise 57 | */ 58 | getCommandsFilePath() { 59 | return ""; 60 | } 61 | 62 | /** 63 | * Gets the contents of this strategy's custom commands file 64 | * @return {String} - the contents of this strategy's custom commands file 65 | */ 66 | getCommandsFileContents() { 67 | return this.commandsFileContents; 68 | } 69 | } 70 | 71 | module.exports = BaseStrategy; -------------------------------------------------------------------------------- /packages/cyphfell/src/CypressArrayUtil.js: -------------------------------------------------------------------------------- 1 | const cypressArrayFunc = (array, callback, methodName) => { 2 | const evaluated = []; 3 | return cy.wrap(array).each((arg, i) => { 4 | const res = callback(arg); 5 | try { 6 | return res.then((res) => { 7 | evaluated[i] = res; 8 | }); 9 | } catch (ex) { 10 | evaluated[i] = res; 11 | return true; 12 | } 13 | }).then(() => { 14 | if (methodName === "find") { 15 | return array.find((item, index) => evaluated[index]) || null; 16 | } 17 | return methodName ? array[methodName]((item, index) => evaluated[index]) : evaluated; 18 | }); 19 | }; 20 | 21 | class CypressArrayUtil { 22 | 23 | /** 24 | * Initializes all functions that allow array iteration with cypress commands inside of the callback function 25 | */ 26 | static init() { 27 | Array.prototype.mapCypress = function(callback) { 28 | return cypressArrayFunc(this, callback); 29 | }; 30 | Array.prototype.filterCypress = function(callback) { 31 | return cypressArrayFunc(this, callback, "filter"); 32 | }; 33 | Array.prototype.findCypress = function(callback) { 34 | return cypressArrayFunc(this, callback, "find"); 35 | }; 36 | Array.prototype.findIndexCypress = function(callback) { 37 | return cypressArrayFunc(this, callback, "findIndex"); 38 | }; 39 | Array.prototype.someCypress = function(callback) { 40 | return cypressArrayFunc(this, callback, "some"); 41 | }; 42 | Array.prototype.everyCypress = function(callback) { 43 | return this.filterCypress(callback).then((res) => { 44 | return res.length === this.length; 45 | }); 46 | }; 47 | } 48 | 49 | /** 50 | * Gets the name of the global function that should be used to wrap a specific array iterator 51 | * @param {String} methodName - the name of the iterator method to wrap 52 | * @return {String} - the name of the function to wrap the iterator if valid, null otherwise 53 | */ 54 | static getCypressFunction(methodName) { 55 | if (this.modifiableIterators.includes(methodName)) { 56 | return `${methodName}Cypress`; 57 | } 58 | return null; 59 | } 60 | } 61 | 62 | CypressArrayUtil.modifiableIterators = ["map", "filter", "find", "findIndex", "some", "every"]; 63 | module.exports = CypressArrayUtil; -------------------------------------------------------------------------------- /packages/cyphfell/src/util/FileParser.js: -------------------------------------------------------------------------------- 1 | const esprima = require("./EsprimaUtils"), 2 | estraverse = require("estraverse"), 3 | handlers = require("../handlers/FileHanderList"), 4 | report = require("../reports/ReportGenerator"); 5 | 6 | const separateVariableDeclarations = (str) => { 7 | const ast = esprima.generateAST(str); 8 | const combinable = []; 9 | estraverse.traverse(ast, { 10 | enter: (node, parent) => { 11 | if (node.type === "VariableDeclaration" && node.declarations.length > 1) { 12 | combinable.push({declarations: [], original: node, kind: node.kind}); 13 | } else if (node.type === "VariableDeclarator" && parent.declarations.length > 1) { 14 | combinable[combinable.length - 1].declarations.push(node); 15 | } 16 | } 17 | }); 18 | combinable.forEach((declGroup) => { 19 | const replacement = declGroup.declarations.reduce((prev, decl) => { 20 | return `${prev}\n${declGroup.kind} ${esprima.generateCodeFromAST(decl)};`; 21 | }, ""); 22 | str = str.replace(esprima.generateCodeFromAST(declGroup.original), replacement); 23 | }); 24 | return str; 25 | }; 26 | 27 | /** 28 | * Parses a test file and converts it into Cypress format if necessary 29 | * @param {String} lines - all of the lines in the file 30 | * @param {String} fileDir - the path to the file 31 | * @param {Object} options - the configuration options passed in 32 | * @param {Array} plugins - all plugins that will modify the AST 33 | * @return {String} - the lines in the file, after modifications are made 34 | */ 35 | module.exports = (lines, fileDir, options, plugins) => { 36 | try { 37 | if (!fileDir.includes(".json")) { 38 | lines = esprima.fixInconsistentSpacing(lines); 39 | lines = separateVariableDeclarations(lines); 40 | } 41 | for (const handler of handlers) { 42 | const res = handler.handleParseAttempt(lines, fileDir, plugins); 43 | if (res) { 44 | //return res.replace(/;\s*?;/g, ";"); 45 | return res.replace(/\s+;\s/g, "\n"); 46 | } else if (lines === "" && handler.canHandle(fileDir)) { 47 | return ""; 48 | } 49 | } 50 | throw new Error("No handler found"); 51 | } catch (ex) { 52 | console.error("Could not parse: " + fileDir); 53 | console.error(ex); 54 | report.onCriticalError(ex.stack); 55 | return lines; 56 | } 57 | }; -------------------------------------------------------------------------------- /packages/cyphfell/test/unit/plugins/LoopWarningTests.js: -------------------------------------------------------------------------------- 1 | const sinon = require("sinon"); 2 | const LongWaitWarning = require("../../../src/plugins/LoopWarningsPlugin"); 3 | const esprima = require("../../../src/util/EsprimaUtils"); 4 | const FunctionBodyTransformer = require("../../../src/util/FunctionBodyTransformer"); 5 | 6 | describe("Tests the loop warning plugin", function() { 7 | const warning = new LongWaitWarning(); 8 | 9 | let sandbox = null; 10 | beforeEach(() => { 11 | sandbox = sinon.createSandbox(); 12 | sandbox.stub(warning, "reportWarning"); 13 | }); 14 | 15 | it("Tests with no loops", () => { 16 | warning.run(esprima.generateAST(` 17 | const x = () => { 18 | call.someFunction(); 19 | }; 20 | `)); 21 | expect(warning.reportWarning.notCalled).to.be.true; 22 | }); 23 | 24 | it("Tests with a while loop with no commands inside of it", () => { 25 | warning.run(esprima.generateAST(` 26 | const x = () => { 27 | while (true) { 28 | doNothing(); 29 | } 30 | }; 31 | `)); 32 | expect(warning.reportWarning.notCalled).to.be.true; 33 | }); 34 | 35 | it("Tests with a while loop with cypress commands inside of it", () => { 36 | sandbox.stub(FunctionBodyTransformer, "isPromiseChain").returns(() => true); 37 | warning.run(esprima.generateAST(` 38 | const x = () => { 39 | while (true) { 40 | doNothing(); 41 | } 42 | }; 43 | `)); 44 | expect(warning.reportWarning.calledOnce).to.be.true; 45 | }); 46 | 47 | it("Tests with a for loop with cypress commands inside of it", () => { 48 | sandbox.stub(FunctionBodyTransformer, "isPromiseChain").returns(() => true); 49 | warning.run(esprima.generateAST(` 50 | const x = () => { 51 | for (let i = 0; i < 25; ++i) { 52 | doNothing(); 53 | } 54 | }; 55 | `)); 56 | expect(warning.reportWarning.calledOnce).to.be.true; 57 | }); 58 | 59 | it("Tests with a do loop with cypress commands inside of it", () => { 60 | sandbox.stub(FunctionBodyTransformer, "isPromiseChain").returns(() => true); 61 | warning.run(esprima.generateAST(` 62 | const x = () => { 63 | do { 64 | doNothing(); 65 | } while (true); 66 | }; 67 | `)); 68 | expect(warning.reportWarning.calledOnce).to.be.true; 69 | }); 70 | 71 | afterEach(() => { 72 | sandbox.restore(); 73 | }); 74 | }); -------------------------------------------------------------------------------- /packages/cyphfell/src/converters/wdio/TemporaryRegexReplacements.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file keeps track of text that appears in a file to prevent it from being mixed up in some other context. For example, some RegExp conversions 3 | * may ultimately result in the same function name, causing an infinite loop. This file lets those be temporarily be changed to some other 4 | * text that will not result in this happening 5 | */ 6 | module.exports = { 7 | IS_SELECTED: { 8 | temporary: "isSubjectSelected", 9 | real: "isSelected" 10 | }, 11 | ELEMENTID_ATTRIBUTE: { 12 | temporary: "elementIdAttributeTemp", 13 | real: "elementIdAttribute" 14 | }, 15 | ELEMENTID_CSS_PROPERTY: { 16 | temporary: "elementIdCssPropertyTemp", 17 | real: "elementIdCssProperty" 18 | }, 19 | ELEMENTID_DISPLAYED: { 20 | temporary: "elementIdDisplayedTemp", 21 | real: "elementIdDisplayed" 22 | }, 23 | ELEMENTID_ELEMENT: { 24 | temporary: "elementIdElementTemp", 25 | real: "elementIdElement" 26 | }, 27 | ELEMENTID_ENABLED: { 28 | temporary: "elementIdEnabledTemp", 29 | real: "elementIdEnabled" 30 | }, 31 | ELEMENTID_LOCATION: { 32 | temporary: "elementIdLocationTemp", 33 | real: "elementIdLocation" 34 | }, 35 | ELEMENTID_LOCATION_IN_VIEW: { 36 | temporary: "elementIdLocationInViewTemp", 37 | real: "elementIdLocationInView" 38 | }, 39 | ELEMENTID_NAME: { 40 | temporary: "elementIdNameTemp", 41 | real: "elementIdName" 42 | }, 43 | ELEMENTID_SELECTED: { 44 | temporary: "elementIdSelectedTemp", 45 | real: "elementIdSelected" 46 | }, 47 | ELEMENTID_SIZE: { 48 | temporary: "elementIdSizeTemp", 49 | real: "elementIdSize" 50 | }, 51 | ELEMENTID_TEXT: { 52 | temporary: "elementIdTextTemp", 53 | real: "elementIdText" 54 | }, 55 | LOG: { 56 | temporary: "logTemp", 57 | real: "log" 58 | }, 59 | SELECT_BY_ATTRIBUTE: { 60 | temporary: "selectByAttributeTemp", 61 | real: "selectByAttribute" 62 | }, 63 | SELECT_BY_INDEX: { 64 | temporary: "selectByIndexTemp", 65 | real: "selectByIndex" 66 | }, 67 | SUBMIT_FORM: { 68 | temporary: "submitFormTemp", 69 | real: "submitForm" 70 | }, 71 | GET_LOCATION: { 72 | temporary: "getLocationTemp", 73 | real: "getLocation" 74 | }, 75 | GET_HTML: { 76 | temporary: "getHTMLTemp", 77 | real: "getHTML" 78 | }, 79 | GET_LOCATION_IN_VIEW: { 80 | temporary: "getLocationInViewTemp", 81 | real: "getLocationInView" 82 | } 83 | }; -------------------------------------------------------------------------------- /packages/cyphfell/src/BasePlugin.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | 3 | const frameworks = require("./constants/FrameworkConstants"); 4 | 5 | /** 6 | * Represents a plugin that can make modifications to a JavaScript file's AST 7 | */ 8 | class BasePlugin { 9 | 10 | /** 11 | * Makes modifications to the AST before lines are converted using regular expression 12 | * @param {Object} ast - the AST representing the code in a file 13 | * @param {String} newFileDir - the new directory of the file being modified 14 | */ 15 | beforeParseLines(ast, newFileDir) { 16 | } 17 | 18 | /** 19 | * Makes modifications to the AST after lines are converted using regular expression 20 | * @param {Object} ast - the AST representing the code in a file 21 | * @param {String} newFileDir - the new directory of the file being modified 22 | */ 23 | afterParseLines(ast, newFileDir) { 24 | } 25 | 26 | /** 27 | * Makes modifications to the AST before transforming the code after the initial regular expression conversion is finished 28 | * @param {Object} ast - the AST representing the code in a file 29 | * @param {String} newFileDir - the new directory of the file being modified 30 | */ 31 | beforeTransformAfterParsing(ast, newFileDir) { 32 | } 33 | 34 | /** 35 | * Makes modifications to the AST after transforming the code after the initial regular expression conversion is finished 36 | * @param {Object} ast - the AST representing the code in a file 37 | * @param {String} newFileDir - the new directory of the file being modified 38 | */ 39 | afterTransformAfterParsing(ast, newFileDir) { 40 | } 41 | 42 | /** 43 | * Provides access to the AST after all changes are made to a file. Changes made to the AST here will not be used 44 | * @param {Object} ast - the AST representing the final converted code for a file 45 | * @param {String} newFileDir - the new directory of the file being modified 46 | */ 47 | afterComplete(ast, newFileDir) { 48 | } 49 | 50 | /** 51 | * Gets the unique name of this plugin 52 | * @return {String} - the unique name of this plugin 53 | */ 54 | getName() { 55 | return ""; 56 | } 57 | 58 | /** 59 | * Gets a list of all frameworks that this plugin supports 60 | * @return {Array} - the unique names of all frameworks that this plugin supports 61 | */ 62 | getSupportedFrameworks() { 63 | return [frameworks.WebdriverIO]; 64 | } 65 | 66 | } 67 | 68 | module.exports = BasePlugin; -------------------------------------------------------------------------------- /packages/cyphfell/test/unit/util/PluginsUtilTests.js: -------------------------------------------------------------------------------- 1 | const rewire = require("rewire"); 2 | const plugins = rewire("../../../src/util/PluginsUtil"); 3 | const sinon = require("sinon"); 4 | const wdio = require("../../../src/constants/FrameworkConstants"); 5 | const _ = require("lodash"); 6 | 7 | describe("Tests PluginsUtil functions", function () { 8 | 9 | const plugin1 = { 10 | getName: () => "TestName1", 11 | getSupportedFrameworks: () => [wdio.WebdriverIO] 12 | }; 13 | const plugin2 = { 14 | getName: () => "SecondPluginTest", 15 | getSupportedFrameworks: () => [wdio.WebdriverIO] 16 | }; 17 | const extra1 = { 18 | getName: () => "ExtraPlugin1", 19 | getSupportedFrameworks: () => [wdio.WebdriverIO] 20 | }; 21 | let sandbox = null; 22 | 23 | beforeEach(() => { 24 | sandbox = sinon.createSandbox(); 25 | sandbox.stub(plugins, "loadPlugins").returns([plugin1, plugin2]); 26 | }); 27 | 28 | it("Tests plugin disabling", () => { 29 | expect(plugins.getActivatedPlugins([], [plugin1.getName()])).to.deep.equal([plugin2]); 30 | expect(plugins.getActivatedPlugins([], [plugin1.getName(), plugin2.getName()])).to.deep.equal([]); 31 | expect(plugins.getActivatedPlugins([], [plugin2.getName()])).to.deep.equal([plugin1]); 32 | 33 | expect(plugins.getActivatedPlugins([], ["NOT A PLUGIN NAME"])).to.deep.equal([plugin1, plugin2]); 34 | expect(plugins.getActivatedPlugins([], [plugin1.getName(), "NOT A PLUGIN NAME"])).to.deep.equal([plugin2]); 35 | expect(plugins.getActivatedPlugins([extra1], [plugin1.getName(), extra1.getName()])).to.deep.equal([plugin2, extra1], "Extra plugins should not be disabled"); 36 | }); 37 | 38 | it("Tests with no disabled plugin", () => { 39 | expect(plugins.getActivatedPlugins([], [])).to.deep.equal([plugin1, plugin2]); 40 | }); 41 | 42 | it("Tests with extra plugins", () => { 43 | const loaded = plugins.getActivatedPlugins([extra1], []); 44 | expect(loaded.length).to.be.equal(3); 45 | expect(loaded.includes(extra1)).to.be.true; 46 | expect(loaded.includes(plugin1)).to.be.true; 47 | expect(loaded.includes(plugin2)).to.be.true; 48 | }); 49 | 50 | it("Tests to make sure plugins that don't support the current framework are not loaded", () => { 51 | const extra2 = _.cloneDeep(extra1); 52 | extra2.getSupportedFrameworks = () => ["none"]; 53 | plugins.loadPlugins.returns([]); 54 | const loaded = plugins.getActivatedPlugins([extra2], []); 55 | expect(loaded.length).to.be.equal(0); 56 | }); 57 | 58 | afterEach(() => { 59 | sandbox.restore(); 60 | }); 61 | 62 | }); -------------------------------------------------------------------------------- /packages/cyphfell/test/unit/plugins/LongWaitWarningTests.js: -------------------------------------------------------------------------------- 1 | const sinon = require("sinon"); 2 | const LongWaitWarning = require("../../../src/plugins/LongWaitWarningPlugin"); 3 | const esprima = require("../../../src/util/EsprimaUtils"); 4 | const MAX_WAIT_MILLISECONDS = require("../../../src/constants/WarningConstants").MAX_WAIT_MILLISECONDS; 5 | 6 | describe("Tests the long wait warning", function() { 7 | const warning = new LongWaitWarning(); 8 | 9 | let sandbox = null; 10 | beforeEach(() => { 11 | sandbox = sinon.createSandbox(); 12 | sandbox.stub(warning, "reportWarning"); 13 | }); 14 | 15 | it("Tests with no waits", () => { 16 | warning.run(esprima.generateAST(` 17 | const x = () => { 18 | cy.doSomething(); 19 | cy.xyz(); 20 | browser.wait(25); 21 | browser.pause(55); 22 | }; 23 | `)); 24 | expect(warning.reportWarning.notCalled).to.be.true; 25 | }); 26 | 27 | it("Tests with short waits", () => { 28 | warning.run(esprima.generateAST(` 29 | const x = () => { 30 | cy.doSomething(); 31 | cy.xyz(); 32 | cy.wait(${MAX_WAIT_MILLISECONDS / 2}); 33 | browser.pause(55); 34 | }; 35 | `)); 36 | expect(warning.reportWarning.notCalled).to.be.true; 37 | }); 38 | 39 | it("Tests with long waits", () => { 40 | warning.run(esprima.generateAST(` 41 | const x = () => { 42 | cy.doSomething(); 43 | cy.xyz(); 44 | cy.wait(${MAX_WAIT_MILLISECONDS + 1}); 45 | cy.wait(${MAX_WAIT_MILLISECONDS + 5000}); 46 | }; 47 | `)); 48 | expect(warning.reportWarning.calledTwice).to.be.true; 49 | expect(warning.reportWarning.getCall(0).args[0]).to.deep.equal(esprima.generateAST(`cy.wait(${MAX_WAIT_MILLISECONDS + 1})`).body[0].expression); 50 | expect(warning.reportWarning.getCall(1).args[0]).to.deep.equal(esprima.generateAST(`cy.wait(${MAX_WAIT_MILLISECONDS + 5000})`).body[0].expression); 51 | }); 52 | 53 | it("Tests with a wait with an identifier instead of a literal", () => { 54 | warning.run(esprima.generateAST(` 55 | const x = () => { 56 | const yz = ${MAX_WAIT_MILLISECONDS + 999}; 57 | cy.doSomething(); 58 | cy.xyz(); 59 | cy.wait(yz); 60 | browser.pause(55); 61 | }; 62 | `)); 63 | expect(warning.reportWarning.notCalled).to.be.true; 64 | }); 65 | 66 | it("Tests with a wait with a string instead of a number", () => { 67 | warning.run(esprima.generateAST(` 68 | const x = () => { 69 | cy.doSomething(); 70 | cy.xyz(); 71 | cy.wait("${MAX_WAIT_MILLISECONDS}"); 72 | browser.pause(55); 73 | }; 74 | `)); 75 | expect(warning.reportWarning.notCalled).to.be.true; 76 | }); 77 | 78 | afterEach(() => { 79 | sandbox.restore(); 80 | }); 81 | }); -------------------------------------------------------------------------------- /packages/cyphfell-test-app/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/cyphfell/index.js: -------------------------------------------------------------------------------- 1 | const dir = require("./src/util/DirectoryUtils"), 2 | fs = require("fs-extra"), 3 | pluginLoader = require("./src/util/PluginsUtil"), 4 | report = require("./src/reports/ReportGenerator"), 5 | frameworks = require("./src/constants/FrameworkConstants"), 6 | converter = require("./src/converters/ActiveConverter"), 7 | exec = require("child_process"), 8 | eslintConfig = require("./src/constants/EslintConstants"), 9 | defaultOptions = { 10 | cypressFolder: "test/cypress/", 11 | baseNormalFolder: "test/", 12 | enableAssertions: false, 13 | glob: "${CWD}/test/!(unit|ui-perf|cypress)/**/*.+(js|jsx|json)", 14 | transpile: false, 15 | validateCypressDir: true, 16 | reportOutputFolder: "cyphfell-output", 17 | replaceModuleImport: (importPath, includeModulesFolder = true) => { 18 | return ""; 19 | }, 20 | transformModuleImportIntoCypress: (originalImport) => { 21 | return originalImport; 22 | }, 23 | disabledPlugins: ["ArgumentSeparation", "TernaryOperator"], 24 | framework: frameworks.WebdriverIO, 25 | eslint: eslintConfig.DISABLED, 26 | moduleResolvePaths: [`${process.cwd()}`, `${process.cwd()}/node_modules`], 27 | moduleAliases: [] 28 | }; 29 | 30 | module.exports = (options = {}, plugins = []) => { 31 | options = Object.assign({}, defaultOptions, options); 32 | options.glob = options.glob.replace("${CWD}", process.cwd()); 33 | if (!options.baseNormalFolder.endsWith("/")) { 34 | options.baseNormalFolder += "/"; 35 | } 36 | if (!options.cypressFolder.endsWith("/")) { 37 | options.cypressFolder += "/"; 38 | } 39 | 40 | converter.init(options.framework); 41 | const parser = require("./src/util/FileParser"); 42 | 43 | plugins = pluginLoader.getActivatedPlugins(plugins, options.disabledPlugins); 44 | 45 | if (options.validateCypressDir) { 46 | dir.verifyCypressDirectory(options.cypressFolder); 47 | } 48 | 49 | global.options = options; 50 | try { 51 | dir.getAllTestFiles(options.glob).forEach((file) => { 52 | console.log(`Starting for: ${file.path}`); 53 | const path = dir.getNewFilePath(file.path, file.contents, options); 54 | report.onFileStart(file.path, path); 55 | const updatedLines = parser(file.contents, file.path, options, plugins); 56 | 57 | fs.mkdirsSync(path.substring(0, path.lastIndexOf("/"))); 58 | fs.writeFileSync(path, updatedLines); 59 | try { 60 | if (options.eslint === eslintConfig.LOCAL) { 61 | exec.execSync(`./node_modules/.bin/eslint --fix ${path}`); 62 | } else if (options.eslint === eslintConfig.GLOBAL) { 63 | exec.execSync(`eslint --fix ${path}`); 64 | } 65 | console.log(`Finished for: ${file.path}`); 66 | } catch (eslintEx) { 67 | console.error(`Could not automatically fix ESlint errors for ${path}`); 68 | } 69 | }); 70 | } catch (ex) { 71 | console.error(ex); 72 | } 73 | 74 | report.generateReport(plugins); 75 | }; -------------------------------------------------------------------------------- /packages/cyphfell/test-automation/cypress/integration/BrowserUtilTests.spec.js: -------------------------------------------------------------------------------- 1 | describe("Tests browser util functions", function() { 2 | 3 | beforeEach(() => { 4 | cy.visit("http://localhost:3000/"); 5 | }); 6 | 7 | it("Tests browser.call()", () => { 8 | const startDate = new Date().getTime(); 9 | const promise = new Promise((resolve) => { 10 | setTimeout(() => { 11 | resolve("success") 12 | }, 3500); 13 | }); 14 | browser.call(() => promise).should((res) => { 15 | expect(new Date().getTime() - startDate).to.be.greaterThan(3500); 16 | expect(res).to.be.equal("success"); 17 | }); 18 | 19 | const promise2 = new Promise((resolve, reject) => { 20 | setTimeout(() => { 21 | reject("failed") 22 | }, 3500); 23 | }); 24 | browser.call(() => promise2).should((res) => { 25 | expect(new Date().getTime() - startDate).to.be.greaterThan(3500); 26 | expect(res).to.be.equal("failed"); 27 | }); 28 | }); 29 | 30 | it("Tests browser.execute()", () => { 31 | browser.execute(() => { 32 | return window.location.origin; 33 | }).should((res) => { 34 | expect(res).to.be.equal("http://localhost:3000"); 35 | }); 36 | }); 37 | 38 | it("Tests browser.addCommand()", () => { 39 | let called = false; 40 | browser.addCommand("commandName", () => { 41 | called = true; 42 | }); 43 | browser.addCommand("command2", () => { 44 | return 35; 45 | }); 46 | 47 | browser.commandName(); 48 | expect(called).to.be.true; 49 | expect(browser.command2()).to.be.equal(35); 50 | }); 51 | 52 | it("Tests browser.setCookie()", () => { 53 | browser.setCookie({name: "testCookieName", value: "testCookieValue"}); 54 | cy.getCookie("testCookieName").should((cookie) => { 55 | expect(cookie.value).to.be.equal("testCookieValue"); 56 | }); 57 | }); 58 | 59 | it("Tests browser.localStorage()", () => { 60 | browser.localStorage("POST", { 61 | key: "lsTest", 62 | value: 2525 63 | }); 64 | browser.localStorage("GET", "lsTest").should((res) => { 65 | expect(res).to.deep.equal({ 66 | key: "lsTest", 67 | value: "2525" 68 | }); 69 | }); 70 | }); 71 | 72 | it("Tests browser.sessionStorage()", () => { 73 | browser.sessionStorage("POST", { 74 | key: "lsTest", 75 | value: 2525 76 | }); 77 | browser.sessionStorage("GET", "lsTest").should((res) => { 78 | expect(res).to.deep.equal({ 79 | key: "lsTest", 80 | value: "2525" 81 | }); 82 | }); 83 | }); 84 | }); -------------------------------------------------------------------------------- /packages/cyphfell/src/plugins/TernaryOperatorPlugin.js: -------------------------------------------------------------------------------- 1 | const BasePlugin = require("../BasePlugin"); 2 | const estraverse = require("estraverse"); 3 | const _ = require("lodash"); 4 | 5 | const replaceChild = (node, childToFind, replaceWith) => { 6 | estraverse.traverse(node, { 7 | enter: function(child, parent) { 8 | if (_.isEqual(child, childToFind)) { 9 | Object.keys(parent).forEach((key) => { 10 | if (parent[key] === child) { 11 | parent[key] = replaceWith; 12 | } 13 | }); 14 | } 15 | } 16 | }); 17 | return node; 18 | }; 19 | 20 | /** 21 | * This plugin transforms ternary operators call expressions with WDIO commands in them into an if/else if format 22 | */ 23 | class TernaryOperatorPlugin extends BasePlugin { 24 | 25 | beforeTransformAfterParsing(ast) { 26 | estraverse.traverse(ast, { 27 | enter: function(node, parent) { 28 | node.parent = parent; 29 | if (node.type === "ObjectExpression") { 30 | this.skip(); 31 | } 32 | if (node.type === "ConditionalExpression") { 33 | let current = node; 34 | let previous = node; 35 | while (current) { 36 | if (current.type === "BlockStatement") { 37 | break; 38 | } 39 | previous = current; 40 | current = current.parent; 41 | } 42 | if (!current) { 43 | this.break(); 44 | return; 45 | } 46 | 47 | if (current.body[current.body.length - 1] !== previous) { 48 | return; 49 | } 50 | 51 | const alternate = node.alternate; 52 | const consequent = node.consequent; 53 | const newAlternate = replaceChild(_.cloneDeep(previous), node, alternate); 54 | const newConsequent = replaceChild(_.cloneDeep(previous), node, consequent); 55 | 56 | if (newAlternate.type === "VariableDeclaration") { 57 | if (newAlternate.declarations[0].init.type !== "CallExpression" || newConsequent.declarations[0].init.type !== "CallExpression") { 58 | return; 59 | } 60 | } else if (newAlternate.type === "ReturnStatement") { 61 | if (newAlternate.argument.type !== "CallExpression" || newConsequent.argument.type !== "CallExpression") { 62 | return; 63 | } 64 | } 65 | // TODO: promise chains only 66 | /*const str = esprima.generateCodeFromAST(ast); 67 | if (!promiseChain(str, "")(str, ) && !promiseChain(str, ""))*/ 68 | 69 | Object.assign(previous, { 70 | type: "IfStatement", 71 | alternate: { 72 | type: "BlockStatement", 73 | body: [newAlternate] 74 | }, 75 | consequent: { 76 | type: "BlockStatement", 77 | body: [newConsequent] 78 | }, 79 | test: node.test 80 | }); 81 | } 82 | } 83 | }); 84 | estraverse.traverse(ast, { 85 | enter: function(node) { 86 | delete node.parent; 87 | } 88 | }); 89 | } 90 | 91 | getName() { 92 | return "TernaryOperator"; 93 | } 94 | } 95 | 96 | module.exports = TernaryOperatorPlugin; -------------------------------------------------------------------------------- /packages/cyphfell/test/unit/plugins/ArgumentSeparationTests.js: -------------------------------------------------------------------------------- 1 | const esprima = require("../../../src/util/EsprimaUtils"); 2 | let plugin = require("../../../src/plugins/ArgumentSeparationPlugin"); 3 | plugin = new plugin(); 4 | 5 | const assertEqual = (ast, expectedString) => { 6 | const newAST = esprima.generateAST(esprima.generateCodeFromAST(ast)); 7 | expect(newAST).to.deep.equal(esprima.generateAST(expectedString)); 8 | }; 9 | 10 | describe("Tests ArgumentSeparation plugin", function() { 11 | 12 | before(() => { 13 | global.options = { 14 | transpile: false 15 | }; 16 | }); 17 | 18 | it("Tests single argument", () => { 19 | const ast = esprima.generateAST(` 20 | const x = () => { 21 | another.func(cy.get()); 22 | }; 23 | `); 24 | plugin.beforeParseLines(ast); 25 | assertEqual(ast, `const x = () => { 26 | const argget1 = cy.get(); 27 | another.func(argget1); 28 | };`); 29 | }); 30 | 31 | it("Tests multiple arguments", () => { 32 | const ast = esprima.generateAST(` 33 | const x = () => { 34 | another.func(cy.get(), cy.get("SOMETHING")); 35 | }; 36 | `); 37 | plugin.beforeParseLines(ast); 38 | assertEqual(ast, `const x = () => { 39 | const argget1 = cy.get(); 40 | const argget2 = cy.get("SOMETHING"); 41 | another.func(argget1, argget2); 42 | };`); 43 | }); 44 | 45 | it("Tests not being the first node in the body", () => { 46 | const ast = esprima.generateAST(` 47 | const x = () => { 48 | doNothing.nothingHere(); 49 | another.func(cy.get(), cy.get("SOMETHING")); 50 | }; 51 | `); 52 | plugin.beforeParseLines(ast); 53 | assertEqual(ast, `const x = () => { 54 | doNothing.nothingHere(); 55 | const argget1 = cy.get(); 56 | const argget2 = cy.get("SOMETHING"); 57 | another.func(argget1, argget2); 58 | };`); 59 | }); 60 | 61 | it("Tests multiple separations in same block", () => { 62 | const ast = esprima.generateAST(` 63 | const x = () => { 64 | const xyz = 25; 65 | doNothing.funcName(cy.getText(".val")); 66 | another.func(cy.get(), cy.get("SOMETHING")); 67 | }; 68 | `); 69 | plugin.beforeParseLines(ast); 70 | assertEqual(ast, `const x = () => { 71 | const xyz = 25; 72 | const arggetText1 = cy.getText(".val"); 73 | doNothing.funcName(arggetText1); 74 | const argget1 = cy.get(); 75 | const argget2 = cy.get("SOMETHING"); 76 | another.func(argget1, argget2); 77 | };`); 78 | }); 79 | 80 | it("Tests ArgumentSeparation plugin name", () => { 81 | expect(plugin.getName()).to.be.equal("ArgumentSeparation"); 82 | }); 83 | }); -------------------------------------------------------------------------------- /packages/cyphfell/test/e2e/noImports/FixErrorTests.js: -------------------------------------------------------------------------------- 1 | const fp = require("../../../src/util/FileParser"); 2 | const plugins = require("../../../src/util/PluginsUtil"); 3 | const esprima = require("../../../src/util/EsprimaUtils"); 4 | 5 | describe("Tests fixing errors", function() { 6 | 7 | let pluginsList = null; 8 | before(() => { 9 | global.options = { 10 | cypressFolder: "test/cypress/", 11 | baseNormalFolder: "test/", 12 | moduleResolvePaths: [] 13 | }; 14 | pluginsList = plugins.loadPlugins("./src/plugins/*"); 15 | }); 16 | 17 | it("Tests a parameter not being passed in correctly to functions", () => { 18 | const res = fp("export default class TestPage {\n" + 19 | " static login(user, password) {\n" + 20 | " if (config.log) {\n" + 21 | " console.log(`Logging in as ${user}`);\n" + 22 | " }\n" + 23 | "\n" + 24 | " global.test.username = user;\n" + 25 | " global.test.password = password;\n" + 26 | "\n" + 27 | " // Exit ....\n" + 28 | " browser.pause(5000);\n" + 29 | "\n" + 30 | " this.userNameField().setValue(user);\n" + 31 | " this.passwordField().waitForVisible();\n" + 32 | " this.passwordField().setValue(password);\n" + 33 | " this.loginButton().click();\n" + 34 | " }\n" + 35 | "\n" + 36 | " static userNameField() {\n" + 37 | " return browser.element(\"#s\");\n" + 38 | " }\n" + 39 | "\n" + 40 | " static passwordField(sectionID=\"in\") {\n" + 41 | " return browser.element(`#${sectionID} d`);\n" + 42 | " }\n" + 43 | "\n" + 44 | "}\n", `${process.cwd()}/test/test.js`, global.options, pluginsList); 45 | expect(esprima.generateAST(res)).to.deep.equal(esprima.generateAST("export default class TestPage {\n" + 46 | " static login(user, password) {\n" + 47 | " if (config.log) {\n" + 48 | " console.log(`Logging in as ${ user }`);\n" + 49 | " }\n" + 50 | " global.test.username = user;\n" + 51 | " global.test.password = password;\n" + 52 | " cy.wait(5000);\n" + 53 | " this.userNameField().setValue(user);\n" + 54 | " this.passwordField().waitForVisible();\n" + 55 | " this.passwordField().setValue(password);\n" + 56 | " this.loginButton().click();\n" + 57 | " }\n" + 58 | " static userNameField() {\n" + 59 | " return cy.get(\"#s\");\n" + 60 | " }\n" + 61 | " static passwordField(sectionID = \"in\") {\n" + 62 | " return cy.get(`#${ sectionID } d`);\n" + 63 | " }\n" + 64 | "}")) 65 | }); 66 | }); -------------------------------------------------------------------------------- /packages/cyphfell-test-app/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import './App.css'; 3 | 4 | class App extends Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | this.state = { 9 | submitted: false, 10 | disabledTest: true 11 | }; 12 | 13 | setTimeout(() => { 14 | this.setState({ 15 | disabledTest: false 16 | }); 17 | }, 5000); 18 | console.error("constructor error"); 19 | } 20 | 21 | onSubmit = (event) => { 22 | event.preventDefault(); 23 | this.setState({ 24 | submitted: true 25 | }); 26 | }; 27 | 28 | render() { 29 | return ( 30 |
31 | {this.state.submitted &&

The form was successfully submitted!

} 32 |
33 | 37 |
38 | 42 |
43 |
44 | 52 |
53 |
54 | 58 |
59 |
60 | 65 |
66 | 67 |
68 | 69 | 70 | TestSpan 71 |
TestDivText
72 |