├── src ├── stylesheets │ ├── components │ │ ├── _reuploaddirectory.scss │ │ ├── _filetree.scss │ │ ├── _itstatement.scss │ │ ├── _all.scss │ │ ├── _testbuilder.scss │ │ ├── _expectstatement.scss │ │ ├── _maincontent.scss │ │ ├── _describeblock.scss │ │ ├── _fileviewer.scss │ │ ├── _proploader.scss │ │ ├── _testblock.scss │ │ └── _folderupload.scss │ └── styles.scss ├── setupEnzyme.ts ├── reduxComponents │ ├── store.ts │ ├── reducers │ │ ├── combineReducers.ts │ │ └── reducer.ts │ ├── constants │ │ └── actionTypes.ts │ └── actions │ │ └── actions.ts ├── typings │ └── declarations.d.ts ├── index.tsx ├── app.tsx ├── index.html ├── components │ ├── MainContent.tsx │ ├── CodeViewer.tsx │ ├── ProjectTestCode.tsx │ ├── FileViewer.tsx │ ├── ReuploadDirectory.tsx │ ├── FileTree.tsx │ ├── PropLoader.tsx │ ├── ExportTestCode.tsx │ ├── DescribeBlock.tsx │ ├── ItStatement.tsx │ ├── FolderUpload.tsx │ ├── TestBuilder.tsx │ ├── TestBlock.tsx │ └── ExpectStatement.tsx ├── icons │ └── icons.tsx └── utils │ └── globalCodeGenerator.ts ├── .vscode └── settings.json ├── .DS_Store ├── .prettierrc.js ├── assets ├── .DS_Store ├── icon.png ├── icons │ ├── .DS_Store │ ├── default_file.svg │ ├── default_folder.svg │ ├── file_type_eslint.svg │ ├── file_type_js.svg │ ├── file_type_config.svg │ ├── file_type_light_config.svg │ ├── file_type_html.svg │ ├── file_type_typescript.svg │ ├── file_type_css.svg │ ├── file_type_scss.svg │ ├── file_type_json.svg │ ├── file_type_reactjs.svg │ ├── file_type_reactts.svg │ ├── file_type_reacttemplate.svg │ └── file_type_babel2.svg ├── CatalystInstallBG-05.tif └── catalyst_icons │ ├── Catalyst-01.png │ ├── Catalystfull-02.png │ ├── CatalystDockIcon-03.png │ └── CatalystDockIconLarge-04.png ├── prettierrc.js ├── __tests__ ├── .DS_Store ├── __snapshots__ │ └── app.test.tsx.snap └── app.test.tsx ├── .gitignore ├── .babelrc ├── jest.config.json ├── tsconfig.json ├── webpack.electron.config.js ├── .eslintrc.js ├── LICENSE ├── webpack.react.config.js ├── electron └── main.ts ├── README.md └── package.json /src/stylesheets/components/_reuploaddirectory.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2 3 | } -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Catalyst/HEAD/.DS_Store -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | printWidth: 80, 4 | }; -------------------------------------------------------------------------------- /assets/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Catalyst/HEAD/assets/.DS_Store -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Catalyst/HEAD/assets/icon.png -------------------------------------------------------------------------------- /prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | printWidth: 80, 4 | }; -------------------------------------------------------------------------------- /__tests__/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Catalyst/HEAD/__tests__/.DS_Store -------------------------------------------------------------------------------- /assets/icons/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Catalyst/HEAD/assets/icons/.DS_Store -------------------------------------------------------------------------------- /assets/CatalystInstallBG-05.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Catalyst/HEAD/assets/CatalystInstallBG-05.tif -------------------------------------------------------------------------------- /assets/catalyst_icons/Catalyst-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Catalyst/HEAD/assets/catalyst_icons/Catalyst-01.png -------------------------------------------------------------------------------- /assets/catalyst_icons/Catalystfull-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Catalyst/HEAD/assets/catalyst_icons/Catalystfull-02.png -------------------------------------------------------------------------------- /assets/catalyst_icons/CatalystDockIcon-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Catalyst/HEAD/assets/catalyst_icons/CatalystDockIcon-03.png -------------------------------------------------------------------------------- /assets/catalyst_icons/CatalystDockIconLarge-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Catalyst/HEAD/assets/catalyst_icons/CatalystDockIconLarge-04.png -------------------------------------------------------------------------------- /assets/icons/default_file.svg: -------------------------------------------------------------------------------- 1 | default_file -------------------------------------------------------------------------------- /assets/icons/default_folder.svg: -------------------------------------------------------------------------------- 1 | default_folder -------------------------------------------------------------------------------- /src/setupEnzyme.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import * as Enzyme from "enzyme"; 3 | import Adapter from "@wojtekmaj/enzyme-adapter-react-17"; 4 | 5 | Enzyme.configure({ 6 | adapter: new Adapter(), 7 | }); 8 | -------------------------------------------------------------------------------- /src/reduxComponents/store.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import { composeWithDevTools } from "redux-devtools-extension"; 3 | import { reducer } from "./reducers/reducer"; 4 | 5 | export const store = createStore(reducer, composeWithDevTools()); 6 | -------------------------------------------------------------------------------- /__tests__/__snapshots__/app.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Testing app component matches snapshot 1`] = ` 4 |
5 |

6 | Hello from React World 7 |

8 | 9 |
10 | `; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore node_modules folder 2 | node_modules 3 | 4 | # Ignore module bundles folder 5 | dist 6 | 7 | # Ignore package-lock.json 8 | package-lock.json 9 | 10 | # Ignore files related to API keys 11 | .env 12 | 13 | # Ignore Mac system files 14 | .DS_Store -------------------------------------------------------------------------------- /src/stylesheets/styles.scss: -------------------------------------------------------------------------------- 1 | @import 'components/all'; 2 | 3 | $primary: #1c768f; 4 | $secondary: #032539; 5 | $fg: #FA991C; 6 | $bg: #FBF3F2; 7 | 8 | 9 | * { 10 | box-sizing: border-box; 11 | font-family: 'Nunito Sans', sans-serif; 12 | } 13 | 14 | button, select { 15 | outline: none; 16 | } 17 | -------------------------------------------------------------------------------- /src/reduxComponents/reducers/combineReducers.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import { reducer } from "./reducer"; 3 | 4 | export const rootReducer = combineReducers({ 5 | generalReducer: reducer, 6 | }); 7 | 8 | // giving type of reducer globally 9 | export type RootState = ReturnType; 10 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/typescript", 5 | "@babel/react" 6 | ], 7 | "plugins": [ 8 | "@babel/proposal-class-properties", 9 | "@babel/proposal-object-rest-spread", 10 | ["@babel/plugin-transform-runtime", 11 | { 12 | "regenerator": true 13 | }] 14 | ] 15 | } -------------------------------------------------------------------------------- /src/typings/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg" { 2 | const content: any; 3 | export default content; 4 | } 5 | 6 | declare module "*.css" { 7 | const css: any; 8 | export default css; 9 | } 10 | 11 | declare module "*.scss" {} 12 | 13 | declare module "*.png" { 14 | const png: any; 15 | export default png; 16 | } 17 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDom from "react-dom"; 3 | import { Provider } from "react-redux"; 4 | import { store } from "./reduxComponents/store"; 5 | import { App } from "./app"; 6 | 7 | ReactDom.render( 8 | 9 | 10 | , 11 | document.getElementById("root") 12 | ); 13 | -------------------------------------------------------------------------------- /jest.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": [ 3 | "ts", 4 | "tsx", 5 | "js" 6 | ], 7 | "transform": { 8 | "\\.(ts|tsx)$": "ts-jest" 9 | }, 10 | "testRegex": "/__tests__/.*\\.(ts|tsx|js)$", 11 | "setupFilesAfterEnv": [ 12 | "./src/setupEnzyme.ts" 13 | ], 14 | "snapshotSerializers": [ 15 | "enzyme-to-json/serializer" 16 | ] 17 | } -------------------------------------------------------------------------------- /src/stylesheets/components/_filetree.scss: -------------------------------------------------------------------------------- 1 | $primary: #032539; 2 | $secondary: #1c768f; 3 | $fg: #FA991C; 4 | $bg: #FBF3F2; 5 | 6 | .FileTree li a:hover{ 7 | cursor: pointer; 8 | text-decoration: underline; 9 | } 10 | 11 | .directory:hover { 12 | cursor: pointer; 13 | } 14 | 15 | .FileTree li { 16 | list-style-type: none; 17 | } 18 | 19 | .FileTree span img { 20 | width: 1rem; 21 | height: 1rem; 22 | margin-right:0.25rem; 23 | } -------------------------------------------------------------------------------- /src/stylesheets/components/_itstatement.scss: -------------------------------------------------------------------------------- 1 | $primary: #032539; 2 | $secondary: #1c768f; 3 | $fg: #FA991C; 4 | $bg: #FBF3F2; 5 | 6 | .itBlock { 7 | margin: 1em 1em; 8 | .itinput { 9 | width: 85%; 10 | border: .5px solid $primary; 11 | border-radius: .5em; 12 | padding: .5em; 13 | } 14 | .removeit { 15 | background: $fg; 16 | font-size: 12px; 17 | padding: .25em .60em; 18 | margin-left: 1em; 19 | } 20 | } -------------------------------------------------------------------------------- /src/app.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { MainContent } from "./components/MainContent"; 4 | import { FolderUpload } from "./components/FolderUpload"; 5 | 6 | import "./stylesheets/styles"; 7 | 8 | export const App: React.FC = () => { 9 | const fileTree = useSelector((state: any) => state.fileTree); 10 | 11 | return fileTree.length ? ( 12 | 13 | ) : ( 14 |
15 | 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/stylesheets/components/_all.scss: -------------------------------------------------------------------------------- 1 | @import 'describeblock'; 2 | @import 'filetree'; 3 | @import 'folderupload'; 4 | @import 'testbuilder'; 5 | @import 'testblock'; 6 | @import 'fileviewer'; 7 | @import 'maincontent'; 8 | @import 'itstatement'; 9 | @import 'expectstatement'; 10 | @import 'reuploaddirectory'; 11 | @import 'proploader'; 12 | 13 | 14 | $primary: #032539; 15 | $secondary: #1c768f; 16 | $fg: #FA991C; 17 | $bg: #FBF3F2; 18 | 19 | body { 20 | margin: 0; 21 | padding: 0; 22 | height:100vh; 23 | width: 100vw; 24 | } 25 | -------------------------------------------------------------------------------- /assets/icons/file_type_eslint.svg: -------------------------------------------------------------------------------- 1 | file_type_eslint -------------------------------------------------------------------------------- /src/stylesheets/components/_testbuilder.scss: -------------------------------------------------------------------------------- 1 | $primary: #032539; 2 | $secondary: #1c768f; 3 | $fg: #FA991C; 4 | $bg: #FBF3F2; 5 | 6 | .testBuilder { 7 | border: 1px solid $primary; 8 | background: $primary; 9 | border-radius: 3em; 10 | box-shadow: 2px 2px 12px $primary; 11 | .addtestbutton { 12 | display:flex; 13 | justify-content: center; 14 | align-items: center; 15 | padding-bottom: 1em; 16 | .adddescribe { 17 | color: $primary; 18 | font-size: 20px; 19 | border: none; 20 | border-radius: 1em; 21 | padding: .25em .5em; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /__tests__/app.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow, mount, render } from 'enzyme'; 2 | import * as React from 'react'; 3 | import { App } from '../src/app'; 4 | import { FolderUpload } from '../src/components/FolderUpload' 5 | 6 | const wrapper = shallow(); 7 | 8 | describe('Testing app component', () => { 9 | it('renders', () => { 10 | expect(wrapper.exists()).toBe(true); 11 | }) 12 | it('matches snapshot', () => { 13 | expect(wrapper).toMatchSnapshot(); 14 | }) 15 | it('render FolderUpload component', () => { 16 | expect(wrapper.children(FolderUpload).length).toEqual(1); 17 | }) 18 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esNext", 4 | "module": "commonjs", 5 | "lib": [ 6 | "dom", 7 | "es2015", 8 | "es2016", 9 | "es2017", 10 | "dom.iterable" 11 | ], 12 | "allowJs": true, 13 | "jsx": "react", 14 | "allowSyntheticDefaultImports": true, 15 | "sourceMap": true, 16 | "outDir": "./dist", 17 | "strict": true, 18 | "esModuleInterop": true, 19 | "typeRoots": ["node_modules/@types", "typings"] 20 | }, 21 | "exclude": [ 22 | "node_modules", 23 | "dist" 24 | ], 25 | "include": ["./src/"] 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Catalyst 11 | 12 | 13 |
14 | 15 | -------------------------------------------------------------------------------- /assets/icons/file_type_js.svg: -------------------------------------------------------------------------------- 1 | file_type_js -------------------------------------------------------------------------------- /webpack.electron.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | resolve: { 5 | extensions: ['.tsx', '.ts', '.js', '.scss'], 6 | }, 7 | devtool: 'source-map', 8 | entry: './electron/main.ts', 9 | target: 'electron-main', 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(js|ts|tsx)$/, 14 | exclude: /node_modules/, 15 | use: { 16 | loader: 'babel-loader', 17 | }, 18 | }, 19 | { 20 | test: /\.s[ac]ss$/i, 21 | use: ["style-loader", "css-loader", "sass-loader"] 22 | }, 23 | { 24 | test: /\.(png|jpe?g|gif)$/i, 25 | use: [ 26 | { 27 | loader: 'file-loader', 28 | }, 29 | ], 30 | } 31 | ], 32 | }, 33 | output: { 34 | path: path.resolve(__dirname, './dist'), 35 | filename: 'main.js', 36 | }, 37 | }; -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["airbnb-typescript-prettier"], 3 | rules: { 4 | 'react/prop-types': 0, 5 | 'react/destructuring-assignment': 0, 6 | 'react/static-property-placement': 0, 7 | 'jsx-a11y/alt-text': 0, 8 | 'react/jsx-props-no-spreading': 0, 9 | 'import/prefer-default-export': 0, 10 | 'no-nested-ternary': 0, 11 | 'react-hooks/exhaustive-deps': 0, 12 | 'no-restricted-syntax': 0, 13 | 'jsx-a11y/no-static-element-interactions': 0, 14 | 'jsx-a11y/click-events-have-key-events': 0, 15 | 'jsx-a11y/anchor-is-valid': 0, 16 | 'no-plusplus': 0, 17 | 'class-methods-use-this': 0, 18 | 'react/button-has-type': 0, 19 | '@typescript-eslint/no-use-before-define': 0, 20 | '@typescript-eslint/no-unused-vars': 0, 21 | '@typescript-eslint/explicit-module-boundary-types': 0, 22 | 'no-case-declarations': 0, 23 | }, 24 | }; -------------------------------------------------------------------------------- /src/components/MainContent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FileTree } from "./FileTree"; 3 | import { CodeViewer } from "./CodeViewer"; 4 | import { TestBuilder } from "./TestBuilder"; 5 | import { TestBlock } from "./TestBlock"; 6 | 7 | export const MainContent: React.FC = () => { 8 | return ( 9 |
10 |
11 | 12 |
13 |
14 |
15 |

PROJECT DIRECTORY

16 | 17 |
18 |
19 |

TEST BUILDER

20 | 21 |
22 |
23 |

CODE VIEWER

24 | 25 |
26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /assets/icons/file_type_config.svg: -------------------------------------------------------------------------------- 1 | file_type_config -------------------------------------------------------------------------------- /assets/icons/file_type_light_config.svg: -------------------------------------------------------------------------------- 1 | file_type_light_config -------------------------------------------------------------------------------- /src/components/CodeViewer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector } from "react-redux"; 3 | import { ProjectTestCode } from "./ProjectTestCode"; 4 | import { FileViewer } from "./FileViewer"; 5 | 6 | export const CodeViewer: React.FC = () => { 7 | const codeViewer = useSelector((state: any) => state.codeViewer); 8 | const codeViewerChecker = useSelector( 9 | (state: any) => state.codeViewerChecker 10 | ); 11 | // using fragment (<>) shorthand over React.Fragment 12 | return codeViewer === false ? ( 13 | <> 14 |
15 |

No File Displayed

16 |

17 | Click on Generate Tests or a file in the project directory to view its 18 | code 19 |

20 |
21 | 22 | ) : codeViewerChecker ? ( 23 | 24 | ) : ( 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /assets/icons/file_type_html.svg: -------------------------------------------------------------------------------- 1 | file_type_html -------------------------------------------------------------------------------- /assets/icons/file_type_typescript.svg: -------------------------------------------------------------------------------- 1 | file_type_typescript -------------------------------------------------------------------------------- /src/stylesheets/components/_expectstatement.scss: -------------------------------------------------------------------------------- 1 | $primary: #032539; 2 | $secondary: #1c768f; 3 | $fg: #FA991C; 4 | $bg: #FBF3F2; 5 | 6 | .expectBlock { 7 | margin: .5em 0em; 8 | padding: .5em; 9 | border: .5px solid $fg; 10 | background: $bg; 11 | border-radius: .5em; 12 | .expect1 { 13 | } 14 | .expect2 { 15 | margin-top: .5em; 16 | } 17 | .expectdrop1 { 18 | margin-left: 1em; 19 | padding: .25em .5em; 20 | border: .5px solid $secondary; 21 | border-radius: 1em; 22 | } 23 | .expectdrop2 { 24 | margin-right: 1em; 25 | padding: .25em .5em; 26 | border: .5px solid $secondary; 27 | border-radius: 1em; 28 | } 29 | input { 30 | font-size: 14px; 31 | border: .5px solid $secondary; 32 | border-radius: .5em; 33 | padding: .25em .5em; 34 | width: 40%; 35 | min-width: 5em; 36 | } 37 | .removeexpect { 38 | float: right; 39 | font-size: 12px; 40 | justify-content: center !important; 41 | background: $fg; 42 | margin: 0em auto; 43 | padding: .2em .55em; 44 | } 45 | } -------------------------------------------------------------------------------- /src/stylesheets/components/_maincontent.scss: -------------------------------------------------------------------------------- 1 | $primary: #032539; 2 | $secondary: #1c768f; 3 | $fg: #FA991C; 4 | $bg: #FBF3F2; 5 | 6 | .main-content{ 7 | height:100%; 8 | } 9 | 10 | .row { 11 | display: grid; 12 | grid-template-columns: 325px repeat(2, 1fr); 13 | grid-template-rows: 100%; 14 | width:100%; 15 | height: 90%; 16 | background: $bg; 17 | color: $primary; 18 | width: 100%; 19 | .left { 20 | overflow: auto; 21 | height:100%; 22 | border-right: 1px solid $secondary; 23 | h1 { 24 | text-align: center; 25 | font-size: 24px; 26 | } 27 | } 28 | .middle { 29 | overflow: auto; 30 | height:100%; 31 | border-left: 1px solid $secondary; 32 | border-right: 1px solid $secondary; 33 | padding: 0em 1.5em; 34 | min-width: 30em; 35 | h1 { 36 | text-align: center; 37 | font-size: 24px; 38 | 39 | } 40 | } 41 | .right { 42 | overflow: auto; 43 | height:100%; 44 | border-left: 1px solid $secondary; 45 | h1 { 46 | text-align: center; 47 | font-size: 24px; 48 | 49 | } 50 | } 51 | } 52 | 53 | 54 | -------------------------------------------------------------------------------- /assets/icons/file_type_css.svg: -------------------------------------------------------------------------------- 1 | file_type_css -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Catalyst 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/stylesheets/components/_describeblock.scss: -------------------------------------------------------------------------------- 1 | $primary: #032539; 2 | $secondary: #1c768f; 3 | $fg: #FA991C; 4 | $bg: #FBF3F2; 5 | 6 | .describeBlock { 7 | // display: block; 8 | background: white; 9 | border-radius: 2em; 10 | padding: 1em 1em 1em 2em; 11 | margin: 1em; 12 | // box-shadow: 4px 4px 12px grey; 13 | button { 14 | font-size: 16px; 15 | border: none; 16 | border-radius: 1em; 17 | padding: .5em 1em; 18 | background: $secondary; 19 | color: $bg; 20 | } 21 | .remove { 22 | background: $fg; 23 | float: right; 24 | border-radius: 1.5em; 25 | padding: .10em .45em; 26 | } 27 | .describe { 28 | margin-bottom: 1em; 29 | .describetext { 30 | font-size: 1.5em; 31 | } 32 | .describeinput { 33 | border: .5px solid $primary; 34 | border-radius: .5em; 35 | padding: .5em; 36 | width: 95%; 37 | border-radius: 4px; 38 | outline: none; 39 | } 40 | form { 41 | margin-top: .1em; 42 | display:flex; 43 | align-items: center; 44 | padding-left: 0; 45 | input { 46 | margin-left: 0; 47 | } 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/stylesheets/components/_fileviewer.scss: -------------------------------------------------------------------------------- 1 | $primary: #032539; 2 | $secondary: #1c768f; 3 | $fg: #FA991C; 4 | $bg: #FBF3F2; 5 | 6 | div p { 7 | margin: 0; 8 | } 9 | 10 | 11 | .codeLine{ 12 | display: flex; 13 | flex-direction: row; 14 | 15 | } 16 | 17 | .codeLine pre{ 18 | margin: 0 19 | } 20 | 21 | .emptyfileview { 22 | color: $fg; 23 | border: none; 24 | width: 20em; 25 | margin: 1em; 26 | display: block; 27 | margin: 10em auto; 28 | padding-bottom: 1.5em; 29 | padding-top: 1.5em; 30 | justify-content: center; 31 | text-align: center; 32 | .emptyfiletitle { 33 | margin-top: 0; 34 | display: block; 35 | font-size: 30px; 36 | font-weight: 600; 37 | } 38 | .emptyfilebody { 39 | font-weight: 200; 40 | font-size: 24px; 41 | } 42 | } 43 | 44 | .codeBlock{ 45 | padding-left: 5%; 46 | height:100%; 47 | .buttonHolder{ 48 | width:100%; 49 | height:5%; 50 | } 51 | } 52 | 53 | .clearFile{ 54 | display: block; 55 | float: right; 56 | } 57 | 58 | .remove { 59 | color: $bg; 60 | background: $fg; 61 | float: right; 62 | border-radius: 1.5em; 63 | border: none; 64 | padding: .10em .45em; 65 | margin-right: 1em; 66 | } 67 | 68 | -------------------------------------------------------------------------------- /src/reduxComponents/constants/actionTypes.ts: -------------------------------------------------------------------------------- 1 | export const UPDATE_DATA = "UPDATE_DATA"; 2 | export const SET_FILE_VIEW = "SET_FILE_VIEW"; 3 | export const CONSTRUCT_FILETREE = "CONSTRUCT_FILETREE"; 4 | export const TOGGLE_FOLDER = "TOGGLE_FOLDER"; 5 | export const UPDATE_KEY_OF_EXPECT = "UPDATE_KEY_OF_EXPECT"; 6 | export const ADD_IT_STATEMENTS = "ADD_IT_STATEMENTS"; 7 | export const UPDATE_KEY_OF_DESCRIBE = "UPDATE_KEY_OF_DESCRIBE"; 8 | export const UPDATE_KEY_OF_IT = "UPDATE_KEY_OF_IT"; 9 | export const UPDATE_IT_OBJ = "UPDATE_IT_OBJ"; 10 | export const UPDATE_DESCRIBE = "UPDATE_DESCRIBE"; 11 | export const UPDATE_COMPONENT_NAME = "UPDATE_COMPONENT_NAME"; 12 | export const UPDATE_IT_STATEMENT = "UPDATE_IT_STATEMENT"; 13 | export const ADD_COMPONENT_NAME = "ADD_COMPONENT_NAME"; 14 | export const CLEAR_FILE = "CLEAR_FILE"; 15 | export const DELETE_EXPECT = "DELETE_EXPECT"; 16 | export const REMOVE_FROM_IT = "REMOVE_FROM_IT"; 17 | export const SET_PROJECT_PATH = "SET_PROJECT_PATH"; 18 | export const REFRESH_STATE = "REFRESH_STATE"; 19 | export const UPDATE_DESCRIBE_BOOLEAN = "UPDATE_DESCRIBE_BOOLEAN"; 20 | export const UPDATE_DESCRIBE_PROPS = "UPDATE_DESCRIBE_PROPS"; 21 | export const SHOW_TESTCODE = "SHOW_TESTCODE"; 22 | export const REMOVE_TESTCODE = "REMOVE_TESTCODE"; 23 | -------------------------------------------------------------------------------- /assets/icons/file_type_scss.svg: -------------------------------------------------------------------------------- 1 | file_type_scss -------------------------------------------------------------------------------- /webpack.react.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | resolve: { 6 | extensions: ['.tsx', '.ts', '.js', '.scss', '.png', '.svg'], 7 | mainFields: ['main', 'module', 'browser'], 8 | }, 9 | entry: './src/index.tsx', 10 | target: 'electron-renderer', 11 | devtool: 'source-map', 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.(js|ts|tsx)$/, 16 | exclude: /node_modules/, 17 | use: { 18 | loader: 'babel-loader', 19 | }, 20 | }, 21 | { 22 | test: /\.svg$/, 23 | use: ['url-loader'] 24 | }, 25 | { 26 | test: /\.s[ac]ss$/i, 27 | use: ["style-loader", "css-loader", "sass-loader"] 28 | }, 29 | { 30 | test: /\.(png|jpe?g|gif)$/i, 31 | use: [ 32 | { 33 | loader: 'file-loader', 34 | }, 35 | ], 36 | } 37 | ], 38 | }, 39 | devServer: { 40 | contentBase: path.join(__dirname, './dist/renderer'), 41 | historyApiFallback: true, 42 | compress: true, 43 | hot: true, 44 | port: 4000, 45 | publicPath: '/', 46 | }, 47 | output: { 48 | path: path.resolve(__dirname, './dist/renderer'), 49 | filename: 'js/index.js', 50 | publicPath: './', 51 | }, 52 | plugins: [ 53 | new HtmlWebpackPlugin({ 54 | template: './src/index.html' 55 | }) 56 | ], 57 | }; -------------------------------------------------------------------------------- /assets/icons/file_type_json.svg: -------------------------------------------------------------------------------- 1 | file_type_json -------------------------------------------------------------------------------- /src/icons/icons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | // import logos from assests file here 4 | 5 | // tsx and jsx logo 6 | import tsxLogo from "../../assets/icons/file_type_reactts.svg"; 7 | 8 | // js logo 9 | import jsLogo from "../../assets/icons/file_type_js.svg"; 10 | 11 | // html logo 12 | import htmlLogo from "../../assets/icons/file_type_html.svg"; 13 | 14 | // css logo 15 | import cssLogo from "../../assets/icons/file_type_css.svg"; 16 | 17 | // ts logo 18 | import tsLogo from "../../assets/icons/file_type_typescript.svg"; 19 | 20 | // scss logo 21 | import scssLogo from "../../assets/icons/file_type_scss.svg"; 22 | 23 | // config logo 24 | import configLogo from "../../assets/icons/file_type_config.svg"; 25 | 26 | // folder icon 27 | import folderIcon from "../../assets/icons/default_folder.svg"; 28 | 29 | // json icon 30 | import jsonIcon from "../../assets/icons/file_type_json.svg"; 31 | 32 | // babel icon 33 | import babelIcon from "../../assets/icons/file_type_babel2.svg"; 34 | 35 | // eslint icon 36 | import eslintIcon from "../../assets/icons/file_type_eslint.svg"; 37 | 38 | const FILE_ICONS: { [k: string]: JSX.Element } = { 39 | tsx: , 40 | jsx: , 41 | js: , 42 | html: , 43 | css: , 44 | ts: , 45 | scss: , 46 | config: , 47 | env: , 48 | folder: , 49 | json: , 50 | babel: , 51 | eslint: , 52 | }; 53 | 54 | export default FILE_ICONS; 55 | -------------------------------------------------------------------------------- /src/stylesheets/components/_proploader.scss: -------------------------------------------------------------------------------- 1 | $primary: #032539; 2 | $secondary: #1c768f; 3 | $fg: #FA991C; 4 | $bg: #FBF3F2; 5 | 6 | .Prop { 7 | margin: .5em 0em; 8 | p { 9 | font-size: 20px; 10 | color: $primary; 11 | } 12 | .propform { 13 | margin-bottom: .5em; 14 | } 15 | .propcheck { 16 | height: 15px; 17 | width: 15px; 18 | border-radius: 4px; 19 | border: 1px solid $secondary; 20 | background: transparent; 21 | -webkit-appearance: none; 22 | -moz-appearance: none; 23 | -o-appearance: none; 24 | appearance: none; 25 | outline: none; 26 | display:inline-block; 27 | } 28 | .propcheck:checked { 29 | background: $secondary; 30 | } 31 | .propcheck:checked::before { 32 | height: 10px; 33 | width: 10px; 34 | content: '\2713'; 35 | color: $bg; 36 | text-align: center; 37 | position: relative; 38 | top: -.1em; 39 | left: .1em; 40 | } 41 | .proplabel { 42 | display: inline; 43 | margin-right: .5em; 44 | } 45 | .addvalue { 46 | font-size: 16px; 47 | line-height: 1em !important; 48 | border-radius: .8em !important; 49 | margin-left: 1em; 50 | } 51 | .propChild { 52 | display: flex; 53 | margin: .5em 0em; 54 | align-items: center; 55 | .propval { 56 | width: 40%; 57 | } 58 | .proplabel { 59 | font-size: 18px; 60 | margin-right: .25em; 61 | margin-top: auto; 62 | margin-bottom: auto; 63 | } 64 | .removeprop { 65 | font-size: 14px !important; 66 | justify-content: center !important; 67 | background: $fg !important; 68 | padding: .175em .55em !important; 69 | } 70 | input { 71 | font-size: 14px; 72 | border: .5px solid $secondary; 73 | border-radius: .5em; 74 | padding: .25em .5em; 75 | margin-right: .5em; 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/components/ProjectTestCode.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { RemoveTestCode } from "../reduxComponents/actions/actions"; 4 | 5 | export const ProjectTestCode: React.FC = () => { 6 | const dispatch = useDispatch(); 7 | const generatedTestCode = useSelector( 8 | (state: any) => state.generatedTestCode 9 | ); 10 | const removeTestCode = () => dispatch(RemoveTestCode()); 11 | 12 | const clearTestCode = () => { 13 | removeTestCode(); 14 | }; 15 | 16 | function pathToText() { 17 | const x: any = generatedTestCode; 18 | let counter = 0; 19 | for (let space = 0; space < x.length - 1; space++) { 20 | if (x[space] === " " && x[space + 1] === " ") { 21 | counter++; 22 | } 23 | } 24 | 25 | // move all in line styles into the css file 26 | const tester = x.split("\n").map((ele: string, id: number) => { 27 | let start = 0; 28 | while (ele[start] === " ") { 29 | start++; 30 | } 31 | let spaces = ""; 32 | for (let i = 0; i < Math.floor(start / 2); i++) { 33 | spaces += "\t"; 34 | } 35 | let begin = 5 - id.toString().length; 36 | let beginSpaces = id.toString(); 37 | while (begin > 0) { 38 | beginSpaces += " "; 39 | begin--; 40 | } 41 | return ( 42 |
43 |
44 |             {beginSpaces}
45 |             {spaces}
46 |             {ele}{" "}
47 |           
48 |
49 | ); 50 | }); 51 | return tester; 52 | } 53 | 54 | return generatedTestCode === "" ? ( 55 | <> // fragment shorthand 56 | ) : ( 57 |
58 |
59 | 62 |
63 | {pathToText()} 64 |
65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /assets/icons/file_type_reactjs.svg: -------------------------------------------------------------------------------- 1 | file_type_reactjs -------------------------------------------------------------------------------- /assets/icons/file_type_reactts.svg: -------------------------------------------------------------------------------- 1 | file_type_reactts -------------------------------------------------------------------------------- /assets/icons/file_type_reacttemplate.svg: -------------------------------------------------------------------------------- 1 | file_type_reacttemplate -------------------------------------------------------------------------------- /src/components/FileViewer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import fs from "fs"; 4 | import { ClearFile } from "../reduxComponents/actions/actions"; 5 | 6 | export const FileViewer: React.FC = () => { 7 | const dispatch = useDispatch(); 8 | const fileViewPath = useSelector((state: any) => state.fileToView); 9 | const clearFileInStore = () => dispatch(ClearFile()); 10 | 11 | function clearFile() { 12 | clearFileInStore(); 13 | } 14 | 15 | function pathToText() { 16 | const fileContent = fs.readFileSync(fileViewPath); 17 | const x: any = fileContent.toString(); 18 | let counter = 0; 19 | for (let space = 0; space < x.length - 1; space++) { 20 | if (x[space] === " " && x[space + 1] === " ") { 21 | counter++; 22 | } 23 | } 24 | 25 | // move all in line styles into the css file 26 | const tester = x.split("\n").map((ele: string, id: number) => { 27 | let start = 0; 28 | while (ele[start] === " ") { 29 | start++; 30 | } 31 | let spaces = ""; 32 | for (let i = 0; i < Math.floor(start / 2); i++) { 33 | spaces += "\t"; 34 | } 35 | let begin = 5 - id.toString().length; 36 | let beginSpaces = id.toString(); 37 | while (begin > 0) { 38 | beginSpaces += " "; 39 | begin--; 40 | } 41 | return ( 42 |
43 |
44 |             {beginSpaces}
45 |             {spaces}
46 |             {ele}{" "}
47 |           
48 |
49 | ); 50 | }); 51 | return tester; 52 | } 53 | 54 | return fileViewPath === "" ? ( 55 | <> // fragment shorthand over React.Fragment 56 | ) : ( 57 |
58 |
59 | 62 |
63 |
64 |
65 | {pathToText()} 66 |
67 |
68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/stylesheets/components/_testblock.scss: -------------------------------------------------------------------------------- 1 | $primary: #032539; 2 | $secondary: #1c768f; 3 | $fg: #FA991C; 4 | $bg: #FBF3F2; 5 | 6 | .testBlock{ 7 | width: 100vw; 8 | height: 10vh; 9 | display: flex; 10 | align-items: center; 11 | background: $primary; 12 | box-shadow: 10px 15px orange; 13 | .catalysticon { 14 | width: 4.5em; 15 | height: 3em; 16 | margin-left: 1em; 17 | } 18 | .headerbar { 19 | display: flex; 20 | width: 95vw; 21 | align-items: center; 22 | justify-content: space-between; 23 | button { 24 | background: $primary; 25 | color:$bg; 26 | align-items: center; 27 | font-size: 24px; 28 | border: none; 29 | border-radius: 1.25em; 30 | padding: .25em .6em; 31 | } 32 | .headerlist { 33 | display: flex; 34 | list-style: none; 35 | padding-left: 0; 36 | margin-left: 0em; 37 | li { 38 | padding: 0em 1em; 39 | color: $bg; 40 | .generatetests { 41 | overflow: hidden; 42 | background: linear-gradient(to right, $fg, $fg 50%, $bg 50%); 43 | background-clip: text; 44 | -webkit-background-clip: text; 45 | -webkit-text-fill-color: transparent; 46 | background-size: 200% 100%; 47 | background-position: 100%; 48 | transition: background-position 275ms ease; 49 | } 50 | .generatetests:hover { 51 | background-position: 0 100%; 52 | } 53 | } 54 | } 55 | .iconlist { 56 | display: flex; 57 | list-style: none; 58 | padding-left: 0; 59 | margin: 0em 1em; 60 | height: 36px; 61 | li { 62 | margin: 0em .5em; 63 | color: $bg; 64 | a, a:hover, a:focus { 65 | text-decoration: none; 66 | color: inherit; 67 | .fa-github { 68 | font-size: 32px; 69 | } 70 | .fa-globe-americas { 71 | font-size: 32px; 72 | } 73 | .fa-question-circle { 74 | font-size: 32px; 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /electron/main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | import * as path from 'path'; 3 | import * as url from 'url'; 4 | import installExtension, { REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS} from 'electron-devtools-installer'; 5 | 6 | // keep a global reference of the window object, if you don't, the window will 7 | // be closed automatically when the javascript object is garbage collected 8 | 9 | let mainWindow: Electron.BrowserWindow | null; 10 | 11 | 12 | function createWindow() { 13 | // create the browser window 14 | mainWindow = new BrowserWindow({ 15 | width: 1920, 16 | height: 1080, 17 | webPreferences: { 18 | nodeIntegration: true, 19 | // this allows us to access remote in other files of the app 20 | enableRemoteModule: true 21 | }, 22 | icon: path.resolve(__dirname, '../assets/catalyst_icons/CatalystDockIconLarge-04.png') 23 | }); 24 | 25 | if (process.platform === 'darwin') { 26 | app.dock.setIcon(path.resolve(__dirname, '../assets/catalyst_icons/CatalystDockIconLarge-04.png')); 27 | } 28 | if (process.env.NODE_ENV === 'development') { 29 | mainWindow.loadURL(`http://localhost:4000`); 30 | 31 | } else { 32 | const urlToLoad = url.format({ 33 | pathname: path.resolve(__dirname, '../dist/renderer/index.html'), 34 | protocol: 'file:', 35 | slashes: true 36 | }); 37 | mainWindow.loadURL(urlToLoad); 38 | }; 39 | 40 | 41 | 42 | // emitted when the window is closed 43 | mainWindow.on('closed', () => { 44 | // derefernece the window object, normally windows are stored in an array if app 45 | // supports multi windows, this is the time when you should delete the corresponding element 46 | mainWindow = null; 47 | }); 48 | } 49 | 50 | app.on('ready', () => { 51 | // calling installExtension func after the ready event was emitted by app 52 | // on the different extensions by looping through array of them 53 | [REDUX_DEVTOOLS, REACT_DEVELOPER_TOOLS].forEach(extension => { 54 | installExtension(extension) 55 | .then((name) => console.log(`Added Extension: ${name}`)) 56 | .catch((err) => console.log('An error occurred: ', err)); 57 | }); 58 | // once extensions are looped through, invoking createWindow method 59 | createWindow(); 60 | }); 61 | 62 | 63 | app.allowRendererProcessReuse = true; 64 | -------------------------------------------------------------------------------- /src/reduxComponents/actions/actions.ts: -------------------------------------------------------------------------------- 1 | import * as types from "../constants/actionTypes"; 2 | 3 | export const ConstructFileTree = (files: []) => ({ 4 | type: types.CONSTRUCT_FILETREE, 5 | payload: files, 6 | }); 7 | 8 | export const SetFileView = (filePath: any) => ({ 9 | type: types.SET_FILE_VIEW, 10 | payload: filePath, 11 | }); 12 | 13 | export const ToggleFolder = (filePath: string) => ({ 14 | type: types.TOGGLE_FOLDER, 15 | payload: filePath, 16 | }); 17 | 18 | export const UpdateKeyOfExpect = () => ({ 19 | type: types.UPDATE_KEY_OF_EXPECT, 20 | payload: "", 21 | }); 22 | 23 | export const UpdateData = (data: any) => ({ 24 | type: types.UPDATE_DATA, 25 | payload: data, 26 | }); 27 | 28 | export const UpdateKeyOfDescribe = () => ({ 29 | type: types.UPDATE_KEY_OF_DESCRIBE, 30 | payload: "", 31 | }); 32 | 33 | export const UpdateKeyOfIt = () => ({ 34 | type: types.UPDATE_KEY_OF_IT, 35 | payload: "", 36 | }); 37 | 38 | export const UpdateItObj = (data: any) => ({ 39 | type: types.UPDATE_IT_OBJ, 40 | payload: data, 41 | }); 42 | 43 | export const UpdateDescribe = (data: any) => ({ 44 | type: types.UPDATE_DESCRIBE, 45 | payload: data, 46 | }); 47 | 48 | export const UpdateComponentName = (name: any) => ({ 49 | type: types.UPDATE_COMPONENT_NAME, 50 | payload: name, 51 | }); 52 | 53 | export const ClearFile = () =>({ 54 | type: types.CLEAR_FILE, 55 | }); 56 | 57 | export const deleteExpect = (data: any) => ({ 58 | type: types.DELETE_EXPECT, 59 | payload: data, 60 | }); 61 | 62 | export const removeFromIt = (data: any) => ({ 63 | type: types.REMOVE_FROM_IT, 64 | payload: data, 65 | }); 66 | 67 | export const SetProjectPath = (filePath: string) => ({ 68 | type: types.SET_PROJECT_PATH, 69 | payload: filePath, 70 | }); 71 | 72 | export const UpdateItStatement = (data: any) => ({ 73 | type: types.UPDATE_IT_STATEMENT, 74 | payload: data, 75 | }); 76 | 77 | export const RefreshState = () => ({ 78 | type: types.REFRESH_STATE, 79 | }); 80 | 81 | export const UpdateDescribeBoolean = (data: any) => ({ 82 | type: types.UPDATE_DESCRIBE_BOOLEAN, 83 | payload: data, 84 | }); 85 | 86 | export const ViewTestCode = (data: string) => ({ 87 | type: types.SHOW_TESTCODE, 88 | payload: data, 89 | }); 90 | 91 | export const RemoveTestCode = () => ({ 92 | type: types.REMOVE_TESTCODE, 93 | }); 94 | -------------------------------------------------------------------------------- /src/components/ReuploadDirectory.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { remote } from "electron"; 3 | import { useDispatch } from "react-redux"; 4 | import { FileTree } from "./FolderUpload"; 5 | import { 6 | ConstructFileTree, 7 | SetProjectPath, 8 | RefreshState, 9 | } from "../reduxComponents/actions/actions"; 10 | 11 | export const ReuploadDirectory: React.FC = () => { 12 | const dispatch = useDispatch(); 13 | // creates dispatch that will send file path as an action payload to reducer 14 | const constructFileTree = (files: any) => dispatch(ConstructFileTree(files)); 15 | const setFilePath = (filePath: any) => dispatch(SetProjectPath(filePath)); 16 | const refreshState = () => dispatch(RefreshState()); 17 | 18 | const { dialog } = remote; 19 | async function uploadFolder() { 20 | // allows users to upload a folder 21 | const Project = await dialog.showOpenDialog( 22 | // sets opendirectory (allows directories to be selected) to be the feature that dialog uses 23 | { 24 | properties: ["openDirectory"], 25 | // the types of files that will be displayed or selected 26 | filters: [ 27 | { name: "Javascript Files", extensions: ["js", "jsx"] }, 28 | { name: "Typescript Files", extensions: ["ts", "tsx"] }, 29 | { name: "HTML Files", extensions: ["html"] }, 30 | ], 31 | message: "Choose a Project to Create Tests for:", 32 | } 33 | ); 34 | 35 | // if the user cancels the action then undefined will be returned 36 | // if the user successfully completes the action then a string array will be returned 37 | // Project is an object that holds canceled (boolean to check if it was cancelled) and filePaths (array of filepaths) 38 | if (Project && !Project.canceled) { 39 | // holds the directory of the project that was selected 40 | let projectDirectory = Project.filePaths[0]; 41 | 42 | // use regex to find all \ in the case of a windows user and replace with / 43 | projectDirectory = projectDirectory.replace(/\\/g, "/"); 44 | 45 | // will create a new FileTree object for the root directory 46 | const rootTree = new FileTree(projectDirectory, "root"); 47 | 48 | // will return an array of FileTree objects along with any children associated with it 49 | const fileTree = rootTree.createTree(projectDirectory); 50 | 51 | // dispatch call to refresh state 52 | refreshState(); 53 | // dispatches fileTree to reducer 54 | constructFileTree(fileTree); 55 | // setting file path of project to access later by sending projectDirectory to store 56 | setFilePath(projectDirectory); 57 | } 58 | } 59 | 60 | return ( 61 |
62 | 65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /assets/icons/file_type_babel2.svg: -------------------------------------------------------------------------------- 1 | file_type_babel2 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://i.imgur.com/OpWjHnD.png) 2 | 3 | ## What is Catalyst? 4 | 5 | Catalyst is an application which allows developers to generate unit test cases for components utilizing the Jest framework and Javascript's Enzyme testing utility. With its interactive GUI, the creation of test code can be completed with just a few clicks. 6 | 7 | Catalyst is currently in beta and compatible for use with React 16. 8 | 9 | ## Installation 10 | 11 | First download the app [here](https://www.catalystjs.com/) 12 | 13 | Or you can also get started by cloning the repo using 14 | 15 | git clone https://github.com/oslabs-beta/Catalyst.git 16 | 17 | - Once you have the repo on your system, install all the dependencies using: 18 | 19 | npm install 20 | 21 | - After that is completed, you can open our app using: 22 | 23 | npm run dev 24 | 25 | Prior to running exported tests, Jest, Enzyme, and Enzyme Adapter for React 16 must be installed. 26 | To do this, enter the following line in the command line interface of your project's directory: 27 | 28 | npm install --save-dev jest enzyme enzyme-adapter-react-16 29 | 30 | ## How to use 31 | 32 | 1. Select the root folder for the project you would like to create tests for. 33 | 34 | ![](https://i.imgur.com/o1EVl5B.gif) 35 | 36 | Opitonally, you can select which file you want to generate tests for and preview the component of choice on the right. 37 | 38 | ![](https://i.imgur.com/XV6acqK.gif) 39 | 40 | 2. Using our interactive GUI, choose from Enzyme's matcher API and fill in their respective selectors. If the selected matcher does not require a selector input, leave blank. Additionally, selector input box can be the value of your choosing. Once completed, click the 'Generate Tests' button to preview your test code. 41 | 42 | ![](https://i.imgur.com/Yze4a98.gif) 43 | 44 | 3. If satisfied, click the 'Export Test Code' button, name your 45 | file, and select the save button to save the generated code in your project. Catalyst will save this file in a \_\_tests\_\_ folder for you automatically if it doesn't already exist. 46 | 47 | ![](https://i.imgur.com/bCiQj4O.gif) 48 | 49 | 4. Icons located in the right corner of application open additional windows with links to Catalyst's Github, Enzyme's official documentation, and a Enzyme Cheat Sheet. 50 | 51 | ## Contributing 52 | 53 | This project is still very early in development, so please inform our team of any issues found in the application or if you have any suggestions. 54 | Feel free to reach out to the development team if you'd like to make a pull requrest to add new features to this project. 55 | 56 | ## License 57 | Distributed under the MIT License. See LICENSE for more information. 58 | 59 | ## Authors 60 | 61 | > Jon Ascencio [jascenc1](https://github.com/jascenc1)  ·  62 | > Danny Chung [chungdanny64](https://github.com/chungdanny64)  ·  63 | > Gavin Crews [gcrews1894](https://github.com/gcrews1894)  ·  64 | > Jarryl Oquias [jaroqui17](https://github.com/jaroqui17) 65 | -------------------------------------------------------------------------------- /src/components/FileTree.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import * as electronFs from "fs"; 4 | import { SetFileView, ToggleFolder } from "../reduxComponents/actions/actions"; 5 | import FILE_ICONS from "../icons/icons"; 6 | 7 | export const FileTree: React.FC = () => { 8 | const dispatch = useDispatch(); 9 | // obtains the filetree from the store 10 | const fileTree = useSelector((state: any) => state.fileTree); 11 | // obtains the folder status from the store 12 | const isOpen = useSelector((state: any) => state.toggleFolder); 13 | // dispatches an action to set the file that will be viewed 14 | const setFileInRedux = (checker: any) => dispatch(SetFileView(checker)); 15 | // dispatches an action to set the folder to open or not 16 | const toggleFolder = (filePath: string) => dispatch(ToggleFolder(filePath)); 17 | 18 | // onclick function to set invoke the function to dispatch an action 19 | function setFileView(event: any) { 20 | // file path is saved as the id of the button 21 | setFileInRedux(event.target.id); 22 | } 23 | 24 | // func will strip extension ending from files which will be used as the key for the imported object that holds images for 25 | // those specific file icons 26 | const extensionGrabber = (ext: string) => { 27 | if (ext.split(".")[1] === undefined) { 28 | return "folder"; 29 | } 30 | if (ext.split(".")[ext.split(".").length - 1].includes("babel")) { 31 | return "babel"; 32 | } 33 | if ( 34 | ext 35 | .split(".") 36 | [ext.split(".").length - 1].includes( 37 | "eslint" || ext.split(".")[1].includes("eslint") 38 | ) 39 | ) { 40 | return "eslint"; 41 | } 42 | return ext.split(".")[ext.split(".").length - 1]; 43 | }; 44 | 45 | // sending filepath to reducer to keep track of which folder is clicked on. if set to a global state then it will 46 | // toggle true and false on each click of the directory 47 | const handleToggle = (filePath: string) => { 48 | toggleFolder(filePath); 49 | }; 50 | 51 | const traverseFileTree = (files: []) => { 52 | return ( 53 |
    54 | {files.map((file: any, id: number) => { 55 | const extension = extensionGrabber(file.name); 56 | const checker = electronFs.statSync(file.filepath); 57 | 58 | if (checker.isDirectory()) { 59 | return ( 60 |
  • 61 | handleToggle(file.filepath)} 65 | > 66 | {FILE_ICONS[extension]} 67 | {file.name} 68 | 69 | {isOpen[file.filepath] && traverseFileTree(file.children)} 70 |
  • 71 | ); 72 | } 73 | return ( 74 |
  • 75 | 76 | {FILE_ICONS[extension]} 77 | {file.name} 78 | 79 | {file.children.length > 0 && traverseFileTree(file.children)} 80 |
  • 81 | ); 82 | })} 83 |
84 | ); 85 | }; 86 | 87 | return fileTree ? traverseFileTree(fileTree) : <>; // using fragment shorthand over React.Fragment 88 | }; 89 | -------------------------------------------------------------------------------- /src/stylesheets/components/_folderupload.scss: -------------------------------------------------------------------------------- 1 | $primary: #032539; 2 | $secondary: #1c768f; 3 | $fg: #FA991C; 4 | $bg: #FBF3F2; 5 | 6 | .frontheader { 7 | background: $primary; 8 | height: 30vh; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | box-shadow: 0px 1px 15px $primary; 13 | } 14 | 15 | .frontpage { 16 | background: $bg; 17 | height: 100vh; 18 | padding: 0; 19 | margin: 0; 20 | } 21 | 22 | .frontbody { 23 | width: 30vw; 24 | height: 25em; 25 | display: flex; 26 | flex-wrap: wrap; 27 | margin-top: 4em; 28 | margin-left: auto; 29 | margin-right: auto; 30 | padding: 0; 31 | justify-content: center; 32 | align-items: center; 33 | text-align: center; 34 | h6{ 35 | margin: 0; 36 | padding: 0; 37 | font-size: 48px; 38 | color: $primary; 39 | .fronth2 { 40 | margin-bottom: 0; 41 | padding-bottom: 0; 42 | } 43 | } 44 | p { 45 | color: $primary; 46 | margin: 0; 47 | padding: 0; 48 | font-size: 32px; 49 | } 50 | } 51 | 52 | // Variables 53 | $speed: "0.25s"; 54 | $transition: all #{$speed} cubic-bezier(0.310, -0.105, 0.430, 1.400); 55 | 56 | /* Main Styles */ 57 | .folderupload { 58 | display: flex; 59 | background-color: $fg; 60 | width: 350px; 61 | height: 75px; 62 | line-height: 75px; 63 | margin: auto; 64 | color: #fff; 65 | position: relative; 66 | top: 0; 67 | bottom: 0; 68 | left: 0; 69 | right: 0; 70 | cursor: pointer; 71 | overflow: hidden; 72 | border-radius: 5em; 73 | border: none; 74 | box-shadow: 0 0 20px 0 rgba(0,0,0,.3); 75 | transition: $transition; 76 | 77 | span, 78 | .icon { 79 | display: block; 80 | height: 100%; 81 | text-align: center; 82 | position: absolute; 83 | top: 0; 84 | } 85 | 86 | span { 87 | width: 72%; 88 | line-height: inherit; 89 | font-size: 22px; 90 | text-transform: uppercase; 91 | left: 0; 92 | transition: $transition; 93 | 94 | &:after { 95 | content: ''; 96 | background-color: $bg; 97 | width: 2px; 98 | height: 70%; 99 | position: absolute; 100 | top: 15%; 101 | right: -1px; 102 | } 103 | } 104 | .icon { 105 | width: 28%; 106 | right: 0; 107 | transition: $transition; 108 | .fas { 109 | font-size: 30px; 110 | vertical-align: middle; 111 | transition: $transition, height #{$speed} ease; 112 | } 113 | .fa-upload { 114 | height: 36px; 115 | } 116 | .fa-check { 117 | display: none; 118 | } 119 | } 120 | &.success, 121 | &:hover { 122 | 123 | span { 124 | left: -72%; 125 | opacity: 0; 126 | } 127 | 128 | .icon { 129 | width: 100%; 130 | 131 | .fa { 132 | font-size: 45px; 133 | } 134 | } 135 | } 136 | 137 | &.success { 138 | background-color: #27ae60; 139 | 140 | .icon { 141 | 142 | .fa-remove { 143 | display: none; 144 | } 145 | 146 | .fa-check { 147 | display: inline-block; 148 | } 149 | } 150 | } 151 | 152 | &:hover { 153 | background-color: lighten($fg, 5%); 154 | 155 | .icon .fa-remove { 156 | height: 46px; 157 | } 158 | } 159 | 160 | &:active { 161 | opacity: 1; 162 | } 163 | } 164 | 165 | // .folderupload:after { 166 | // content: ''; 167 | // position: absolute; 168 | // bottom: 0; 169 | // left: 0; 170 | // width: 100%; 171 | // height: 100%; 172 | // background-color: $fg; 173 | // border-radius: 10rem; 174 | // z-index: -2; 175 | // } 176 | // .folderupload:before { 177 | // content: ''; 178 | // position: absolute; 179 | // bottom: 0; 180 | // left: 0; 181 | // width: 0%; 182 | // height: 100%; 183 | // background-color: darken($fg, 10%); 184 | // transition: all .3s; 185 | // border-radius: 10rem; 186 | // z-index: -1; 187 | // } 188 | // .folderupload:hover { 189 | // color: #fff; 190 | // &:before { 191 | // width: 100%; 192 | // } 193 | // } 194 | 195 | .catalystlogo { 196 | width: 40em; 197 | height: auto; 198 | } -------------------------------------------------------------------------------- /src/components/PropLoader.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { UpdateDescribeBoolean } from "../reduxComponents/actions/actions"; 4 | 5 | interface Props { 6 | id: string; 7 | } 8 | 9 | export const PropLoader: React.FC = ({ id }) => { 10 | const temp = useRef([]); 11 | const [propArray, updatePropArray] = useState([]); 12 | const [displayArray, updateDisplay] = useState([]); 13 | const [count, updateCount] = useState(0); 14 | const [check, updateBool] = useState(false); 15 | 16 | const dispatch = useDispatch(); 17 | const propBoolean = useSelector((state: any) => state.describePropBoolean); 18 | 19 | const updatePropBooleanInStore = (data: any) => 20 | dispatch(UpdateDescribeBoolean(data)); 21 | 22 | useEffect(() => { 23 | const display = []; 24 | for (const x of propArray) { 25 | for (const ele of Object.values(x)) { 26 | display.push(ele); 27 | } 28 | } 29 | updateDisplay(display); 30 | }, [propArray]); 31 | 32 | // adds a key value element to the display 33 | function addProp( 34 | event: React.MouseEvent 35 | ): void { 36 | event?.preventDefault(); 37 | // checks to see if the checkbox is fille and if it is then add the prop field 38 | if (propBoolean[`${id}`]) { 39 | // save the element as in an object with a key associated to it for easy access on deletion 40 | const prop: { [k: string]: any } = {}; 41 | prop[`describe${id}prop${count}`] = ( 42 |
43 | {/* */} 44 | 45 | 46 | 53 |
54 | ); 55 | 56 | temp.current = temp.current.concat([prop]); 57 | updatePropArray([...propArray, prop]); 58 | updateCount(count + 1); 59 | } 60 | } 61 | 62 | // deletes the selected key value field 63 | function test(event: React.MouseEvent): void { 64 | event.preventDefault(); 65 | const answer = []; 66 | // loops through the current array of key value fields and adds those that are not selected 67 | for (const x of temp.current) { 68 | if (Object.keys(x)[0] !== event.target?.id.replace("button", "")) { 69 | answer.push(x); 70 | } 71 | } 72 | temp.current = answer; 73 | updatePropArray(answer); 74 | } 75 | 76 | // toggles the check box 77 | function updateCheck(): void { 78 | // if the checkbox is checked 79 | if (propBoolean[`${id}`]) { 80 | updatePropArray([]); 81 | temp.current = []; 82 | updateCount(0); 83 | } 84 | // sets the boolean value to the opposite value 85 | propBoolean[`${id}`] = !propBoolean[`${id}`]; 86 | // update the boolean value in the store 87 | updatePropBooleanInStore(propBoolean); 88 | updateBool(!check); 89 | } 90 | 91 | return !check ? ( 92 |
93 |
94 | 95 | 102 |
103 |
104 | ) : ( 105 |
106 |
107 | 108 | 115 |
116 | {displayArray} 117 | 118 | 119 |
120 | ); 121 | }; 122 | -------------------------------------------------------------------------------- /src/components/ExportTestCode.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | // eslint-disable-next-line import/no-extraneous-dependencies 4 | import { remote } from "electron"; 5 | import * as electronFs from "fs"; 6 | import path from "path"; 7 | import { ViewTestCode } from "../reduxComponents/actions/actions"; 8 | import { generateTestCode } from "../utils/globalCodeGenerator"; 9 | 10 | const { dialog } = remote; 11 | 12 | export const ExportTestCode: React.FC = () => { 13 | // get generated TestCode from Global Store 14 | const generatedTestCode = useSelector( 15 | (state: any) => state.generatedTestCode 16 | ); 17 | // get users project file path from store to aid in exporting generated test code to their machine 18 | const projectFilePath = useSelector((state: any) => state.filePathOfProject); 19 | 20 | const dispatch = useDispatch(); 21 | // generated testCode from GUI is dispactched to store to be displayed in viewer 22 | const viewTestCode = (data: string) => dispatch(ViewTestCode(data)); 23 | 24 | // helper function that will open browser window and default to a __tests__ directory for generated test code placement 25 | const openDialog = (userFilePath: string, testCode: string) => { 26 | // normalizing filePath to work cross-platform 27 | const filePath = path.normalize(`${userFilePath}/__tests__/`); 28 | dialog 29 | .showSaveDialog({ 30 | title: "Please name your Test File", 31 | defaultPath: filePath, // can add location to save file on users directory 32 | filters: [ 33 | { 34 | name: "Test Files", 35 | extensions: ["test"], 36 | }, 37 | ], 38 | message: "Choose location", 39 | properties: ["createDirectory"], 40 | }) 41 | .then((file) => { 42 | // stating whether dialog operation was cancelled or not 43 | if (!file.canceled) { 44 | // creating and writing to the generated.test file, adding js extension for enzyme compatibility 45 | electronFs.writeFile( 46 | `${file.filePath?.toString()}.js`, 47 | testCode, 48 | (err) => { 49 | if (err) { 50 | // eslint-disable-next-line no-console 51 | console.log(err.message); 52 | } 53 | // eslint-disable-next-line no-console 54 | console.log("saved"); 55 | } 56 | ); 57 | } 58 | }) 59 | .catch((err) => { 60 | // eslint-disable-next-line no-console 61 | console.log(err); 62 | }); 63 | }; 64 | 65 | // determing if directory __tests__ exists already 66 | const exportTestCode = (userFilePath: string, testCode: string) => { 67 | // if __tests__ directory does not exist then create one and write generated test code into that newly created directory 68 | if (!electronFs.existsSync(`${userFilePath}/__tests__`)) { 69 | electronFs.mkdirSync(`${userFilePath}/__tests__`); 70 | openDialog(userFilePath, testCode); 71 | } 72 | // if __tests__ directory does exist then just generate another file into that directory 73 | else { 74 | openDialog(userFilePath, testCode); 75 | } 76 | }; 77 | 78 | const exportCodeToDirectory = () => { 79 | if (generatedTestCode === "") { 80 | // if user logs in and does not want to visualize code, helper function will create test code 81 | // this test code will be sent to store as well as be exported 82 | const testCode = generateTestCode(); 83 | // open window to export file 84 | exportTestCode(projectFilePath, testCode); 85 | // dispatch generated code to store to be seen on side panel 86 | viewTestCode(testCode); 87 | } else { 88 | const testCode = generateTestCode(); 89 | // if user clicked on generat code before exporting code, it will be pulled from store 90 | exportTestCode(projectFilePath, testCode); 91 | viewTestCode(testCode); 92 | } 93 | }; 94 | 95 | return ( 96 |
97 | 100 |
101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /src/components/DescribeBlock.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { ItStatement } from "./ItStatement"; 4 | import { PropLoader } from "./PropLoader"; 5 | import { 6 | UpdateKeyOfIt, 7 | UpdateDescribe, 8 | UpdateComponentName, 9 | } from "../reduxComponents/actions/actions"; 10 | 11 | interface Props { 12 | describeProp: string; 13 | removeDescribe: (id: number) => boolean; 14 | } 15 | 16 | export const DescribeBlock: React.FC = ({ 17 | describeProp, 18 | removeDescribe, 19 | }) => { 20 | const globalDescribeObj = useSelector((state: any) => state.describes); 21 | const index = useSelector((state: any) => state.keyOfIt); 22 | const componentObj = useSelector((state: any) => state.componentObj); 23 | 24 | const [arrayOfIt, updateItArray] = useState([]); 25 | 26 | const dispatch = useDispatch(); 27 | 28 | const updateItKey = () => dispatch(UpdateKeyOfIt()); 29 | const updateGlobalDescribe = (data: any) => dispatch(UpdateDescribe(data)); 30 | const updateComponentName = (name: string) => 31 | dispatch(UpdateComponentName(name)); 32 | 33 | const storeval: { [k: string]: any } = {}; 34 | storeval[`${index}`] = ""; 35 | 36 | useEffect(async (): Promise => { 37 | const itComponent: { [k: string]: any } = {}; 38 | // creates a key value pair that will hold the index and the component 39 | itComponent[`${index}`] = await ( 40 | 46 | ); 47 | // update the array to be displayed with the component that was created 48 | updateItArray(arrayOfIt.concat(itComponent[`${index}`])); 49 | // update the key value of the it statements 50 | updateItKey(); 51 | }, []); 52 | 53 | async function addIt() { 54 | const itComponent: { [k: string]: any } = {}; 55 | itComponent[`${index}`] = await ( 56 | 62 | ); 63 | // add the index of the created it component to the object holding all describe blocks 64 | // globalDescribeObj[`${describeProp}`] = globalDescribeObj[`${describeProp}`].concat(index) 65 | globalDescribeObj[`${describeProp}`][index] = ""; 66 | // updates the describe element in the store 67 | updateGlobalDescribe(globalDescribeObj); 68 | // updates the array to be displayed 69 | updateItArray(arrayOfIt.concat(itComponent[`${index}`])); 70 | // increment the number of it statements since one was added 71 | updateItKey(); 72 | } 73 | 74 | function addComponentName(input: any): void { 75 | componentObj[describeProp] = input; 76 | updateComponentName(componentObj); 77 | } 78 | 79 | // removes the it block selected from the current describe block 80 | function removeIt(removeItId: number): boolean { 81 | // if there are more than one it statements then delete selected 82 | if (Object.keys(globalDescribeObj[describeProp]).length > 1) { 83 | delete globalDescribeObj[describeProp][`${removeItId}`]; 84 | updateGlobalDescribe(globalDescribeObj); 85 | return true; 86 | } 87 | // if there is only one it statement then return false to notify that it cannot be deleted 88 | return false; 89 | } 90 | 91 | function removeDescribeComponent(): void { 92 | removeDescribe(parseInt(describeProp, 10)); 93 | } 94 | 95 | return ( 96 |
97 | 100 |
101 |

Describe Block

102 | addComponentName(e.target.value)} 106 | placeholder="Please enter component name:" 107 | /> 108 |
109 |
110 | 114 | 115 | {arrayOfIt} 116 |
117 | 120 |
121 | ); 122 | }; 123 | -------------------------------------------------------------------------------- /src/components/ItStatement.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { 4 | UpdateData, 5 | UpdateItObj, 6 | UpdateItStatement, 7 | } from "../reduxComponents/actions/actions"; 8 | import { ExpectStatement } from "./ExpectStatement"; 9 | 10 | interface Props { 11 | itProp: string; 12 | id: any; 13 | removeIt: (id: number) => boolean; 14 | } 15 | 16 | export const ItStatement: React.FC = ({ itProp, removeIt, id }) => { 17 | const dispatch = useDispatch(); 18 | const index = useSelector((state: any) => state.keyOfExpect); 19 | const data = useSelector((state: any) => state.expects); 20 | const itObject = useSelector((state: any) => state.its); 21 | const updateIts = (data: any) => dispatch(UpdateItObj(data)); 22 | const updateData = (data: any) => dispatch(UpdateData(data)); 23 | const [arrayOfExpect, updateArray] = useState([]); 24 | const globalItInput = useSelector((state: any) => state.itInputObj); 25 | const updateItGlobal = (data: any) => dispatch(UpdateItStatement(data)); 26 | 27 | const itKey = useSelector((state: any) => state.keyOfIt); 28 | const storeval: { [k: string]: any } = {}; 29 | storeval[`${index}`] = ""; 30 | 31 | useEffect(() => { 32 | // unsure about this typing and if piping would work ConcatArray | JSX.Element 33 | const x: { [k: string]: any } = {}; 34 | x[`${index}`] = ( 35 | 36 | ); 37 | updateArray(arrayOfExpect.concat(x[`${index}`])); 38 | // update the store with the newly added it statement 39 | itObject[`${itKey}`] = storeval; 40 | updateIts(itObject); 41 | }, []); 42 | 43 | // adds an expect statement to the current it statement 44 | function addExpect() { 45 | const x: { [k: string]: any } = {}; 46 | itObject[`${itProp}`][index] = ""; 47 | 48 | x[`${index}`] = ( 49 | 50 | ); 51 | updateArray(arrayOfExpect.concat(x[index])); 52 | // create an object in to be passed into the store 53 | const newExpect: { [k: string]: any } = {}; 54 | newExpect[`firstInput${index}`] = "type"; 55 | newExpect.testTypes = "equal"; 56 | newExpect[`lastInput${index}`] = ""; 57 | // add the key value pair to the expect object in the store 58 | data[index] = newExpect; 59 | 60 | updateData(data); 61 | // updates the it object associated with the component to hold the key value of the child 62 | updateIts(itObject); 63 | } 64 | 65 | // remove an expect block from the it statement 66 | function removeExpect(removeId: number) { 67 | // if there are more than one then delete selected 68 | if (Object.keys(itObject[`${itProp}`]).length > 1) { 69 | delete itObject[`${itProp}`][`${removeId}`]; 70 | updateIts(itObject); 71 | return true; 72 | } 73 | // if there are not more then one return false to notify that it cannot be deleted 74 | return false; 75 | } 76 | 77 | // updates the it statement value in the store 78 | const updateItStatement = (input: any) => { 79 | globalItInput[itProp] = input; 80 | updateItGlobal(globalItInput); 81 | }; 82 | 83 | // removes the it statement from the current describe block 84 | function removeItStatement() { 85 | // id is the id of the it block that needs to be removed from the describe 86 | if (removeIt(id)) { 87 | // here we have to delete it blocks 88 | for (const itBlocks of Object.keys(itObject[`${itProp}`])) { 89 | delete data[`${itBlocks}`]; 90 | } 91 | document.getElementById(`it-block ${itProp}`)?.remove(); 92 | updateData(data); 93 | // delete the it block 94 | delete itObject[`${itProp}`]; 95 | } else { 96 | console.log("cannot remove it block"); 97 | } 98 | } 99 | 100 | return ( 101 |
102 | { 106 | updateItStatement(e.target?.value); 107 | }} 108 | /> 109 | 112 | {arrayOfExpect} 113 | 116 |
117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /src/reduxComponents/reducers/reducer.ts: -------------------------------------------------------------------------------- 1 | import * as types from "../constants/actionTypes"; 2 | 3 | const initialState = { 4 | fileTree: [], 5 | fileToView: "", 6 | toggleFolder: {}, 7 | filePathOfProject: "", 8 | generatedTestCode: "", 9 | 10 | describes: {}, 11 | its: {}, 12 | expects: {}, 13 | 14 | keyOfDescribe: 0, 15 | keyOfIt: 0, 16 | keyOfExpect: 0, 17 | componentObj: {}, 18 | itInputObj: {}, 19 | 20 | describePropBoolean: {}, 21 | codeViewer: false, 22 | codeViewerChecker: true, 23 | }; 24 | 25 | export const reducer = (state: any = initialState, action: any) => { 26 | switch (action.type) { 27 | case types.CONSTRUCT_FILETREE: 28 | return { 29 | ...state, 30 | fileTree: action.payload, 31 | }; 32 | 33 | case types.SET_FILE_VIEW: 34 | return { 35 | ...state, 36 | codeViewer: true, 37 | fileToView: action.payload, 38 | codeViewerChecker: true, 39 | }; 40 | 41 | case types.TOGGLE_FOLDER: 42 | // creating an object that will hold the file path of each directory and will toggle from T to F 43 | // important as we can now keep track of whether that specifc directory/filepath has been clicked or not 44 | const toggleFolder = { ...state.toggleFolder }; 45 | toggleFolder[action.payload] = !toggleFolder[action.payload]; 46 | return { 47 | ...state, 48 | toggleFolder, 49 | }; 50 | 51 | case types.UPDATE_KEY_OF_EXPECT: 52 | return { 53 | ...state, 54 | keyOfExpect: state.keyOfExpect + 1, 55 | }; 56 | 57 | case types.UPDATE_KEY_OF_IT: 58 | return { 59 | ...state, 60 | keyOfIt: state.keyOfIt + 1, 61 | }; 62 | 63 | case types.UPDATE_DATA: 64 | return { 65 | ...state, 66 | expects: action.payload, 67 | }; 68 | 69 | case types.UPDATE_IT_OBJ: 70 | return { 71 | ...state, 72 | its: action.payload, 73 | }; 74 | 75 | case types.UPDATE_KEY_OF_DESCRIBE: 76 | return { 77 | ...state, 78 | keyOfDescribe: state.keyOfDescribe + 1, 79 | }; 80 | 81 | case types.UPDATE_DESCRIBE: 82 | return { 83 | ...state, 84 | describes: action.payload, 85 | }; 86 | 87 | case types.UPDATE_COMPONENT_NAME: 88 | return { 89 | ...state, 90 | componentObj: action.payload, 91 | }; 92 | 93 | case types.UPDATE_IT_STATEMENT: 94 | return { 95 | ...state, 96 | itInputObj: action.payload, 97 | }; 98 | 99 | case types.CLEAR_FILE: 100 | return { 101 | ...state, 102 | codeViewer: false, 103 | codeViewerChecker: false, 104 | fileToView: "", 105 | }; 106 | 107 | case types.DELETE_EXPECT: 108 | const { expects } = state; 109 | const id = action.payload.toString(); 110 | delete expects[`${id}`]; 111 | return { 112 | ...state, 113 | expects, 114 | }; 115 | 116 | case types.REMOVE_FROM_IT: 117 | return { 118 | ...state, 119 | }; 120 | 121 | case types.SET_PROJECT_PATH: 122 | return { 123 | ...state, 124 | filePathOfProject: action.payload, 125 | }; 126 | 127 | case types.REFRESH_STATE: 128 | return { 129 | fileTree: [], 130 | fileToView: "", 131 | toggleFolder: {}, 132 | filePathOfProject: "", 133 | generatedTestCode: "", 134 | 135 | describes: {}, 136 | its: {}, 137 | expects: {}, 138 | 139 | keyOfDescribe: 0, 140 | keyOfIt: 0, 141 | keyOfExpect: 0, 142 | componentObj: {}, 143 | itInputObj: {}, 144 | describePropBoolean: {}, 145 | codeViewer: false, 146 | codeViewerChecker: true, 147 | }; 148 | 149 | case types.UPDATE_DESCRIBE_BOOLEAN: 150 | return { 151 | ...state, 152 | describePropBoolean: action.payload, 153 | }; 154 | 155 | case types.SHOW_TESTCODE: 156 | return { 157 | ...state, 158 | codeViewer: true, 159 | codeViewerChecker: false, 160 | generatedTestCode: action.payload, 161 | }; 162 | 163 | case types.REMOVE_TESTCODE: 164 | return { 165 | ...state, 166 | codeViewer: false, 167 | codeViewerChecker: true, 168 | generatedTestCode: "", 169 | }; 170 | 171 | default: 172 | return state; 173 | } 174 | }; 175 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "catalyst", 3 | "productName": "Catalyst", 4 | "version": "0.0.1", 5 | "description": "Catalyst will simplify the generation of unit tests for React framework with an emphasis on enzyme", 6 | "main": "dist/main.js", 7 | "scripts": { 8 | "dev:electron": "cross-env NODE_ENV=development webpack --config webpack.electron.config.js --mode development && electron .", 9 | "dev:react": "cross-env NODE_ENV=development webpack serve --config webpack.react.config.js --mode development", 10 | "prod:electron": "cross-env NODE_ENV=production webpack --config webpack.electron.config.js --mode production && electron .", 11 | "prod:react": "cross-env NODE_ENV=production webpack --config webpack.react.config.js --mode production", 12 | "dev": " concurrently \"npm run dev:electron\" \"npm run dev:react\"", 13 | "prod": "npm run prod:react && npm run prod:electron", 14 | "test": "jest", 15 | "testall": "jest --converge", 16 | "build-installer": "electron-builder", 17 | "format": "prettier --write src/**/*.{js,jsx,ts,tsx}", 18 | "lint": "eslint --fix src/**/*.{js,jsx,ts,tsx}" 19 | }, 20 | "build": { 21 | "appId": "com.oslabs.catalyst", 22 | "productName": "Catalyst", 23 | "directories": { 24 | "output": "builder", 25 | "buildResources": "assets" 26 | }, 27 | "files": [ 28 | "dist/**/*", 29 | "node_modules/**/*", 30 | "assets/**/*" 31 | ], 32 | "win": { 33 | "target": [ 34 | "nsis" 35 | ], 36 | "icon": "/assets/icon.png" 37 | }, 38 | "nsis": { 39 | "oneClick": false, 40 | "allowToChangeInstallationDirectory": true 41 | }, 42 | "mac": { 43 | "target": [ 44 | "dmg" 45 | ], 46 | "icon": "assets/icon.png", 47 | "category": "public.app-category.Developer-tools", 48 | "identity": null 49 | }, 50 | "dmg": { 51 | "background": "assets/CatalystInstallBG-05.tif", 52 | "icon": "assets/icon.png", 53 | "iconSize": 100, 54 | "window": { 55 | "width": 544, 56 | "height": 408 57 | }, 58 | "contents": [ 59 | { 60 | "x": 105, 61 | "y": 250 62 | }, 63 | { 64 | "x": 440, 65 | "y": 250, 66 | "type": "link", 67 | "path": "/Applications" 68 | } 69 | ] 70 | } 71 | }, 72 | "repository": { 73 | "type": "git", 74 | "url": "git+https://github.com/chungdanny64/Catalyst.git" 75 | }, 76 | "keywords": [], 77 | "author": "Catalyst", 78 | "license": "MIT", 79 | "bugs": { 80 | "url": "https://github.com/chungdanny64/Catalyst/issues" 81 | }, 82 | "dependencies": { 83 | "@babel/runtime": "^7.12.5", 84 | "@types/jest": "^26.0.15", 85 | "@types/react": "^16.9.56", 86 | "@types/react-dom": "^16.9.9", 87 | "@types/react-select": "^3.0.27", 88 | "concurrently": "^5.3.0", 89 | "cross-env": "^7.0.2", 90 | "dotenv": "^8.2.0", 91 | "html-webpack-plugin": "^4.5.0", 92 | "jest": "^26.6.3", 93 | "jquery": "^3.5.1", 94 | "react": "^17.0.1", 95 | "react-dom": "^17.0.1", 96 | "react-redux": "^7.2.2", 97 | "react-select": "^3.1.1", 98 | "redux": "^4.0.5", 99 | "redux-devtools-extension": "^2.13.8", 100 | "typescript": "^4.0.5" 101 | }, 102 | "devDependencies": { 103 | "@babel/core": "^7.12.3", 104 | "@babel/plugin-proposal-class-properties": "^7.12.1", 105 | "@babel/plugin-proposal-object-rest-spread": "^7.12.1", 106 | "@babel/plugin-transform-runtime": "^7.12.1", 107 | "@babel/preset-env": "^7.12.1", 108 | "@babel/preset-react": "^7.12.5", 109 | "@babel/preset-typescript": "^7.12.1", 110 | "@types/enzyme": "^3.10.8", 111 | "@types/jquery": "^3.5.4", 112 | "@types/react-redux": "^7.1.11", 113 | "@wojtekmaj/enzyme-adapter-react-17": "^0.3.1", 114 | "babel-loader": "^8.2.1", 115 | "css-loader": "^5.0.1", 116 | "electron": "^11.0.4", 117 | "electron-builder": "^22.9.1", 118 | "electron-devtools-installer": "^3.1.1", 119 | "enzyme": "^3.11.0", 120 | "enzyme-to-json": "^3.6.1", 121 | "eslint": "^7.15.0", 122 | "eslint-config-airbnb-typescript-prettier": "^4.1.0", 123 | "file-loader": "^6.2.0", 124 | "prettier": "^2.2.1", 125 | "sass": "^1.30.0", 126 | "sass-loader": "^10.1.0", 127 | "style-loader": "^2.0.0", 128 | "ts-jest": "^26.4.4", 129 | "ts-loader": "^8.0.11", 130 | "typings-for-css-modules-loader": "^1.7.0", 131 | "url-loader": "^4.1.1", 132 | "webpack": "^5.10.0", 133 | "webpack-cli": "^4.2.0", 134 | "webpack-dev-server": "^3.11.0" 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/components/FolderUpload.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { remote } from "electron"; 3 | import * as electronFs from "fs"; 4 | import { useDispatch } from "react-redux"; 5 | import { 6 | ConstructFileTree, 7 | SetProjectPath, 8 | } from "../reduxComponents/actions/actions"; 9 | import catalystLogo from "../../assets/catalyst_icons/Catalystfull-02.png"; 10 | 11 | export class FileTree { 12 | filepath: string; 13 | 14 | name: string; 15 | 16 | children: FileTree[]; 17 | 18 | constructor(filepath: string, name: string) { 19 | this.filepath = filepath; 20 | this.name = name; 21 | this.children = []; 22 | } 23 | 24 | // directory will be root directory path 25 | createTree(directory: string): FileTree[] { 26 | let treeElements: FileTree[] = []; 27 | 28 | // readdirSync will return all the names of the files in the directory as an array 29 | electronFs.readdirSync(directory).forEach((fileName: string) => { 30 | if ( 31 | fileName !== ".git" && 32 | fileName !== "node_modules" && 33 | fileName !== "dist" && 34 | fileName !== "assets" && 35 | fileName !== ".DS_Store" && 36 | fileName !== ".eslintrc" && 37 | fileName !== "README.md" && 38 | fileName !== "package-lock.json" && 39 | fileName !== "package.json" && 40 | fileName !== ".gitignore" && 41 | fileName !== "build" && 42 | fileName !== ".vscode" && 43 | fileName !== "webpack.config" 44 | ) { 45 | const fileInfo = new FileTree(`${directory}/${fileName}`, fileName); 46 | 47 | // statsync will return an object with properties of the filepath indicated 48 | const moreFileInfo = electronFs.statSync(fileInfo.filepath); 49 | 50 | // if the file that is currently at is a directory it will append the children to it 51 | if (moreFileInfo.isDirectory()) { 52 | fileInfo.children = fileInfo.createTree(fileInfo.filepath); 53 | } 54 | 55 | treeElements.push(fileInfo); 56 | } 57 | }); 58 | return treeElements; 59 | } 60 | } 61 | 62 | export const FolderUpload: React.FC = () => { 63 | const dispatch = useDispatch(); 64 | // creates dispatch that will send file path as an action payload to reducer 65 | const constructFileTree = (files: any) => dispatch(ConstructFileTree(files)); 66 | const setFilePath = (filePath: any) => dispatch(SetProjectPath(filePath)); 67 | 68 | const { dialog } = remote; 69 | async function uploadFolder() { 70 | // allows users to upload a folder 71 | const Project = await dialog.showOpenDialog( 72 | // sets opendirectory (allows directories to be selected) to be the feature that dialog uses 73 | { 74 | properties: ["openDirectory"], 75 | // the types of files that will be displayed or selected 76 | filters: [ 77 | { name: "Javascript Files", extensions: ["js", "jsx"] }, 78 | { name: "Typescript Files", extensions: ["ts", "tsx"] }, 79 | { name: "HTML Files", extensions: ["html"] }, 80 | ], 81 | message: "Choose a Project to Create Tests for:", 82 | } 83 | ); 84 | 85 | // if the user cancels the action then undefined will be returned 86 | // if the user successfully completes the action then a string array will be returned 87 | // Project is an object that holds canceled (boolean to check if it was cancelled) and filePaths (array of filepaths) 88 | if (Project && !Project.canceled) { 89 | // holds the directory of the project that was selected 90 | let projectDirectory = Project.filePaths[0]; 91 | 92 | // use regex to find all \ in the case of a windows user and replace with / 93 | projectDirectory = projectDirectory.replace(/\\/g, "/"); 94 | 95 | // will create a new FileTree object for the root directory 96 | const rootTree = new FileTree(projectDirectory, "root"); 97 | 98 | // will return an array of FileTree objects along with any children associated with it 99 | const fileTree = rootTree.createTree(projectDirectory); 100 | // dispatches fileTree to reducer 101 | constructFileTree(fileTree); 102 | // setting file path of project to access later by sending projectDirectory to store 103 | setFilePath(projectDirectory); 104 | } 105 | } 106 | 107 | return ( 108 |
109 |
110 | 111 |
112 |
113 |
Make tests quickly
114 |

No coding necessary

115 | 121 |
122 |
123 | ); 124 | }; 125 | -------------------------------------------------------------------------------- /src/components/TestBuilder.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import { DescribeBlock } from "./DescribeBlock"; 4 | import { 5 | UpdateItObj, 6 | UpdateKeyOfDescribe, 7 | UpdateData, 8 | UpdateDescribe, 9 | UpdateDescribeBoolean, 10 | } from "../reduxComponents/actions/actions"; 11 | 12 | export const TestBuilder: React.FC = () => { 13 | const dispatch = useDispatch(); 14 | 15 | const describesFromStore = useSelector((state: any) => state.describes); 16 | const itFromStore = useSelector((state: any) => state.its); 17 | const expectFromStore = useSelector((state: any) => state.expects); 18 | const describeIndex = useSelector((state: any) => state.keyOfDescribe); 19 | const itIndex = useSelector((state: any) => state.keyOfIt); 20 | const describeBool = useSelector((state: any) => state.describePropBoolean); 21 | const fileTree = useSelector((state: any) => state.fileTree); 22 | const propBool = useSelector((state: any) => state.describePropBoolean); 23 | 24 | const updateDescribeIndex = () => dispatch(UpdateKeyOfDescribe()); 25 | const updateIt = (data: any) => dispatch(UpdateItObj(data)); 26 | const updateExpects = (data: any) => dispatch(UpdateData(data)); 27 | const updateDescribe = (data: any) => dispatch(UpdateDescribe(data)); 28 | const updateDescribeBool = (data: any) => 29 | dispatch(UpdateDescribeBoolean(data)); 30 | 31 | const [describes, updateDescribes] = useState([]); 32 | 33 | const storeval: { [k: string]: any } = {}; 34 | storeval[`${itIndex}`] = ""; 35 | 36 | useEffect(() => { 37 | const x: { [k: string]: any } = {}; 38 | // create a new Describe block to be rendered. Will be inital describe 39 | x[`${describeIndex}`] = ( 40 | 47 | ); 48 | // add the describe block to be mapped later 49 | updateDescribes(describes.concat(x[`${describeIndex}`])); 50 | // adds the initial it statement key to the describe object in store to keep track of children components 51 | describesFromStore[`${describeIndex}`] = storeval; 52 | describeBool[`${describeIndex}`] = false; 53 | // updates the index of the it and describe because each has been added to the store 54 | updateDescribeIndex(); 55 | updateDescribeBool(describeBool); 56 | // making fileTree as its dependency in order to re-render describe block if new project is chosen 57 | }, [fileTree]); 58 | 59 | function addDescribe() { 60 | const x: { [k: string]: any } = {}; 61 | x[describeIndex] = ( 62 | 69 | ); 70 | // adds the describe block to the array of describe blocks to be rendered 71 | updateDescribes(describes.concat(x[`${describeIndex}`])); 72 | describesFromStore[`${describeIndex}`] = storeval; 73 | // creates a boolean value set to false within the describe prop boolean object in store 74 | describeBool[`${describeIndex}`] = false; 75 | // update the store 76 | updateDescribeBool(describeBool); 77 | updateDescribeIndex(); 78 | } 79 | 80 | function removeDescribe(id: number): boolean { 81 | // if there are multiple describe blocks then delete current one selected 82 | if (Object.keys(describesFromStore).length > 1) { 83 | // delete all expect and it statements associated with the describe block from the store 84 | for (const it of Object.keys(describesFromStore[`${id}`])) { 85 | for (const expect of Object.keys(itFromStore[`${it}`])) { 86 | delete expectFromStore[`${expect}`]; 87 | } 88 | delete itFromStore[`${it}`]; 89 | updateExpects(expectFromStore); 90 | updateIt(itFromStore); 91 | } 92 | // deletes the describe key from the store in the describes object 93 | delete describesFromStore[`${id}`]; 94 | // deletes the prop boolean key from the store describe prop boolean object 95 | delete propBool[`${id}`]; 96 | updateDescribe(describesFromStore); 97 | // remove the describe block and all its children 98 | document.getElementById(`describeBlock${id}`)?.remove(); 99 | return true; 100 | } 101 | // if there is only one block left then cannot delete 102 | console.log("cannot delete describe block"); 103 | return false; 104 | } 105 | 106 | return ( 107 |
108 |
109 | {describes} 110 |
111 |
112 | 115 |
116 |
117 | ); 118 | }; 119 | -------------------------------------------------------------------------------- /src/utils/globalCodeGenerator.ts: -------------------------------------------------------------------------------- 1 | import * as electronFs from "fs"; 2 | import { store } from "../reduxComponents/store"; 3 | 4 | export function generateTestCode() { 5 | const currentState = store.getState(); 6 | 7 | const describeGlobal = currentState.describes; 8 | const itsGlobal = currentState.its; 9 | const expectGlobal = currentState.expects; 10 | const describeInputGlobal = currentState.componentObj; 11 | const itInputGlobal = currentState.itInputObj; 12 | const { describePropBoolean } = currentState; 13 | const { fileTree } = currentState; 14 | 15 | const keysOfDescribe = Object.keys(describeGlobal); 16 | 17 | function findFile(fileTree: any, name: string): string { 18 | for (const x of fileTree) { 19 | const file = electronFs.statSync(x.filepath); 20 | if (file.isDirectory()) { 21 | const find = findFile(x.children, name); 22 | if (find !== "") { 23 | return find; 24 | } 25 | } else if ( 26 | x.name.toLowerCase().includes(name) && 27 | !x.name.toLowerCase().includes("_") 28 | ) { 29 | return x.filepath; 30 | } 31 | } 32 | return ""; 33 | } 34 | 35 | // console.log(tree) 36 | let finalString = ""; 37 | finalString += `import React from 'react';\nimport { configure, shallow } from 'enzyme';\nimport Adapter from 'enzyme-adapter-react-16';\n\nconfigure({ adapter: new Adapter() });\n\n`; 38 | 39 | // inserts the correct import statement for each component 40 | for (const i of keysOfDescribe) { 41 | let fileLocation = findFile( 42 | fileTree, 43 | `${describeInputGlobal[i]}`.trim().toLowerCase() 44 | ); 45 | if (fileLocation !== "") { 46 | // let relativePath = path.relative(process.cwd(), fileLocation); 47 | // console.log(relativePath, 'this is relativePath'); 48 | fileLocation = fileLocation.replace(".jsx", ""); 49 | finalString += `import ${describeInputGlobal[i]} from \'${fileLocation}\'; \n\n`; 50 | } 51 | } 52 | 53 | // obtains all the prop fields from the dom and puts it into an array of html elements 54 | const allProps: HTMLCollectionOf = document.getElementsByClassName( 55 | "Prop" 56 | ); 57 | // counter for the array of allProps 58 | let counter = 0; 59 | 60 | // loop all the describe blocks 61 | for (const i of keysOfDescribe) { 62 | finalString += `describe('${describeInputGlobal[i]}', () => {\n\tlet wrapper; \n\n`; 63 | 64 | // if the describe block should have props then retrieve it from the allProps object, if no then skip 65 | if ( 66 | describePropBoolean[i] && 67 | allProps[counter].getElementsByClassName("propChild").length > 0 68 | ) { 69 | finalString += `\tconst props = { \n`; 70 | for (const element of allProps[counter].getElementsByClassName( 71 | "propChild" 72 | )) { 73 | if ( 74 | element.getElementsByTagName("input")[0].value === "" && 75 | element.getElementsByTagName("input")[1].value === "" 76 | ) { 77 | continue; 78 | } else { 79 | finalString += `\t\t${ 80 | element.getElementsByTagName("input")[0].value 81 | } : ${element.getElementsByTagName("input")[1].value}, \n`; 82 | } 83 | } 84 | 85 | finalString += `\t}; \n\n`; 86 | finalString += `\tbeforeAll(() => {\n\t\twrapper = shallow(<${describeInputGlobal[i]} {...props}/>);\n \t}); \n`; 87 | } else { 88 | finalString += `\tbeforeAll(() => {\n\t\twrapper = shallow(<${describeInputGlobal[i]}/>)\n \t}); \n`; 89 | } 90 | counter += 1; 91 | // loop through all the it statements that should be within the specidfied describe block 92 | for (const j of Object.keys(describeGlobal[i])) { 93 | finalString += `\n\tit('${itInputGlobal[j]}', () => { \n`; 94 | // loop through all of the expect statements that should be within the specific expect block 95 | for (const expect of Object.keys(itsGlobal[j])) { 96 | finalString += `\t\texpect(wrapper`; 97 | // loop through all of the selectors that exist 98 | for (const element of Object.values(expectGlobal[expect].selectors)) { 99 | // if the selector holds an object then print out the key with the value inside the () 100 | if (typeof element === "object") { 101 | // checking if selector value is empty but still needs to be chained 102 | if (Object.keys(element)[0] && Object.values(element)[0] === "") { 103 | finalString += `${Object.keys(element)[0]}()`; 104 | } else { 105 | finalString += `${Object.keys(element)[0]}(${ 106 | Object.values(element)[0] 107 | })`; 108 | } 109 | } 110 | // if the selector does not hold a string then append the string 111 | else if (element !== ".nothing") { 112 | finalString += `${element}()`; 113 | } 114 | } 115 | finalString += `)${expectGlobal[expect].testTypes}(${ 116 | expectGlobal[expect][`lastInput${expect}`] 117 | });\n`; 118 | } 119 | finalString += "\t});\n"; 120 | } 121 | finalString += "});\n"; 122 | } 123 | 124 | return finalString; 125 | } 126 | -------------------------------------------------------------------------------- /src/components/TestBlock.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useSelector, useDispatch } from "react-redux"; 3 | import * as electronFs from "fs"; 4 | import catalystIcon from "../../assets/catalyst_icons/Catalyst-01.png"; 5 | import { ViewTestCode } from "../reduxComponents/actions/actions"; 6 | import { ReuploadDirectory } from "./ReuploadDirectory"; 7 | import { ExportTestCode } from "./ExportTestCode"; 8 | 9 | export const TestBlock: React.FC = () => { 10 | const describeGlobal = useSelector((state: any) => state.describes); 11 | const itsGlobal = useSelector((state: any) => state.its); 12 | const expectGlobal = useSelector((state: any) => state.expects); 13 | const describeInputGlobal = useSelector((state: any) => state.componentObj); 14 | const itInputGlobal = useSelector((state: any) => state.itInputObj); 15 | const describePropBoolean = useSelector( 16 | (state: any) => state.describePropBoolean 17 | ); 18 | const fileTree = useSelector((state: any) => state.fileTree); 19 | 20 | const dispatch = useDispatch(); 21 | // generated testCode from GUI is dispactched to store to be displayed in viewer 22 | const viewTestCode = (data: string) => dispatch(ViewTestCode(data)); 23 | 24 | function findFile(fileTree: any, name: string): string { 25 | for (const x of fileTree) { 26 | const file = electronFs.statSync(x.filepath); 27 | if (file.isDirectory()) { 28 | const find = findFile(x.children, name); 29 | if (find !== "") { 30 | return find; 31 | } 32 | } else if ( 33 | x.name.toLowerCase().includes(name) && 34 | !x.name.toLowerCase().includes("_") 35 | ) { 36 | return x.filepath; 37 | } 38 | } 39 | return ""; 40 | } 41 | 42 | function handleClick(): void { 43 | const keysOfDescribe = Object.keys(describeGlobal); 44 | 45 | let finalString = ""; 46 | finalString += `import React from 'react';\nimport { configure, shallow } from 'enzyme';\nimport Adapter from 'enzyme-adapter-react-16';\n\nconfigure({ adapter: new Adapter() });\n\n`; 47 | 48 | // inserts the correct import statement for each component 49 | for (const i of keysOfDescribe) { 50 | let fileLocation = findFile( 51 | fileTree, 52 | `${describeInputGlobal[i]}`.trim().toLowerCase() 53 | ); 54 | if (fileLocation !== "") { 55 | fileLocation = fileLocation.replace(".jsx", ""); 56 | finalString += `import ${describeInputGlobal[i]} from \'${fileLocation}\'; \n\n`; 57 | } 58 | } 59 | 60 | // obtains all the prop fields from the dom and puts it into an array of html elements 61 | const allProps: HTMLCollectionOf = document.getElementsByClassName( 62 | "Prop" 63 | ); 64 | // counter for the array of allProps 65 | let counter = 0; 66 | 67 | // loop all the describe blocks 68 | for (const i of keysOfDescribe) { 69 | finalString += `describe('${describeInputGlobal[i]}', () => {\n\tlet wrapper; \n\n`; 70 | 71 | // if the describe block should have props then retrieve it from the allProps object, if no then skip 72 | if ( 73 | describePropBoolean[i] && 74 | allProps[counter].getElementsByClassName("propChild").length > 0 75 | ) { 76 | finalString += `\tconst props = { \n`; 77 | for (const element of allProps[counter].getElementsByClassName( 78 | "propChild" 79 | )) { 80 | if ( 81 | element.getElementsByTagName("input")[0].value === "" && 82 | element.getElementsByTagName("input")[1].value === "" 83 | ) { 84 | continue; 85 | } else { 86 | finalString += `\t\t${ 87 | element.getElementsByTagName("input")[0].value 88 | } : ${element.getElementsByTagName("input")[1].value}, \n`; 89 | } 90 | } 91 | 92 | finalString += `\t}; \n\n`; 93 | finalString += `\tbeforeAll(() => {\n\t\twrapper = shallow(<${describeInputGlobal[i]} {...props}/>);\n \t}); \n`; 94 | } else { 95 | finalString += `\tbeforeAll(() => {\n\t\twrapper = shallow(<${describeInputGlobal[i]}/>)\n \t}); \n`; 96 | } 97 | counter += 1; 98 | // loop through all the it statements that should be within the specidfied describe block 99 | for (const j of Object.keys(describeGlobal[i])) { 100 | finalString += `\n\tit('${itInputGlobal[j]}', () => { \n`; 101 | // loop through all of the expect statements that should be within the specific expect block 102 | for (const expect of Object.keys(itsGlobal[j])) { 103 | finalString += `\t\texpect(wrapper`; 104 | // loop through all of the selectors that exist 105 | for (const element of Object.values(expectGlobal[expect].selectors)) { 106 | // if the selector holds an object then print out the key with the value inside the () 107 | if (typeof element === "object") { 108 | // checking if selector value is empty but still needs to be chained 109 | if (Object.keys(element)[0] && Object.values(element)[0] === "") { 110 | finalString += `${Object.keys(element)[0]}()`; 111 | } else { 112 | finalString += `${Object.keys(element)[0]}(${ 113 | Object.values(element)[0] 114 | })`; 115 | } 116 | } 117 | // if the selector does not hold a string then append the string 118 | else if (element !== ".nothing") { 119 | finalString += `${element}()`; 120 | } 121 | } 122 | finalString += `)${expectGlobal[expect].testTypes}(${ 123 | expectGlobal[expect][`lastInput${expect}`] 124 | });\n`; 125 | } 126 | finalString += "\t});\n"; 127 | } 128 | finalString += "});\n"; 129 | } 130 | // dispatches generated test code to store for global usage 131 | viewTestCode(finalString); 132 | } 133 | 134 | return ( 135 |
136 | 137 |
138 |
    139 |
  • 140 | 141 |
  • 142 |
  • 143 | 146 |
  • 147 |
  • 148 | 149 |
  • 150 |
151 | 180 |
181 |
182 | ); 183 | }; 184 | -------------------------------------------------------------------------------- /src/components/ExpectStatement.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { 4 | UpdateData, 5 | UpdateKeyOfExpect, 6 | deleteExpect, 7 | } from "../reduxComponents/actions/actions"; 8 | 9 | interface Props { 10 | id: string; 11 | remove: (x: number) => boolean; 12 | } 13 | 14 | export const ExpectStatement: React.FC = ({ id, remove }) => { 15 | const dispatch = useDispatch(); 16 | let counter = 0; 17 | let first: number; 18 | const data = useSelector((state: any) => state.expects); 19 | const index = useSelector((state: any) => state.keyOfExpect); 20 | 21 | const updateData = (data: any) => dispatch(UpdateData(data)); 22 | const updateExpectKey = () => dispatch(UpdateKeyOfExpect()); 23 | const deleteExpectFromStore = (data: any) => dispatch(deleteExpect(data)); 24 | 25 | useEffect(() => { 26 | data[index] = {}; 27 | data[index].testTypes = ".toEqual"; 28 | data[index][`lastInput${index}`] = ""; 29 | data[index].selectors = {}; 30 | data[index].selectors[`expect${index}selector0`] = ".type"; 31 | updateData(data); 32 | updateExpectKey(); 33 | }, []); 34 | 35 | // removes the expect block selected 36 | function removeExpect() { 37 | // uses remove prop from ItStatement componene to see if the user is able to delete 38 | if (remove(parseInt(id, 10))) { 39 | deleteExpectFromStore(id); 40 | document.getElementById(`expect-block ${id}`)?.remove(); 41 | } 42 | // if there is only one expect block then do not delete 43 | else { 44 | // eslint-disable-next-line no-console 45 | console.log("cannot remove expect block"); 46 | } 47 | } 48 | 49 | // when a selector is changed then create new fields 50 | function handleChange( 51 | event: 52 | | React.ChangeEvent 53 | | React.ChangeEvent 54 | ) { 55 | const block = document.getElementById(id); 56 | // if the selector is set to find then append a new text box and selector 57 | if ( 58 | event.target?.value === ".find" || 59 | event.target?.value === ".contains" || 60 | event.target?.value === ".every" || 61 | event.target?.value === ".everyWhere" || 62 | event.target?.value === ".hasClass" || 63 | event.target?.value === ".exists" || 64 | event.target?.value === ".forEach" || 65 | event.target?.value === ".is" || 66 | event.target?.value === ".at" || 67 | event.target?.value === ".simulate" || 68 | event.target?.value === ".prop" || 69 | event.target?.value === ".tap" || 70 | event.target?.value === ".some" || 71 | event.target?.value === ".name" || 72 | event.target?.value === ".isEmptyRender" || 73 | event.target?.value === ".first" || 74 | event.target?.value === ".get" || 75 | event.target?.value === ".getElements" || 76 | event.target?.value === ".hostNodes" 77 | ) { 78 | if (event.target?.id === `expect${id}selector0`) { 79 | first = counter; 80 | } 81 | // will keep value of prop key if user wants to change key again 82 | if (typeof data[`${id}`].selectors[`${event.target?.id}`] === "object") { 83 | const value = Object.values( 84 | data[`${id}`].selectors[`${event.target?.id}`] 85 | )[0]; 86 | data[`${id}`].selectors[`${event.target?.id}`] = {}; 87 | data[`${id}`].selectors[`${event.target?.id}`][ 88 | `${event.target?.value}` 89 | ] = value; 90 | } else { 91 | data[`${id}`].selectors[`${event.target?.id}`] = {}; 92 | data[`${id}`].selectors[`${event.target?.id}`][ 93 | `${event.target?.value}` 94 | ] = ""; 95 | // creates a text box for the input 96 | const child = document.createElement("input"); 97 | child.id = `expect${id}input${counter}`; 98 | child.className = "inputbox"; 99 | child.type = "text"; 100 | child.placeholder = "Selector optional"; 101 | 102 | child.onchange = () => { 103 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 104 | inputText(event.target?.id, event.target?.value); 105 | }; 106 | 107 | // eslint-disable-next-line no-plusplus 108 | counter++; 109 | // creates a selector for the next portion 110 | const secondSelector = document.createElement("select"); 111 | secondSelector.id = `expect${id}selector${counter}`; 112 | secondSelector.className = "expectdrop1"; 113 | secondSelector.innerHTML = ` 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | `; 136 | data[`${id}`].selectors[`expect${id}selector${counter}`] = ".nothing"; 137 | secondSelector.onchange = handleChange; 138 | 139 | // create line break 140 | const linebreak = document.createElement("br"); 141 | 142 | if (block) { 143 | block.appendChild(child); 144 | block.appendChild(linebreak); 145 | block.appendChild(secondSelector); 146 | } 147 | } 148 | } 149 | // select a new option for the selector 150 | else { 151 | const breaks = document.getElementsByTagName("br"); 152 | let breaksLength = breaks.length; 153 | data[`${id}`].selectors[`${event.target?.id}`] = event.target?.value; 154 | let checker = false; 155 | // if the value changed was for the first selector 156 | if (event.target?.id === `expect${id}selector0`) { 157 | document.getElementById(`expect${id}input${first}`)?.remove(); 158 | // eslint-disable-next-line no-plusplus 159 | breaks[breaksLength--]?.remove(); 160 | } 161 | 162 | // delete all elements that were appended after the selector chosen 163 | for (const keys of Object.keys(data[`${id}`].selectors)) { 164 | // checker will be true when the correct identifier is found 165 | // all values after that identifier will be removed from the store 166 | if (checker) { 167 | delete data[`${id}`].selectors[`${keys}`]; 168 | document.getElementById(`${keys}`)?.remove(); 169 | document 170 | .getElementById(`${keys}`.replace("selector", "input")) 171 | ?.remove(); 172 | // eslint-disable-next-line no-plusplus 173 | breaks[breaksLength--]?.remove(); 174 | // eslint-disable-next-line no-plusplus 175 | counter--; 176 | } else if (keys === event.target.id) { 177 | checker = true; 178 | document 179 | .getElementById(`${keys}`.replace("selector", "input")) 180 | ?.remove(); 181 | // eslint-disable-next-line no-plusplus 182 | breaks[breaksLength--]?.remove(); 183 | } 184 | } 185 | } 186 | // updates the elements in the store 187 | updateData(data); 188 | } 189 | 190 | // updates the last input box in the store 191 | function updateInput( 192 | event: 193 | | React.ChangeEvent 194 | | React.ChangeEvent 195 | ) { 196 | data[`${id}`][`${event.target?.id}`] = event.target?.value; 197 | updateData(data); 198 | } 199 | 200 | // updates the last selector value (expectdrop2) in the store 201 | function updateLastSelector(event: any) { 202 | data[`${id}`][`${event.target?.id}`] = event.target?.value; 203 | updateData(data); 204 | } 205 | 206 | // sets the input box text as the value of the object associated with the selector 207 | function inputText(elementId: string, elementKey: string) { 208 | const text = (document.getElementById( 209 | elementId.replace("selector", "input") 210 | ) as HTMLInputElement).value; 211 | data[`${id}`].selectors[`${elementId}`][`${elementKey}`] = text; 212 | updateData(data); 213 | } 214 | 215 | return ( 216 |
217 |
218 | 219 | 222 |
223 | 251 |
252 | 253 |
254 | 264 | 265 |
266 |
267 | ); 268 | }; 269 | --------------------------------------------------------------------------------