├── .babelrc ├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── babel.config.js ├── contributing.md ├── jest.config.ts ├── jest.setup.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── App.tsx │ ├── components │ │ ├── Editor.css │ │ ├── Editor.jsx │ │ ├── ExaminPanel-Style.tsx │ │ ├── ExaminPanel.tsx │ │ └── Howto.tsx │ └── index.tsx ├── backend │ ├── injected.ts │ ├── testGenerator.ts │ ├── treeTraversal.ts │ └── types.d.ts ├── extension │ ├── assets │ │ ├── 128.png │ │ ├── 148.png │ │ ├── 16.png │ │ ├── 192.png │ │ ├── 38.png │ │ ├── 48.png │ │ ├── edit-import-statements.gif │ │ ├── examin-small.png │ │ ├── examin-small.svg │ │ ├── failing-test-sample.png │ │ ├── githubcover-img.png │ │ ├── main.png │ │ ├── state-changes-component-selection.gif │ │ ├── step1.gif │ │ ├── step2.gif │ │ └── step3.gif │ ├── background.js │ ├── content.js │ ├── devtools.html │ ├── devtools.js │ ├── manifest.json │ └── panel.html └── tests │ ├── newest.test.js │ └── test.js ├── tests ├── enzyme.test.js └── extension-tests.js ├── tsconfig.json └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaFeatures": { 15 | "jsx": true 16 | }, 17 | "ecmaVersion": 12, 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "react", 22 | "@typescript-eslint" 23 | ], 24 | "rules": { 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /src/extension/bundles 2 | .vscode 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.activeBackground": "#758699", 4 | "activityBar.activeBorder": "#d6c8cf", 5 | "activityBar.background": "#758699", 6 | "activityBar.foreground": "#15202b", 7 | "activityBar.inactiveForeground": "#15202b99", 8 | "activityBarBadge.background": "#d6c8cf", 9 | "activityBarBadge.foreground": "#15202b", 10 | "statusBar.background": "#5d6d7e", 11 | "statusBar.foreground": "#e7e7e7", 12 | "statusBarItem.hoverBackground": "#758699", 13 | "titleBar.activeBackground": "#5d6d7e", 14 | "titleBar.activeForeground": "#e7e7e7", 15 | "titleBar.inactiveBackground": "#5d6d7e99", 16 | "titleBar.inactiveForeground": "#e7e7e799" 17 | }, 18 | "peacock.color": "#5D6D7E" 19 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 OSLabs Beta 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Examin Cover 2 | 3 |
4 |

Examin

5 | Automatic React Unit Test Generator
6 | examin.dev | Install Examin 7 |
8 | 9 |
10 | 11 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebook/react/blob/master/LICENSE) 12 | [![Version](https://badge.fury.io/gh/tterb%2FHyde.svg)]() 13 | [![GitHub Release](https://img.shields.io/static/v1?label=release&message=1.0.0.1&color=brightgreen)]() 14 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)]() 15 | [![GitHub contributors](https://img.shields.io/static/v1?label=contributers&message=4&color=yellowgreen)]() 16 | 17 | ## Features ⚡ 18 | 19 | Examin is a developer tool that generates React unit tests for your application. Ensure your application renders as expected before adding new features. Examin writes the baseline unit tests and allows developers to customize their tests for their application. 20 | 21 | - Install the Examin extension 22 | - Install npm dependencies for Jest/Enzyme 23 | - Navigate to the Examin panel in Chrome developer tools 24 | - ✨ Generate tests ✨ 25 | 26 | Preview of Examin 27 | 28 | ## Installation 🔌 29 | 30 | To get started, manually install Examin in Developer mode. 31 | 32 | 1. Clone the repo
33 | `git clone https://github.com/oslabs-beta/Examin.git` 34 | 2. Install NPM packages
35 | `npm install` 36 | 3. Create a build directory
37 | `npm run build` 38 | 4. Load the unpacked extension from src/extension to Chrome 39 | 40 | NOTE: The React Developer Tools extension is also required for Examin to run, if you do not already have it installed on your browser. 41 | 42 | ## How to Use ❓️ 43 | 44 | 1. Install Jest/Enzyme for your project 45 | 46 | - `npm install jest enzyme enzyme-adapter-react-16 @babel/core @babel/preset-env` 47 | - Add presets to your `.babelrc` file
48 | `{ "presets": ["@babel/preset-env", "@babel/preset-react"] }` 49 | 50 | 2. Run the Examin build using npm run dev 51 | 52 | 3. Navigate to the Examin panel in Chrome DevTools 53 | 54 | - Must be in developer mode 55 | - Revise import statements as needed 56 | 57 | 4. Add Generated tests into your application 58 | - Add `__tests__` directory in root directory 59 | - Add test js file to `__tests__` directory 60 | - Run tests using `jest` or `jest ` 61 | 62 |
Editing Import Statements Demo Gif 63 | 64 | - Editing import statements 65 | 66 |
State Change and Component Selection Demo Gif 67 | 68 | - Updating tests with state changes 69 | - Selecting components for generated tests 70 | 71 | ## Compatibility 72 | 73 | - Requires React v16.8.0 or higher 74 | - Functional components + React hooks 75 | - Not yet compatible with component libraries or Context API 76 | 77 | ## Troubleshooting ⁉️ 78 | 79 | - [Jest docs](https://jestjs.io/docs/getting-started) 80 | - [Enzyme docs](https://enzymejs.github.io/enzyme/) 81 | - Error: Unable to resolve dependency tree while installing `enzyme-adapter-react-16` 82 | - Add peerDependencies to your **package.json** file 83 | ```sh 84 | "peerDependencies": { "react": "^16.8.0 || ^17.0.0", "react-dom": "^16.8.0 || ^17.0.0" } 85 | ``` 86 | 87 | ## Contributing 88 | 89 | Examin is open source on Github through the tech accelerator umbrella OS Labs. Please read the [contribution documentation](./contributing.md) to learn more on how you can participate in improvements. 90 | 91 | ## Core Team ☕ 💼 92 | 93 | - **Cliff Assuncao** - [@Github](https://github.com/WizardSource) - [@LinkedIn](https://www.linkedin.com/in/cliff-assuncao-1b2593211/) 94 | - **Kirsten Yoon** - [@Github](https://github.com/kirstenyoon) - [@LinkedIn](http://linkedin.com/in/kirstenyoon) 95 | - **Nicholas Brush** - [@Github](https://github.com/Njbrush) - [@LinkedIn](https://www.linkedin.com/in/nicholas-j-brush/) 96 | - **Ty Doan** - [@Github](https://github.com/tdoan35) - [@LinkedIn](https://www.linkedin.com/in/ty-thanh-doan/) 97 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | '@babel/preset-react', 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Exmain encourages open source contributions and feedback to this product. 4 | 5 | ## Pull Requests 6 | 7 | 1. Fork the repo and create a working branch from main. 8 | 2. If you've added any code that requires testing, add tests. 9 | 3. Esnure code follows the Airbnb React/JSX Style Guide. 10 | 4. Create a pull request to staging. 11 | 12 | ## Issues 13 | 14 | Examin is utilizes community feedback and is always looking to optimize the developer and user experience. The team behind Exmain is interested in hearing about your experience and how we can improve it. 15 | 16 | When submitting issues, ensure your description is clear and has instructions to be able to reproduce the issue. 17 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import register from 'ignore-styles'; 2 | register(['.css', '.sass', '.scss']); 3 | 4 | /* 5 | * For a detailed explanation regarding each configuration property and type check, visit: 6 | * https://jestjs.io/docs/en/configuration.html 7 | */ 8 | 9 | export default { 10 | setupFilesAfterEnv: ['./jest.setup.js'], 11 | 12 | // All imported modules in your tests should be mocked automatically 13 | // automock: false, 14 | 15 | // Stop running tests after `n` failures 16 | // bail: 0, 17 | 18 | // The directory where Jest should store its cached dependency information 19 | // cacheDirectory: "/private/var/folders/fp/xzh030y92pqc9nkh6vktr25c0000gp/T/jest_dy", 20 | 21 | // Automatically clear mock calls and instances between every test 22 | clearMocks: true, 23 | 24 | // Indicates whether the coverage information should be collected while executing the test 25 | // collectCoverage: false, 26 | 27 | // An array of glob patterns indicating a set of files for which coverage information should be collected 28 | // collectCoverageFrom: undefined, 29 | 30 | // The directory where Jest should output its coverage files 31 | // coverageDirectory: undefined, 32 | 33 | // An array of regexp pattern strings used to skip coverage collection 34 | // coveragePathIgnorePatterns: [ 35 | // "/node_modules/" 36 | // ], 37 | 38 | // Indicates which provider should be used to instrument code for coverage 39 | coverageProvider: 'v8', 40 | 41 | // A list of reporter names that Jest uses when writing coverage reports 42 | // coverageReporters: [ 43 | // "json", 44 | // "text", 45 | // "lcov", 46 | // "clover" 47 | // ], 48 | 49 | // An object that configures minimum threshold enforcement for coverage results 50 | // coverageThreshold: undefined, 51 | 52 | // A path to a custom dependency extractor 53 | // dependencyExtractor: undefined, 54 | 55 | // Make calling deprecated APIs throw helpful error messages 56 | // errorOnDeprecated: false, 57 | 58 | // Force coverage collection from ignored files using an array of glob patterns 59 | // forceCoverageMatch: [], 60 | 61 | // A path to a module which exports an async function that is triggered once before all test suites 62 | // globalSetup: undefined, 63 | 64 | // A path to a module which exports an async function that is triggered once after all test suites 65 | // globalTeardown: undefined, 66 | 67 | // A set of global variables that need to be available in all test environments 68 | // globals: {}, 69 | 70 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 71 | // maxWorkers: "50%", 72 | 73 | // An array of directory names to be searched recursively up from the requiring module's location 74 | // moduleDirectories: [ 75 | // "node_modules" 76 | // ], 77 | 78 | // An array of file extensions your modules use 79 | // moduleFileExtensions: [ 80 | // "js", 81 | // "json", 82 | // "jsx", 83 | // "ts", 84 | // "tsx", 85 | // "node" 86 | // ], 87 | 88 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 89 | // moduleNameMapper: {}, 90 | 91 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 92 | // modulePathIgnorePatterns: [], 93 | 94 | // Activates notifications for test results 95 | // notify: false, 96 | 97 | // An enum that specifies notification mode. Requires { notify: true } 98 | // notifyMode: "failure-change", 99 | 100 | // A preset that is used as a base for Jest's configuration 101 | // preset: undefined, 102 | 103 | // Run tests from one or more projects 104 | // projects: undefined, 105 | 106 | // Use this configuration option to add custom reporters to Jest 107 | // reporters: undefined, 108 | 109 | // Automatically reset mock state between every test 110 | // resetMocks: false, 111 | 112 | // Reset the module registry before running each individual test 113 | // resetModules: false, 114 | 115 | // A path to a custom resolver 116 | // resolver: undefined, 117 | 118 | // Automatically restore mock state between every test 119 | // restoreMocks: false, 120 | 121 | // The root directory that Jest should scan for tests and modules within 122 | // rootDir: undefined, 123 | 124 | // A list of paths to directories that Jest should use to search for files in 125 | // roots: [ 126 | // "" 127 | // ], 128 | 129 | // Allows you to use a custom runner instead of Jest's default test runner 130 | // runner: "jest-runner", 131 | 132 | // The paths to modules that run some code to configure or set up the testing environment before each test 133 | // setupFiles: [], 134 | 135 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 136 | // setupFilesAfterEnv: [], 137 | 138 | // The number of seconds after which a test is considered as slow and reported as such in the results. 139 | // slowTestThreshold: 5, 140 | 141 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 142 | // snapshotSerializers: [], 143 | 144 | // The test environment that will be used for testing 145 | // testEnvironment: "node", 146 | testEnvironment: 'jsdom', 147 | 148 | // Options that will be passed to the testEnvironment 149 | // testEnvironmentOptions: {}, 150 | 151 | // Adds a location field to test results 152 | // testLocationInResults: false, 153 | 154 | // The glob patterns Jest uses to detect test files 155 | // testMatch: [ 156 | // "**/__tests__/**/*.[jt]s?(x)", 157 | // "**/?(*.)+(spec|test).[tj]s?(x)" 158 | // ], 159 | 160 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 161 | // testPathIgnorePatterns: [ 162 | // "/node_modules/" 163 | // ], 164 | 165 | // The regexp pattern or array of patterns that Jest uses to detect test files 166 | // testRegex: [], 167 | 168 | // This option allows the use of a custom results processor 169 | // testResultsProcessor: undefined, 170 | 171 | // This option allows use of a custom test runner 172 | // testRunner: "jasmine2", 173 | 174 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 175 | // testURL: "http://localhost", 176 | 177 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 178 | // timers: "real", 179 | 180 | // A map from regular expressions to paths to transformers 181 | // transform: undefined, 182 | 183 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 184 | // transformIgnorePatterns: [ 185 | // "/node_modules/", 186 | // "\\.pnp\\.[^\\/]+$" 187 | // ], 188 | 189 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 190 | // unmockedModulePathPatterns: undefined, 191 | 192 | // Indicates whether each individual test should be reported during the run 193 | // verbose: undefined, 194 | 195 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 196 | // watchPathIgnorePatterns: [], 197 | 198 | // Whether to use watchman for file crawling 199 | // watchman: true, 200 | }; 201 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | Object.assign(global, require('jest-chrome')); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examin", 3 | "version": "0.0.1", 4 | "description": "An easy way to unit test React apps", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "webpack --mode production", 9 | "dev": "webpack --config webpack.config.js --mode=development --watch " 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/tdoan35/TyKirstenNickCliff.git" 14 | }, 15 | "author": "Cliff Assucnao, Kirsten Yoon, Nicholas Brush, Ty Doan", 16 | "license": "ISC", 17 | "bugs": { 18 | "url": "https://github.com/tdoan35/TyKirstenNickCliff/issues" 19 | }, 20 | "homepage": "https://github.com/tdoan35/TyKirstenNickCliff#readme", 21 | "devDependencies": { 22 | "@babel/core": "^7.14.0", 23 | "@babel/preset-env": "^7.14.1", 24 | "@babel/preset-react": "^7.13.13", 25 | "@types/chrome": "0.0.135", 26 | "@types/codemirror": "0.0.109", 27 | "@typescript-eslint/eslint-plugin": "^4.22.0", 28 | "@typescript-eslint/parser": "^4.22.0", 29 | "babel-loader": "^8.2.2", 30 | "css-loader": "^5.2.4", 31 | "deep-object-diff": "^1.1.0", 32 | "eslint": "^7.26.0", 33 | "eslint-config-airbnb": "^18.2.1", 34 | "eslint-plugin-import": "^2.22.1", 35 | "eslint-plugin-jsx-a11y": "^6.4.1", 36 | "eslint-plugin-react": "^7.23.2", 37 | "file-loader": "^6.2.0", 38 | "html-loader": "^2.1.2", 39 | "ignore-styles": "^5.0.1", 40 | "jest": "^26.6.3", 41 | "jest-css-modules": "^2.1.0", 42 | "raw-loader": "^4.0.2", 43 | "sass-loader": "^11.0.1", 44 | "sinon-chrome": "^3.0.1", 45 | "style-loader": "^2.0.0", 46 | "ts-loader": "^9.1.0", 47 | "typescript": "^4.2.4", 48 | "webpack": "^5.35.0", 49 | "webpack-chrome-extension-reloader": "^1.3.0", 50 | "webpack-cli": "^4.6.0" 51 | }, 52 | "dependencies": { 53 | "@babel/preset-typescript": "^7.13.0", 54 | "@material-ui/core": "^4.11.3", 55 | "@material-ui/icons": "^4.11.2", 56 | "@types/react": "^17.0.3", 57 | "@types/react-codemirror": "^1.0.3", 58 | "@uiw/react-codemirror": "^3.0.6", 59 | "@uiw/react-markdown-preview": "^3.0.6", 60 | "codemirror": "^5.61.0", 61 | "copy-to-clipboard": "^3.3.1", 62 | "enzyme": "^3.11.0", 63 | "enzyme-adapter-react-16": "^1.15.6", 64 | "jest-chrome": "^0.7.1", 65 | "react": "^16.8.0 || ^17.0.0", 66 | "react-codemirror2": "^7.2.1", 67 | "react-dom": "^16.8.0 || ^17.0.0", 68 | "react-download-link": "^2.3.0", 69 | "react-downloader-file": "^1.0.0", 70 | "ts-node": "^9.1.1" 71 | }, 72 | "peerDependencies": { 73 | "react": "^16.8.0 || ^17.0.0", 74 | "react-dom": "^16.8.0 || ^17.0.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/app/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ThemeProvider, createMuiTheme } from '@material-ui/core/styles'; 3 | import { CssBaseline } from '@material-ui/core'; 4 | 5 | import ExaminPanel from './components/ExaminPanel' 6 | 7 | const App = () => { 8 | 9 | const theme = createMuiTheme({ 10 | palette: { 11 | primary: { 12 | main: '#272839', 13 | }, 14 | secondary: { 15 | light: '#0C4B40', 16 | main: '#50fa7b', 17 | } 18 | } 19 | }); 20 | 21 | return ( 22 |
23 | 24 | 25 | 26 | 27 |
28 | ) 29 | } 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /src/app/components/Editor.css: -------------------------------------------------------------------------------- 1 | .CodeMirror { 2 | height: auto; 3 | border-radius: 5px; 4 | font-size: 12px; 5 | margin-bottom: 75px; 6 | } -------------------------------------------------------------------------------- /src/app/components/Editor.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import 'codemirror/lib/codemirror.css'; 3 | import 'codemirror/theme/dracula.css'; 4 | import 'codemirror/mode/xml/xml'; 5 | import 'codemirror/mode/javascript/javascript'; 6 | import 'codemirror/mode/css/css'; 7 | import { Controlled as ControlledEditor } from 'react-codemirror2'; 8 | import './Editor.css'; 9 | 10 | // import CodeMirror from '@uiw/react-codemirror'; 11 | 12 | // import { makeStyles } from '@material-ui/core/styles'; 13 | 14 | export default function Editor(props) { 15 | const { language, displayName, value, onChange } = props; 16 | // const [open, setOpen] = useState(true) 17 | 18 | function handleChange(editor, data, value) { 19 | // onChange === setCode(value) where value === code 20 | onChange(value); 21 | } 22 | 23 | return ( 24 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/app/components/ExaminPanel-Style.tsx: -------------------------------------------------------------------------------- 1 | import { makeStyles, Theme, useTheme } from '@material-ui/core/styles'; 2 | 3 | const drawerWidth = 230; 4 | 5 | const useStyles = makeStyles((theme: Theme) => ({ 6 | root: { 7 | flexGrow: 1, 8 | backgroundColor: theme.palette.background.paper, 9 | }, 10 | appBar: { 11 | transition: theme.transitions.create(['margin', 'width'], { 12 | easing: theme.transitions.easing.sharp, 13 | duration: theme.transitions.duration.leavingScreen, 14 | }), 15 | zIndex: theme.zIndex.drawer + 1, 16 | }, 17 | appBarShift: { 18 | // width: `calc(100% - ${drawerWidth}px)`, 19 | transition: theme.transitions.create(['margin', 'width'], { 20 | easing: theme.transitions.easing.easeOut, 21 | duration: theme.transitions.duration.enteringScreen, 22 | }), 23 | // marginRight: drawerWidth, 24 | zIndex: theme.zIndex.drawer + 1, 25 | }, 26 | content: { 27 | flexGrow: 1, 28 | transition: theme.transitions.create('margin', { 29 | easing: theme.transitions.easing.sharp, 30 | duration: theme.transitions.duration.leavingScreen, 31 | }), 32 | backgroundColor: theme.palette.background.paper, 33 | paddingTop: theme.spacing(5), 34 | paddingLeft: theme.spacing(0), 35 | paddingRight: theme.spacing(0), 36 | paddingBottom: theme.spacing(0), 37 | }, 38 | contentShift: { 39 | transition: theme.transitions.create('margin', { 40 | easing: theme.transitions.easing.easeOut, 41 | duration: theme.transitions.duration.enteringScreen, 42 | }), 43 | backgroundColor: theme.palette.background.paper, 44 | marginRight: drawerWidth, 45 | }, 46 | drawer: { 47 | width: drawerWidth, 48 | flexShrink: 0, 49 | }, 50 | drawerPaper: { 51 | marginTop: 50, 52 | width: drawerWidth, 53 | }, 54 | drawerHeader: { 55 | display: 'flex', 56 | alignItems: 'center', 57 | padding: theme.spacing(0, 1), 58 | // necessary for content to be below app bar 59 | ...theme.mixins.toolbar, 60 | justifyContent: 'flex-start', 61 | }, 62 | codeEditor: { 63 | transition: theme.transitions.create('margin', { 64 | easing: theme.transitions.easing.sharp, 65 | duration: theme.transitions.duration.leavingScreen, 66 | }), 67 | }, 68 | codeEditorShift: { 69 | transition: theme.transitions.create('margin', { 70 | easing: theme.transitions.easing.easeOut, 71 | duration: theme.transitions.duration.enteringScreen, 72 | }), 73 | }, 74 | extendedIcon: { 75 | marginRight: theme.spacing(1), 76 | }, 77 | hide: { 78 | display: 'none', 79 | }, 80 | logo: { 81 | width: 70, 82 | height: 27, 83 | }, 84 | copyBtn: { 85 | margin: 0, 86 | top: 'auto', 87 | right: 150, 88 | bottom: 20, 89 | left: 'auto', 90 | position: 'fixed', 91 | zIndex: theme.zIndex.drawer + 2, 92 | }, 93 | exportBtn: { 94 | margin: 0, 95 | top: 'auto', 96 | right: 20, 97 | bottom: 20, 98 | left: 'auto', 99 | position: 'fixed', 100 | zIndex: theme.zIndex.drawer + 2, 101 | }, 102 | recordBtn: { 103 | position: 'fixed', 104 | margin: 0, 105 | top: 'auto', 106 | bottom: 15, 107 | left: 20, 108 | right: 'auto', 109 | zIndex: theme.zIndex.drawer + 2, 110 | }, 111 | prevBtn: { 112 | position: 'fixed', 113 | margin: 0, 114 | top: 'auto', 115 | bottom: 20, 116 | left: 20, 117 | right: 'auto', 118 | zIndex: theme.zIndex.drawer + 2, 119 | }, 120 | nextBtn: { 121 | position: 'fixed', 122 | margin: 0, 123 | top: 'auto', 124 | bottom: 20, 125 | left: 130, 126 | right: 'auto', 127 | zIndex: theme.zIndex.drawer + 2, 128 | }, 129 | btnContainer: { 130 | position: 'fixed', 131 | bottom: 0, 132 | height: 75, 133 | width: '100%', 134 | borderTop: '1px solid #dedede', 135 | backgroundColor: theme.palette.background.paper, 136 | zIndex: theme.zIndex.drawer + 1, 137 | }, 138 | rootDirInput: { 139 | marginRight: 10, 140 | marginBottom: 10, 141 | transition: theme.transitions.create('margin', { 142 | easing: theme.transitions.easing.sharp, 143 | duration: theme.transitions.duration.leavingScreen, 144 | }), 145 | }, 146 | rootDirInputShift: { 147 | marginRight: 10, 148 | marginBottom: 10, 149 | transition: theme.transitions.create('margin', { 150 | easing: theme.transitions.easing.easeOut, 151 | duration: theme.transitions.duration.enteringScreen, 152 | }), 153 | }, 154 | copyText: { 155 | padding: theme.spacing(2), 156 | } 157 | })); 158 | 159 | 160 | export { useStyles }; -------------------------------------------------------------------------------- /src/app/components/ExaminPanel.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import clsx from "clsx"; 3 | import { useTheme } from "@material-ui/core/styles"; 4 | import { 5 | AppBar, 6 | Box, 7 | Checkbox, 8 | Drawer, 9 | Divider, 10 | IconButton, 11 | Fab, 12 | Grid, 13 | List, 14 | ListItem, 15 | ListItemText, 16 | ListItemSecondaryAction, 17 | Tabs, 18 | Tab, 19 | Typography, 20 | TextField, 21 | Button, 22 | Popover, 23 | } from "@material-ui/core"; 24 | import { ChevronLeft, ChevronRight } from "@material-ui/icons"; 25 | import PauseIcon from "@material-ui/icons/Pause"; 26 | import FileCopyIcon from "@material-ui/icons/FileCopy"; 27 | import ListItemIcon from "@material-ui/core/ListItemIcon"; 28 | import FiberManualRecordIcon from "@material-ui/icons/FiberManualRecord"; 29 | import GetAppIcon from "@material-ui/icons/GetApp"; 30 | import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown"; 31 | import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp"; 32 | import Howto from "./Howto"; 33 | import Editor from "./Editor"; 34 | 35 | import copy from "copy-to-clipboard"; 36 | 37 | import { useStyles } from "./ExaminPanel-Style"; 38 | 39 | function TabPanel(props: any) { 40 | const { children, value, index, ...other } = props; 41 | 42 | return ( 43 | 56 | ); 57 | } 58 | 59 | const ExaminPanel = () => { 60 | // MaterialUI Styling Hook --------------------------------------- 61 | const classes = useStyles(); 62 | const theme = useTheme(); 63 | // --------------------------------------------------------------- 64 | 65 | // Stateful functions for handling Tab Logic --------------------- 66 | const [tab, setTab] = useState(0); 67 | 68 | const handleChange = (event: React.ChangeEvent<{}>, newTab: number) => { 69 | setTab(newTab); 70 | }; 71 | // --------------------------------------------------------------- 72 | 73 | // Stateful functions for handling Drawer Logic ------------------ 74 | const [open, setOpen] = useState(false); 75 | 76 | const handleDrawerOpen = () => { 77 | setOpen(true); 78 | }; 79 | 80 | const handleDrawerClose = () => { 81 | setOpen(false); 82 | }; 83 | // --------------------------------------------------------------- 84 | 85 | // Stateful functions for handling Pause/Recording Button-------- 86 | const [isRecording, setIsRecording] = useState(true); 87 | 88 | const handlePauseRecClick = () => { 89 | if (isRecording) { 90 | // Send a postMessage to background.js with payload shape of 91 | // request = { name: 'pauseClicked', tabId: '' } 92 | port.postMessage({ 93 | name: "pauseClicked", 94 | tabId: chrome.devtools.inspectedWindow.tabId, 95 | }); 96 | setIsRecording(false); 97 | } else { 98 | // Send a postMessage to background.js with payload shape of 99 | // request = { name: 'recordClicked', tabId: '' } 100 | port.postMessage({ 101 | name: "recordClicked", 102 | tabId: chrome.devtools.inspectedWindow.tabId, 103 | }); 104 | setIsRecording(true); 105 | } 106 | }; 107 | // --------------------------------------------------------------- 108 | 109 | // Export Button Handling ---------------------------------------- 110 | const exportHandler = (text: string) => { 111 | // create invisible download anchor link 112 | const fileDownload = document.createElement("a"); 113 | 114 | // set file in anchor link 115 | fileDownload.href = URL.createObjectURL( 116 | new Blob([text], { type: "javascript" }) 117 | // new Blob([JSON.stringify(text)], {type: 'javascript'}) 118 | ); 119 | 120 | // set anchor as file download and click it 121 | fileDownload.setAttribute("download", "testfile.js"); 122 | fileDownload.click(); 123 | 124 | // remove file url 125 | URL.revokeObjectURL(fileDownload.href); 126 | }; 127 | // --------------------------------------------------------------- 128 | 129 | // Stateful functions for handling componentNames ---------------- 130 | const [componentNames, setComponentNames] = useState([]); 131 | const [componentData, setComponentData] = useState([]); 132 | // [app, todolist] 133 | // {app: describeTest} 134 | // [{app: describe, isChecked: true}, {}] 135 | const createComponentNamesArray = (messageString) => { 136 | let componentNames = []; 137 | let componentData = []; 138 | let checkedComps = []; 139 | const strArray = messageString.split("describe('"); 140 | 141 | // Iterate through the split messageString 142 | for (let i = 0; i < strArray.length; i++) { 143 | // if(tempObj[i]) // dont overwrite previously created objects 144 | 145 | // Initialize a temp object 146 | let tempObj = {}; 147 | if (i === 0) { 148 | tempObj = { import: strArray[0], isChecked: true }; 149 | // tempObj = { name: componentNames[i], import: strArray[0], isChecked: true }; 150 | componentData.push(tempObj); 151 | } else { 152 | // Substantiate the checkedComponents State variable 153 | checkedComps.push(true); 154 | 155 | componentNames.push(strArray[i].split(" ")[0]); 156 | // Set temporary key / value pairs that will change as for loop [i] increments 157 | let val = "describe('" + strArray[i]; 158 | let key = strArray[i].split(" ")[0]; 159 | 160 | tempObj = { [key]: val, isChecked: true }; 161 | 162 | componentData.push(tempObj); 163 | } 164 | // componentData[componentNames[i-1]] = "describe('" + strArray[i]; 165 | } 166 | setCheckedComponents(checkedComps); 167 | console.log("result of checkedComponents: ", checkedComps); 168 | setComponentNames(componentNames); 169 | // console.log('result of componentNames: ', componentNames); 170 | setComponentData(componentData); 171 | // console.log('result of componentData: ', componentData); 172 | 173 | codeFromComponentData(componentData); 174 | }; 175 | // --------------------------------------------------------------- 176 | 177 | // Function for Generating New Code String ----------------------- 178 | // Input: componentData = [{import: '...', isChecked: true}, {}, {}, {}] 179 | // Output: String = 'import ... describe ....' 180 | const codeFromComponentData = (componentData) => { 181 | // Initialize a resultant string 182 | let codeString = ""; 183 | // Iterate through the componentData array 184 | componentData.forEach((element) => { 185 | for (const key in element) { 186 | if (key !== "isChecked") { 187 | if (element.isChecked) { 188 | codeString += element[key]; 189 | } 190 | } 191 | } 192 | }); 193 | // Return the resultant string 194 | setCode(codeString); 195 | }; 196 | // --------------------------------------------------------------- 197 | 198 | // isChecked Handling -------------------------------------------- 199 | const [checkedImport, setCheckedImport] = React.useState(true); 200 | const [checkedComponents, setCheckedComponents] = React.useState([]); 201 | 202 | const handleCheckbox = (key: any) => () => { 203 | console.log("the index is ", key); 204 | let currComponentData = componentData; 205 | let currCheckedComponents = checkedComponents; 206 | // console.log(currComponentData); 207 | if (key === -1) { 208 | let tempObj = {}; 209 | tempObj = currComponentData[0]; 210 | if (tempObj["isChecked"]) { 211 | tempObj["isChecked"] = false; 212 | currComponentData.shift(); 213 | currComponentData.unshift(tempObj); 214 | setCheckedImport(false); 215 | setComponentData(currComponentData); 216 | codeFromComponentData(currComponentData); 217 | } else { 218 | tempObj["isChecked"] = true; 219 | currComponentData.shift(); 220 | currComponentData.unshift(tempObj); 221 | setCheckedImport(true); 222 | setComponentData(currComponentData); 223 | codeFromComponentData(currComponentData); 224 | } 225 | console.log(currComponentData); 226 | } else { 227 | let tempObj = {}; 228 | tempObj = currComponentData[key + 1]; 229 | if (currComponentData[key + 1]["isChecked"]) { 230 | tempObj["isChecked"] = false; 231 | currComponentData[key + 1] = tempObj; 232 | // Handle checkedComponents here 233 | currCheckedComponents[key] = false; 234 | setCheckedComponents(currCheckedComponents); 235 | 236 | setComponentData(currComponentData); 237 | codeFromComponentData(currComponentData); 238 | } else { 239 | tempObj["isChecked"] = true; 240 | currComponentData[key + 1] = tempObj; 241 | // Handle checkedComponents here 242 | currCheckedComponents[key] = true; 243 | setCheckedComponents(currCheckedComponents); 244 | 245 | setComponentData(currComponentData); 246 | codeFromComponentData(currComponentData); 247 | } 248 | console.log(currComponentData); 249 | } 250 | }; 251 | // --------------------------------------------------------------- 252 | 253 | // Stateful functions for handling code panel values ------------- 254 | const [code, setCode] = useState("loading..."); 255 | 256 | // Connect chrome to the port where name is "examin-demo" from (examin panel?) 257 | const port = chrome.runtime.connect({ name: "examin-demo" }); 258 | 259 | useEffect(() => { 260 | port.postMessage({ 261 | name: "connect", 262 | tabId: chrome.devtools.inspectedWindow.tabId, 263 | }); 264 | 265 | port.onMessage.addListener((message) => { 266 | // Update code displayed on Examin panel 267 | // console.log('message: ', message); 268 | console.log("useeffect fired"); 269 | // Start handling componentNames 270 | let text = message; 271 | createComponentNamesArray(text); 272 | setCheckedImport(true); 273 | // let compData = componentData; 274 | // console.log(compData); 275 | // codeFromComponentData(compData); 276 | // setCode(message); 277 | }); 278 | }, []); 279 | 280 | // Stateful functions for handling Root Dir --------------------- 281 | const [openRootDir, setOpenRootDir] = useState(false); 282 | const [userRootInput, setUserRootInput] = useState(""); 283 | 284 | const handleRootDirOpen = () => { 285 | setOpenRootDir(true); 286 | }; 287 | 288 | const handleRootDirClose = () => { 289 | setOpenRootDir(false); 290 | }; 291 | 292 | // handleSubmitRootDir submits user input (root-directory-name) 293 | const handleSubmitRootDir = () => { 294 | // setUserRootInput(''); 295 | // console.log('user root input', userRootInput); 296 | // let text = code; 297 | port.postMessage({ 298 | name: "submitRootDir", 299 | tabId: chrome.devtools.inspectedWindow.tabId, 300 | userInput: userRootInput, 301 | }); 302 | }; 303 | // --------------------------------------------------------------- 304 | 305 | // Copy Button Popover Handling ---------------------------------- 306 | const [anchorEl, setAnchorEl] = 307 | React.useState(null); 308 | 309 | const handleCopyClick = (event: React.MouseEvent) => { 310 | copy(code); 311 | setAnchorEl(event.currentTarget); 312 | }; 313 | 314 | const handleCopyClose = () => { 315 | setAnchorEl(null); 316 | }; 317 | 318 | const copyPopOpen = Boolean(anchorEl); 319 | // --------------------------------------------------------------- 320 | 321 | return ( 322 |
323 | 329 | 330 | 331 | 332 | Examin Logo 337 | 338 | 339 | 340 | 345 | 346 | 347 | 348 | 349 | 350 | 358 | {open ? : } 359 | 360 | 361 | 362 | 363 | 364 | 371 | {openRootDir ? ( 372 | 380 | setUserRootInput(e.target.value)} 395 | /> 396 | 403 | 404 | ) : ( 405 | <> 406 | )} 407 | 412 | 418 | 419 | 420 | 427 | 428 | 429 | 436 | Components 437 | 438 | 439 | 448 | 449 | 455 | 456 | 462 | 463 | 464 | 465 | 470 | {openRootDir ? ( 471 | 472 | ) : ( 473 | 474 | )} 475 | 476 | 477 | 478 | 479 | 480 | 481 | {componentNames.map((name, index) => ( 482 | 489 | 490 | {checkIsChecked(index)}} 497 | /> 498 | 499 | 500 | 501 | ))} 502 | 503 | 504 | 505 |
506 | {/* 512 | 513 | */} 514 | 515 | 527 | {isRecording ? ( 528 | 529 | ) : ( 530 | 531 | )} 532 | 533 | {/* 539 | 540 | */} 541 | { 546 | // copy(code); 547 | // }} 548 | onClick={handleCopyClick} 549 | > 550 | 551 | Copy 552 | 553 | 567 | 568 | Copied to Clipboard! 569 | 570 | 571 | exportHandler(code)} 576 | > 577 | 578 | Export 579 | 580 |
581 |
582 | ); 583 | }; 584 | 585 | export default ExaminPanel; 586 | -------------------------------------------------------------------------------- /src/app/components/Howto.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MarkdownPreview from '@uiw/react-markdown-preview'; 3 | 4 | const source = ` 5 | # How to Use 6 | 7 | **1. Setup** 8 | - **Install Jest/Enzyme** for your project
9 | ${`npm install jest enzyme enzyme-adapter-react-16 @babel/core @babel/preset-env`} 10 | - Add **presets** to your .babelrc file
11 | { "presets": ["@babel/preset-env", "@babel/preset-react"] } 12 | - Must have **React DevTools** extenion installed 13 | - Must be in developer mode (no minification or uglification) 14 | 15 | **2. Select Unit Tests** 16 | - Select the components you want to test 17 | - Edit import statements as needed 18 | 19 | **3. Export to Project Files** 20 | - Copy/Pase or Expot the Jest test file into your Project Directory 21 | 22 | ## Additional Information 23 | - **exmain.dev** 24 | - GitHub: **github.com/oslabs-beta/Examin** 25 | - Contact: **examindev@gmail.com** 26 | 27 | 28 | 29 | `; 30 | 31 | function Howto() { 32 | return ( 33 |
34 | 35 |
36 | ); 37 | } 38 | 39 | export default Howto; 40 | -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render( 6 | , 7 | document.getElementById("root") 8 | ); -------------------------------------------------------------------------------- /src/backend/injected.ts: -------------------------------------------------------------------------------- 1 | // This file contains the main logic that accesses the user's React application's state 2 | // Stores state on load 3 | // Uses React Dev Tools Global Hook to track state changes based on user interactions 4 | console.log('Currently in injected.js'); 5 | 6 | // any declaration is necessary here because the window will only have the react devtools global hook 7 | // property once the page is loading into a chrome browser with the 8 | declare const window: any; 9 | 10 | import testGenerator from './testGenerator'; 11 | import treeTraversal from './treeTraversal'; 12 | 13 | const dev = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; 14 | 15 | let currMemoizedState; 16 | 17 | let userInput = ''; 18 | // ----------------------------------------------------------------------------------- 19 | // Initializineg a message object which will be sent to content.js 20 | const msgObj = { type: 'addTest', message: [] }; 21 | // ----------------------------------------------------------------------------------- 22 | 23 | // Logic for pause/recording ----------------------------------------------------- 24 | const mode = { 25 | paused: false, 26 | }; 27 | 28 | // ----------------------------------------------------------------------------------- 29 | // Save fiberNode on load 30 | let fiberNode = dev.getFiberRoots(1).values().next().value.current.child; 31 | console.log('fiberNode on load:', fiberNode); 32 | // ----------------------------------------------------------------------------------- 33 | 34 | // Listens to messages from content.js 35 | const handleMessage = (request) => { 36 | if (request.data.name === 'pauseClicked') { 37 | mode.paused = true; 38 | } 39 | if (request.data.name === 'recordClicked') { 40 | mode.paused = false; 41 | } 42 | // Handle logic for 43 | if (request.data.name === 'submitRootDir') { 44 | userInput = request.data.userInput; 45 | // makes the tests and puts it into the examin window - ensuring refresh 46 | createAndSendTestArray(fiberNode, userInput) 47 | } 48 | }; 49 | 50 | window.addEventListener('message', handleMessage); 51 | 52 | // ----------------------------------------------------------------------------------- 53 | // findMemState returns the user's application's state 54 | const findMemState = ( node : FiberNode) => { 55 | // Finds the fiberNode on which memoizedState resides 56 | while (node.memoizedState === null) { 57 | node = node.child; 58 | } 59 | node = node.memoizedState; 60 | while (typeof node.memoizedState !== 'object') { 61 | node = node.next; 62 | } 63 | // return the memoizedState of the found fiberNode 64 | return node.memoizedState; 65 | }; 66 | 67 | // the createAndSendTestArray will use the fibernode and user input (root directory) to generate the array of 68 | // test strings and send that array to the panel to be rendered 69 | const createAndSendTestArray = (node : FiberNode, rootDirectory : string) => { 70 | //the imported treeTraversal function generates the array of objects needed by testGenerator to create the tests 71 | const testInfoArray = treeTraversal(node, rootDirectory); 72 | // testGenerator uses that array to create the array of test strings 73 | const tests = testGenerator(testInfoArray); 74 | // those testStrings are added to the msgObj object, which is then sent to the examin panel 75 | msgObj.message = tests; // msgObj = {type: 'addTest', message: []} 76 | window.postMessage(msgObj, '*'); 77 | } 78 | 79 | 80 | // ----------------------------------------------------------------------------------- 81 | // Generate tests for default state 82 | createAndSendTestArray(fiberNode, userInput) 83 | // ----------------------------------------------------------------------------------- 84 | 85 | // onCommitFiberRoot is USED TO TRACK STATE CHANGES ---------------------------------------- 86 | // patching / rewriting the onCommitFiberRoot functionality 87 | // onCommitFiberRoot runs functionality every time there is a change to the page 88 | dev.onCommitFiberRoot = (function (original) { 89 | console.log('original test', original) 90 | return function (...args) { 91 | if (!mode.paused) { 92 | // Reassign fiberNode when onCommitFiberRoot is invoked 93 | fiberNode = args[1].current.child; 94 | 95 | // save newMemState 96 | const newMemState = findMemState(fiberNode); 97 | 98 | // initialize a stateChange variable as a boolean which will tell if state changed or not 99 | // onCommitFiberRoot will run every time the user interacts with the page, regardless of if 100 | // that interaction actually changes state 101 | const stateChange = 102 | JSON.stringify(newMemState) !== JSON.stringify(currMemoizedState); 103 | // Run the test generation function only if the state has actually changed 104 | if (stateChange) { 105 | createAndSendTestArray(fiberNode, userInput); 106 | currMemoizedState = newMemState; 107 | } 108 | } 109 | }; 110 | })(dev.onCommitFiberRoot); 111 | // ----------------------------------------------------------------------------------- 112 | -------------------------------------------------------------------------------- /src/backend/testGenerator.ts: -------------------------------------------------------------------------------- 1 | const initialImportString = `import React from 'react'; 2 | import { configure, shallow, mount } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | configure({ adapter: new Adapter() }); 5 | 6 | `; 7 | 8 | const fileNameImports = (componentName: string, fileName: string) => { 9 | return `import ${componentName} from '${fileName}'; 10 | `; 11 | }; 12 | 13 | const describeBlockGen = (componentName: string) => { 14 | return ` 15 | describe('${componentName} Component', () => {`; 16 | }; 17 | 18 | const mockfnGen = (key: string) => { 19 | return ` 20 | let mock${key} = jest.fn();`; 21 | }; 22 | 23 | const propsGen = (componentName: string, propsStr: string) => { 24 | return ` 25 | let ${componentName}Props = ${propsStr}; 26 | `; 27 | }; 28 | 29 | const shallowNoProps = (componentName: string) => { 30 | return ` 31 | const wrapper = shallow(<${componentName} />); 32 | `; 33 | }; 34 | 35 | const shallowWithProps = (componentName: string) => { 36 | return ` 37 | const wrapper = shallow(<${componentName} {...${componentName}Props} />); 38 | `; 39 | }; 40 | 41 | const componentItGen = (key: string, childFreqKey: string) => { 42 | return ` 43 | it('Contains ${key} component', () => { 44 | expect(wrapper.find(${key}).length).toBe(${childFreqKey}) 45 | }) 46 | `; 47 | }; 48 | 49 | const htmlCountItGen = (componentName: string) => { 50 | return ` 51 | it('${componentName} includes html elements', () => {`; 52 | }; 53 | 54 | const htmlCountFindGen = (key: string, htmlFreqKey: string) => { 55 | return ` 56 | expect(wrapper.find('${key}').length).toEqual(${htmlFreqKey});`; 57 | }; 58 | 59 | const htmlInnertextItGen = (componentName: string) => { 60 | return ` 61 | it('${componentName} includes correct html innerText', () => {`; 62 | }; 63 | 64 | const htmlInnertextFindGen = (elementType: string, innerText: string) => { 65 | return ` 66 | expect(wrapper.find('${elementType}').text()).toEqual(${innerText});`; 67 | }; 68 | 69 | const endWithLineBreak = ` 70 | }); 71 | `; 72 | 73 | const endWithoutLineBreak = ` 74 | });`; 75 | 76 | const finalEnd = ` 77 | }); 78 | `; 79 | 80 | const testGenerator: TestGenerator = (componentData) => { 81 | const describeBlockArray = []; 82 | // --------- Create and fill the frequency table --------- 83 | // Initialize a cache object to check frequency of names 84 | const nameFreq = {}; 85 | // Iterate through the componentData object and fill the nameFreq object 86 | for (let i = 0; i < componentData.length; i++) { 87 | // Conditional: check if the name already is in the object 88 | if (componentData[i].name in nameFreq) { 89 | // If so, increment the freq of that element by 1 90 | nameFreq[componentData[i].name] += 1; 91 | // Else, assign to 1 92 | } else { 93 | nameFreq[componentData[i].name] = 1; 94 | } 95 | } 96 | // nameFreq = { "App": 1, "TodoList": 1, "TodoListItem": 2, "AddTodoForm": 1 } // This every component 97 | // --------------------------------------------------------- 98 | 99 | describeBlockArray.push(initialImportString); 100 | 101 | // Initialize an object to check if component has been added to describeBlock already 102 | const componentHasBeenAdded = {}; 103 | for (let i = 0; i < componentData.length; i++) { 104 | if (!componentHasBeenAdded[componentData[i].name]) { 105 | componentHasBeenAdded[componentData[i].name] = true; 106 | describeBlockArray.push( 107 | fileNameImports(componentData[i].name, componentData[i].fileName) 108 | ); 109 | } 110 | } 111 | 112 | // Iterate through the componentData and generate the initial component render tests 113 | for (let i = 0; i < componentData.length; i++) { 114 | // Push the initialization describe string into describeBlockArray 115 | describeBlockArray.push(describeBlockGen(componentData[i].name)); 116 | // Initialize a currentProps variable 117 | const currentProps = componentData[i].props; 118 | // Initialize a tempProps variable 119 | const tempProps = {}; 120 | 121 | // Conditional: check if the current element has props (length not equal to zero) 122 | if (Object.keys(currentProps).length !== 0) { 123 | // Enumerate through the props object 124 | for (const key in currentProps) { 125 | tempProps[key] = currentProps[key]; 126 | // Conditional: check if the type of the current key is a function 127 | if (typeof currentProps[key] === 'function') { 128 | // Push an initialization for the props functions into the describeBlock 129 | describeBlockArray.push(mockfnGen(key)); 130 | tempProps[key] = `mock${key}`; 131 | } 132 | } 133 | // Push the current element's props into the describeBlock 134 | console.log('temp props log: ', tempProps); 135 | console.log('strigified props', JSON.stringify(currentProps)); 136 | // ` 137 | // ); 138 | describeBlockArray.push( 139 | propsGen(componentData[i].name, JSON.stringify(tempProps)) 140 | ); 141 | } 142 | 143 | // Conditional: check if the current element has componentComponent children of length > 0 144 | if (Object.keys(componentData[i].props).length > 0) { 145 | // If so, push the mount component describe string into describeBlockArray 146 | describeBlockArray.push(shallowWithProps(componentData[i].name)); 147 | } else { 148 | describeBlockArray.push(shallowNoProps(componentData[i].name)); 149 | } 150 | 151 | // } 152 | // ----------- Construct Render Tests -------------------------- 153 | // Initialize a cache object to check frequency of child components 154 | const childFreq = {}; 155 | // Iterate through the componentData.componentChildren and fill the childFreq object 156 | for (let j = 0; j < componentData[i].componentChildren.length; j++) { 157 | // Conditional: check if the name already is in object 158 | if (componentData[i].componentChildren[j].componentName in childFreq) { 159 | // If so, increment the freq of that element by 1 160 | childFreq[componentData[i].componentChildren[j].componentName] += 1; 161 | // Else, assign to 1 162 | } else { 163 | childFreq[componentData[i].componentChildren[j].componentName] = 1; 164 | } 165 | } 166 | // Conditional: check if childFreq is not empty 167 | if (Object.keys(childFreq).length !== 0) { 168 | // Enumerate through childFreq 169 | for (const key in childFreq) { 170 | describeBlockArray.push(componentItGen(key, childFreq[key])); 171 | } 172 | } 173 | 174 | // Initialize a cache object to check frequency of child components 175 | const htmlFreq = {}; 176 | // Iterate through the componentData.componentChildren and fill the childFreq object 177 | for (let j = 0; j < componentData[i].htmlChildren.length; j++) { 178 | // Conditional: check if the name already is in object 179 | if (componentData[i].htmlChildren[j].elementType in htmlFreq) { 180 | // If so, increment the freq of that element by 1 181 | htmlFreq[componentData[i].htmlChildren[j].elementType] += 1; 182 | // Else, assign to 1 183 | } else { 184 | htmlFreq[componentData[i].htmlChildren[j].elementType] = 1; 185 | } 186 | } 187 | 188 | // Conditional: check if htmlChildren length is not 0 189 | if (componentData[i].htmlChildren.length !== 0) { 190 | describeBlockArray.push(htmlCountItGen(componentData[i].name)); 191 | // Enumerate through the htmlFreq object 192 | for (const key in htmlFreq) { 193 | describeBlockArray.push(htmlCountFindGen(key, htmlFreq[key])); 194 | } 195 | describeBlockArray.push(endWithLineBreak); 196 | 197 | if (componentData[i].htmlChildren.some((el) => el.innerText !== '')) { 198 | describeBlockArray.push(htmlInnertextItGen(componentData[i].name)); 199 | // Iterate through the htmlChildren array 200 | for (let j = 0; j < componentData[i].htmlChildren.length; j++) { 201 | // Conditional: check if htmlChildren.innerText is not an empty string or undefined 202 | if ( 203 | componentData[i].htmlChildren[j].innerText !== '' && 204 | componentData[i].htmlChildren[j].innerText !== undefined 205 | ) { 206 | if (htmlFreq[componentData[i].htmlChildren[j].elementType] === 1) { 207 | let innerTextStr: string = JSON.stringify( 208 | componentData[i].htmlChildren[j].innerText 209 | ); 210 | // regex to remove new line characters from each string 211 | const regex = /\\n/g; 212 | if (innerTextStr) { 213 | innerTextStr = innerTextStr.replace(regex, ''); 214 | } 215 | describeBlockArray.push( 216 | htmlInnertextFindGen( 217 | componentData[i].htmlChildren[j].elementType, 218 | innerTextStr 219 | ) 220 | ); 221 | } 222 | } 223 | } 224 | describeBlockArray.push(endWithoutLineBreak); 225 | } 226 | } 227 | 228 | // Closing the describeBlock 229 | describeBlockArray.push(finalEnd); 230 | } // Closing the current element componentData for loop 231 | // Return the describeBlockArray 232 | return describeBlockArray; 233 | }; 234 | 235 | export default testGenerator; 236 | -------------------------------------------------------------------------------- /src/backend/treeTraversal.ts: -------------------------------------------------------------------------------- 1 | // function to acquire the file path from the users fibernode 2 | const getComponentFileName = (node: FiberNode, rootDirectory: string) => { 3 | // if the file path is not accessible, return a default value for the user to fill in 4 | if (!node.child || !node.child._debugSource) { 5 | return ''; 6 | } 7 | // regex expression to remove backslashes with forward slashes in the file path 8 | const regex = /\\/g; 9 | // acquire the filepath from the passed in node 10 | let fileName: string = node.child._debugSource.fileName; 11 | // if the user has not input a root Directory or has input a root directory that is not included in the absolute filepath 12 | // return the absolute filepath 13 | if (rootDirectory === '' || !fileName.includes(rootDirectory)) { 14 | fileName = fileName.replace(regex, '/'); 15 | return fileName; 16 | } 17 | // if the user has input a filepath and the absolute filepath includes that filepath, 18 | // return .. followed by the path after the root directory (the relative filepath) 19 | 20 | const indexOfFirst = fileName.indexOf(rootDirectory); 21 | const index = indexOfFirst + rootDirectory.length; 22 | fileName = fileName.replace(regex, '/'); 23 | return '..' + fileName.slice(index); 24 | }; 25 | 26 | // function to acquire the information of react functional component children 27 | const grabComponentChildInfo = (node: FiberNode) => { 28 | // initialize the object that will be returned 29 | const componentChildInfo: ComponentChildInfo = { 30 | componentName: '', 31 | }; 32 | // assign the component name key of that object to the name of the passed in node 33 | componentChildInfo.componentName = node.elementType.name; 34 | return componentChildInfo; 35 | }; 36 | 37 | // function to acquire the information of html component children 38 | const grabHtmlChildInfo = (node: FiberNode) => { 39 | // initialize the object that will be returned 40 | const htmlChildInfo: HtmlChildInfo = { 41 | innerText: '', 42 | elementType: '', 43 | }; 44 | // assign the elementType of the node to that key in the htmlChildInfo object 45 | htmlChildInfo.elementType = node.elementType; 46 | // conditional to check if the html component is a container which will include other components 47 | // If so, the innerText will not be used. This also needs to check if there is a statenode key, which is what stores the innerText 48 | if ( 49 | htmlChildInfo.elementType !== 'div' && 50 | htmlChildInfo.elementType !== 'ul' && 51 | node.stateNode 52 | ) { 53 | htmlChildInfo.innerText = node.stateNode.innerText; 54 | } 55 | return htmlChildInfo; 56 | }; 57 | 58 | // this function will be run whenever the treeTraversal algorithm finds a react functional component. 59 | // It will generate an object which will correspond to a single describe block in the final examin panel 60 | const getComponentInfo = (node: FiberNode, rootDirectory: string) => { 61 | // initialize the object that will be returned 62 | const componentInfo: ComponentInfo = { 63 | name: '', 64 | fileName: '', 65 | props: {}, 66 | componentChildren: [], 67 | htmlChildren: [], 68 | }; 69 | // assign the non-array elements of the object. These will not depend on the children of the node in any way 70 | componentInfo.name = node.elementType.name; 71 | componentInfo.fileName = getComponentFileName(node, rootDirectory); 72 | componentInfo.props = node.memoizedProps; 73 | 74 | // recursive helper function to dig through the children of the passed-in node and pull out the 75 | // function and html component children to populate the relevant arrays 76 | const getComponentInfoHelper = (currNode: FiberNode) => { 77 | // currNode to the child of the currNode argument. This will begin the process of looking through the child components. 78 | currNode = currNode.child; 79 | // use a while loop to search through all children (the original child and all siblings) of the original currNode 80 | while (currNode !== null) { 81 | // Conditional to check if currNode is a react functional component 82 | if (currNode.elementType && currNode.elementType.name) { 83 | // there is no recursive call in this case. Because all of the tests are shallow renders we do not want this logic digging 84 | // further into functional component children 85 | componentInfo.componentChildren.push(grabComponentChildInfo(currNode)); 86 | 87 | // Conditional to check if currNode is an html component 88 | } else if (currNode.elementType) { 89 | componentInfo.htmlChildren.push(grabHtmlChildInfo(currNode)); 90 | // We do want to dig further into any html children in order to generate all relevant tests. 91 | // Thus, there is a recursive call here 92 | getComponentInfoHelper(currNode); 93 | } 94 | currNode = currNode.sibling; 95 | } 96 | }; 97 | // Call the recursive helper function, passing in the original node 98 | getComponentInfoHelper(node); 99 | 100 | return componentInfo; 101 | }; 102 | 103 | // This function will loop through the entirety of the user's application's fiber tree 104 | // in order to populate the array of objects the testGenerator function needs to create the tests 105 | const treeTraversal: TreeTraversal = (fiberNode, rootDirectory) => { 106 | // Initialize the array of objects which will be returned 107 | const testInfoArray: Array = []; 108 | // Recursive helper function to loop through all contents of the fibernode. 109 | // The fiber node is a tree shaped data structure in which each node may have an arbitrarily large number of children. 110 | // The first child of a node is node.child, the second is node.child.sibling, the third is node.child.sibling.sibling, and so on. 111 | const treeHelper = (currNode: FiberNode) => { 112 | // check if the current node is null or its elementType is null. If so, the function has reached the bottom 113 | // most element in that series of children and is finished 114 | if (currNode === null || currNode.elementType === null) return; 115 | // check if the current node is a react functional component. If so, use the getComponentInfo function to pass 116 | // that node's information into the testInfoArray. 117 | if (currNode.elementType.name) { 118 | testInfoArray.push(getComponentInfo(currNode, rootDirectory)); 119 | } 120 | // reassign currNode to the child of the passed in node in order to begin digging through the tree 121 | currNode = currNode.child; 122 | // while loop to look through the first child and all siblings of the passed in node 123 | while (currNode !== null) { 124 | // recursive call here to look through all children of each node, and their children etc. 125 | treeHelper(currNode); 126 | currNode = currNode.sibling; 127 | } 128 | }; 129 | treeHelper(fiberNode); 130 | return testInfoArray; 131 | }; 132 | 133 | export default treeTraversal; 134 | -------------------------------------------------------------------------------- /src/backend/types.d.ts: -------------------------------------------------------------------------------- 1 | type ComponentChildInfo = { 2 | componentName : string; 3 | } 4 | 5 | type HtmlChildInfo = { 6 | innerText: string; 7 | elementType: string; 8 | } 9 | 10 | // any types are used when those values are pulled from the user's page and can have different shapes 11 | 12 | type ComponentInfo = { 13 | name: string; 14 | fileName: string; 15 | props: any; 16 | componentChildren: Array; 17 | htmlChildren: Array; 18 | } 19 | 20 | type FiberNode = { 21 | elementType : any; 22 | child : FiberNode; 23 | sibling : FiberNode; 24 | _debugSource : { 25 | fileName: string; 26 | } 27 | memoizedProps : any; 28 | memoizedState: any; 29 | stateNode : { 30 | innerText : string; 31 | } 32 | next: FiberNode; 33 | } 34 | 35 | type TreeTraversal = ( fiberNode : FiberNode, rootDirectory : string) => Array 36 | 37 | type TestGenerator = ( componentData: Array ) => Array -------------------------------------------------------------------------------- /src/extension/assets/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/128.png -------------------------------------------------------------------------------- /src/extension/assets/148.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/148.png -------------------------------------------------------------------------------- /src/extension/assets/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/16.png -------------------------------------------------------------------------------- /src/extension/assets/192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/192.png -------------------------------------------------------------------------------- /src/extension/assets/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/38.png -------------------------------------------------------------------------------- /src/extension/assets/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/48.png -------------------------------------------------------------------------------- /src/extension/assets/edit-import-statements.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/edit-import-statements.gif -------------------------------------------------------------------------------- /src/extension/assets/examin-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/examin-small.png -------------------------------------------------------------------------------- /src/extension/assets/examin-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/extension/assets/failing-test-sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/failing-test-sample.png -------------------------------------------------------------------------------- /src/extension/assets/githubcover-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/githubcover-img.png -------------------------------------------------------------------------------- /src/extension/assets/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/main.png -------------------------------------------------------------------------------- /src/extension/assets/state-changes-component-selection.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/state-changes-component-selection.gif -------------------------------------------------------------------------------- /src/extension/assets/step1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/step1.gif -------------------------------------------------------------------------------- /src/extension/assets/step2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/step2.gif -------------------------------------------------------------------------------- /src/extension/assets/step3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Examin/9e2ed8d4201cb3076cbd9e9b0331efa421c69c76/src/extension/assets/step3.gif -------------------------------------------------------------------------------- /src/extension/background.js: -------------------------------------------------------------------------------- 1 | // The background script (background.js) is running in the background of the Chrome browser 2 | // this is analogous a server.js script where the server (or in this case a background.js) script 3 | // runs continually to listen and route requests. 4 | 5 | // In background.js, messages are analogous to requests 6 | 7 | console.log('logging in background.js'); 8 | 9 | const connections = {}; 10 | 11 | let firstRun = true; 12 | let joinedMsg = 'loading...'; 13 | 14 | // Chrome on connecting to the Examin Panel, add an Listener 15 | chrome.runtime.onConnect.addListener((port) => { 16 | console.log('in port connection: ', port); 17 | // create a new variable for a listener function 18 | const listenerForDevtool = (msg, sender, sendResponse) => { 19 | // msg = request 20 | // creates a new key/value pair of current window & devtools tab 21 | 22 | // Initial request (or msg) shape = { 23 | // name: 'connect', 24 | // tabId: chrome.devtools.inspectedWindow.tabId, 25 | // } 26 | if (msg.name === 'connect' && msg.tabId) { 27 | // on 28 | console.log('The tabId is: ', msg.tabId); 29 | connections[msg.tabId] = port; 30 | 31 | // Chrome sends a message to the tab at tabId to content.js with a shape of 32 | // request = { name: 'initial page load', tabId: msg.tabId } 33 | // connections[msg.tabId].postMessage(joinedMsg); 34 | 35 | chrome.tabs.sendMessage(msg.tabId, { 36 | name: 'initial panel load', 37 | tabId: msg.tabId, 38 | }); 39 | } else if (msg.name === 'pauseClicked' && msg.tabId) { 40 | console.log('background.js hears pauseClicked!'); 41 | // Chrome sends a message to the tab at tabId to content.js with a shape of 42 | // request = { name: 'pauseClicked ' } 43 | chrome.tabs.sendMessage(msg.tabId, msg); 44 | } else if (msg.name === 'recordClicked' && msg.tabId) { 45 | console.log('background.js hears recordClicked!'); 46 | // Chrome sends a message to the tab at tabId to content.js with a shape of 47 | // request = { name: 'recordClicked' } 48 | chrome.tabs.sendMessage(msg.tabId, msg); 49 | } else if (msg.name === 'submitRootDir') { 50 | console.log('background.js hears submitRootDir!'); 51 | console.log('user input', msg.userInput); 52 | chrome.tabs.sendMessage(msg.tabId, msg); 53 | } 54 | }; 55 | // Listen from App.jsx 56 | // consistently listening on open port 57 | port.onMessage.addListener(listenerForDevtool); 58 | 59 | // console.log("port.sender is: ", port.sender); 60 | console.log('Listing from devtool successfully made'); 61 | // chrome.runtime.sendMessage({ action: 'testMessage' }); 62 | }); 63 | 64 | // Chrome listening for messages (from content.js?) 65 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 66 | // IGNORE THE AUTOMATIC MESSAGE SENT BY CHROME WHEN CONTENT SCRIPT IS FIRST LOADED 67 | if (request.type === 'SIGN_CONNECT') { 68 | return true; 69 | } 70 | // console.log(request.action) 71 | 72 | const { action, message } = request; 73 | const tabId = sender.tab.id; 74 | 75 | // Check for action payload from request body 76 | switch (action) { 77 | case 'injectScript': { 78 | // Injects injected.js into the body element of the user's application 79 | console.log('injecting script to the current tab'); 80 | 81 | chrome.tabs.executeScript(tabId, { 82 | code: ` 83 | console.log('injecting javascript----'); 84 | 85 | const injectScript = (file, tag) => { 86 | const htmlBody = document.getElementsByTagName(tag)[0]; 87 | const script = document.createElement('script'); 88 | script.setAttribute('type', 'text/javascript'); 89 | script.setAttribute('src', file); 90 | htmlBody.appendChild(script); 91 | }; 92 | injectScript(chrome.runtime.getURL('bundles/backend.bundle.js'), 'body'); 93 | `, 94 | }); 95 | 96 | console.log('after injectScript ran, finished injecting'); 97 | break; 98 | } 99 | // Where action = 'addTest', and message = testArray; 100 | // shape of message being recieve = { action: 'addTest', message: [(testArray)] } 101 | case 'addTest': { 102 | console.log('received addTest'); 103 | console.log('The request message is: ', message); 104 | joinedMsg = message.join(''); 105 | // Sending another message to the front-end examin panel (at the current tab) 106 | // Access tabId property on connections object and posting a message to Examin frontend panel 107 | // connections[tabId] value is the id of user’s application’s tab 108 | if (connections[tabId.toString()]) { 109 | connections[tabId.toString()].postMessage(joinedMsg); 110 | } 111 | 112 | break; 113 | } 114 | case 'initial panel load': { 115 | console.log('received initial panel load in background.js'); 116 | connections[tabId.toString()].postMessage(joinedMsg); 117 | } 118 | default: 119 | break; 120 | } 121 | }); 122 | -------------------------------------------------------------------------------- /src/extension/content.js: -------------------------------------------------------------------------------- 1 | // Content scripts are files that run in the context of the webpage, currently limited to only running on 2 | // "http://localhost/*", "https://localhost/*" 3 | 4 | // Runs when user navigates to localhost:3000 5 | 6 | console.log('Chrome Extension READY!!'); 7 | 8 | let firstRun = true; 9 | 10 | // Listener for Frontend to Backend --------------------------------------- 11 | // Window Listening for messeges in the window, if it recieves a 'message' (listening for injected.js) 12 | window.addEventListener('message', (request, sender, sendResponse) => { 13 | // the shape of request.data = { type: 'addTest', message: [(testArray)] } 14 | if (request.data.type === 'addTest') { 15 | console.log('in content.js, request.data: ', request.data); 16 | // Send a message to background.js with the shape { action: 'addTest', message: [(testArray)] } 17 | return chrome.runtime.sendMessage({ 18 | action: request.data.type, 19 | message: request.data.message, 20 | }); 21 | } 22 | }); 23 | // ------------------------------------------------------------------------ 24 | 25 | // Listener for Chrome Browser -------------------------------------------- 26 | // Chrome Listening for messeges in the browser, if it recieves a 'message' (listening for background.js) 27 | chrome.runtime.onMessage.addListener((request) => { 28 | console.log('Recieved a msg from background.js, request is: ', request); 29 | 30 | if (request.name === 'initial panel load') { 31 | // Send a message back to background.js to initialize the initial state 32 | console.log('In initial panel load!'); 33 | chrome.runtime.sendMessage({ action: 'initial panel load' }); 34 | } else if (request.name === 'pauseClicked' && request.tabId) { 35 | // Send a postMessage to window to forward request to injected.js 36 | window.postMessage(request, '*'); 37 | } else if (request.name === 'recordClicked' && request.tabId) { 38 | // Send a postMessage to window to forward request to injected.js 39 | window.postMessage(request, '*'); 40 | } else if (request.name === 'submitRootDir' && request.tabId) { 41 | // Send a postMessage to window to forward request to injected.js 42 | window.postMessage(request, '*'); 43 | } 44 | }); 45 | // ------------------------------------------------------------------------ 46 | 47 | // Send a message to background.js with the shape { action: 'injectScript' } to injectScript 48 | chrome.runtime.sendMessage({ action: 'injectScript' }); 49 | -------------------------------------------------------------------------------- /src/extension/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/extension/devtools.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create('Examin', null, '/panel.html', null); 2 | -------------------------------------------------------------------------------- /src/extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Examin", 3 | "description": "A Chrome extension developer tool that generators Jest/Enzyme render unit tests for React applications", 4 | "version": "1.0.0.1", 5 | "author": "Ty Doan, Kirsten Yoon, Nicholas Brush, Cliff Assuncao", 6 | "manifest_version": 2, 7 | "background": { 8 | "scripts": ["bundles/background.bundle.js"], 9 | "persistent": false 10 | }, 11 | "content_scripts": [ 12 | { 13 | "matches": ["http://localhost/*", "https://localhost/*"], 14 | "js": ["bundles/content.bundle.js"] 15 | } 16 | ], 17 | "icons": { 18 | "16": "assets/16.png", 19 | "38": "assets/38.png", 20 | "48": "assets/48.png", 21 | "128": "assets/128.png", 22 | "148": "assets/148.png", 23 | "192": "assets/192.png" 24 | }, 25 | "web_accessible_resources": ["bundles/backend.bundle.js"], 26 | "permissions": ["http://localhost/*", "https://localhost/*"], 27 | "externally_connectable": { 28 | "matches": ["http://localhost/*", "https://localhost/*"] 29 | }, 30 | "devtools_page": "./devtools.html", 31 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'" 32 | } 33 | -------------------------------------------------------------------------------- /src/extension/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Examin 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/tests/newest.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure, shallow, mount } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import renderer from 'react-test-renderer'; 5 | import App from '../src/App'; 6 | import { TodoList } from '../src/TodoList'; 7 | import { TodoListItem } from '../src/TodoListItem'; 8 | import { AddTodoForm } from '../src/AddTodoForm'; 9 | 10 | // import from ''; 11 | 12 | configure({ adapter: new Adapter() }); 13 | 14 | describe('React unit tests', () => { 15 | let props; 16 | let mockToggleComplete = jest.fn(); 17 | 18 | // input: array of objects 19 | // objects contain information required to generate tests for each component 20 | // input = 21 | // [ 22 | // { 23 | // name: "", 24 | // componentChildren: [ 25 | // { 26 | // componentName: "", 27 | // componentIndex: 0, 28 | // }, 29 | // ] 30 | // htmlChildren: [ 31 | // { 32 | // elementType: 'li', 33 | // innerHtml: 'Walk the dog', 34 | // elementIndex: 0 35 | // }, 36 | // { 37 | // elementType: 'li', 38 | // innerHtml: 'Second item', 39 | // elementIndex: 1, 40 | // } 41 | // ] 42 | // props: [] 43 | // } 44 | // ] 45 | 46 | // output: 47 | // component name 48 | // expect(wrap.find('li').at([index of the object])).toEqual('Walk the dog'); 49 | 50 | describe('App Component', () => { 51 | const wrapper = shallow(); 52 | 53 | beforeEach(() => {}); 54 | 55 | it('Contains TodoList component', () => { 56 | // const wrapper = shallow(); 57 | // expect(wrapper.find(TodoList)).to.have.lengthOf(1); 58 | expect(wrapper.find(TodoList).length).toBe(1); 59 | // expect(wrapper.find(AddTodoForm).length).toBe(1); 60 | }); 61 | 62 | it('Contains AddTodoForm component', () => { 63 | // const wrapper = shallow(); 64 | // expect(wrapper.find(TodoList)).to.have.lengthOf(1); 65 | // expect(wrapper.find(TodoList).length).toBe(1); 66 | expect(wrapper.find(AddTodoForm).length).toBe(1); 67 | }); 68 | 69 | it('should update state on click', () => { 70 | const updateState = jest.fn(); 71 | const wrapper = mount(); 72 | const handleClick = jest.spyOn(React, 'useState'); 73 | handleClick.mockImplementation((size) => [size, updateState]); 74 | 75 | // Specify element type and index to test state change 76 | // wrapper.find().at(1).simulate('click'); 77 | wrapper.find('input').at(1).simulate('click'); 78 | expect(updateState).toBeTruthy(); 79 | }); 80 | }); 81 | 82 | describe('TodoList Component', () => { 83 | beforeEach(() => { 84 | props = { 85 | todos: [ 86 | { text: 'Walk the dog', complete: true }, 87 | { text: 'Make app', complete: false }, 88 | // { text: 'test', complete: false }, 89 | ], 90 | toggleComplete: mockToggleComplete, 91 | }; 92 | }); 93 | 94 | it('includes two list items', () => { 95 | const wrap = mount(); 96 | expect(wrap.find('li').length).toEqual(props.todos.length); 97 | }); 98 | }); 99 | 100 | describe('TodoListItem Component', () => { 101 | beforeEach(() => { 102 | props = { 103 | todo: { text: 'Walk the dog', complete: true }, 104 | toggleComplete: mockToggleComplete, 105 | }; 106 | }); 107 | 108 | it('contains input (checkbox)', () => { 109 | const wrap = mount(); 110 | expect(wrap.find('input').length).toEqual(1); 111 | }); 112 | }); 113 | }); 114 | -------------------------------------------------------------------------------- /src/tests/test.js: -------------------------------------------------------------------------------- 1 | // Edits to the codepanel still copy! 2 | 3 | describe('State Initializes', () => { 4 | let state; 5 | 6 | beforeEach(() => { 7 | state = [{"text":"Walk the dog","complete":true},{"text":"Write app","complete":false}]; 8 | }); 9 | 10 | it('should return a default state when given an undefined input', () => { 11 | expect(state[0]).toEqual({"text":"Walk the dog","complete":true}); 12 | expect(state[1]).toEqual({"text":"Write app","complete":false}); 13 | expect(state).toEqual( 14 | [{"text":"Walk the dog","complete":true},{"text":"Write app","complete":false}] 15 | ); 16 | }); 17 | }); 18 | 19 | describe('Adds to State', () => { 20 | let prevState, currState, stateDiff; 21 | 22 | beforeEach(() => { 23 | prevState = [{"text":"Walk the dog","complete":true},{"text":"Write app","complete":false}]; 24 | currState = [{"text":"Walk the dog","complete":true},{"text":"Write app","complete":false},{"text":"test","complete":false}]; 25 | stateDiff = {"added":{"2":{"text":"test","complete":false}},"deleted":{},"updated":{}}; 26 | }); 27 | 28 | it('prevMemoizedState should not equal currMemoizedState', () => { 29 | expect(prevState).not.toEqual(currState); 30 | }); 31 | it('should useStateHook variable where component changed', () => { 32 | expect(currState).toEqual([{"text":"Walk the dog","complete":true},{"text":"Write app","complete":false},{"text":"test","complete":false}]); 33 | expect(stateDiff).toEqual({"added":{"2":{"text":"test","complete":false}},"deleted":{},"updated":{}}); 34 | }); 35 | }); 36 | 37 | describe('State Updates', () => { 38 | let prevState, currState, stateDiff; 39 | 40 | beforeEach(() => { 41 | prevState = [{"text":"Walk the dog","complete":true},{"text":"Write app","complete":false},{"text":"test","complete":false}]; 42 | currState = [{"text":"Walk the dog","complete":true},{"text":"Write app","complete":false},{"text":"test","complete":true}]; 43 | stateDiff = {"added":{},"deleted":{},"updated":{"2":{"complete":true}}}; 44 | }); 45 | 46 | it('prevMemoizedState should not equal currMemoizedState', () => { 47 | expect(prevState).not.toEqual(currState); 48 | }); 49 | it('should useStateHook variable where component changed', () => { 50 | expect(currState).toEqual([{"text":"Walk the dog","complete":true},{"text":"Write app","complete":false},{"text":"test","complete":true}]); 51 | expect(stateDiff).toEqual({"added":{},"deleted":{},"updated":{"2":{"complete":true}}}); 52 | }); 53 | }); -------------------------------------------------------------------------------- /tests/enzyme.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure, shallow, mount } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | import App from '../src/app/App.tsx'; 6 | import ExaminPanel from '../src/app/components/ExaminPanel.tsx'; 7 | import { AppBar, Grid } from '@material-ui/core'; 8 | import { createShallow } from '@material-ui/core/test-utils'; 9 | 10 | jest.mock('codemirror/lib/codemirror.css'); 11 | 12 | configure({ adapter: new Adapter() }); 13 | 14 | describe('App Component', () => { 15 | const wrapper = shallow(); 16 | 17 | it('Contains ExaminPanel component', () => { 18 | expect(wrapper.find(ExaminPanel).length).toBe(1); 19 | }); 20 | }); 21 | 22 | xdescribe('ExaminPanel Component', () => { 23 | const wrapper = shallow(); 24 | 25 | it('Contains MaterialUI Box component', () => { 26 | expect(wrapper.find(Box).length).toBe(1); 27 | expect(wrapper.find(AppBar).length).toBe(1); 28 | }); 29 | }); 30 | 31 | describe('', () => { 32 | let shallow; 33 | let wrapper; 34 | 35 | beforeAll(() => { 36 | shallow = createShallow(); 37 | }); 38 | 39 | it('shallow render', () => { 40 | wrapper = shallow(); 41 | }); 42 | 43 | xit('contains Grid', () => { 44 | expect(wrapper.find(Grid).length).toBe(0); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tests/extension-tests.js: -------------------------------------------------------------------------------- 1 | import { chrome } from 'jest-chrome'; 2 | 3 | describe('chrome api functions', () => { 4 | const manifest = { 5 | name: 'Examin', 6 | description: 7 | 'A Chrome extension developer tool that generators Jest/Enzyme render unit tests for React applications', 8 | version: '1.0.0.1', 9 | author: 'Ty Doan, Kirsten Yoon, Nicholas Brush, Cliff Assuncao', 10 | manifest_version: 2, 11 | background: { 12 | scripts: ['bundles/background.bundle.js'], 13 | persistent: false, 14 | }, 15 | content_scripts: [ 16 | { 17 | matches: ['http://localhost/*', 'https://localhost/*'], 18 | js: ['bundles/content.bundle.js'], 19 | }, 20 | ], 21 | icons: { 22 | 16: 'assets/16.png', 23 | 38: 'assets/38.png', 24 | 48: 'assets/48.png', 25 | 128: 'assets/128.png', 26 | 148: 'assets/148.png', 27 | 192: 'assets/192.png', 28 | }, 29 | web_accessible_resources: ['bundles/backend.bundle.js'], 30 | permissions: ['http://localhost/*', 'https://localhost/*'], 31 | externally_connectable: { 32 | matches: ['http://localhost/*', 'https://localhost/*'], 33 | }, 34 | devtools_page: './devtools.html', 35 | content_security_policy: 36 | "script-src 'self' 'unsafe-eval'; object-src 'self'", 37 | }; 38 | 39 | chrome.runtime.getManifest.mockImplementation(() => manifest); 40 | 41 | it('correct manifest', () => { 42 | expect(chrome.runtime.getManifest()).toEqual(manifest); 43 | expect(chrome.runtime.getManifest).toBeCalled(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ 13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | // "outDir": "./", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": false, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ 44 | 45 | /* Module Resolution Options */ 46 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 47 | "resolveJsonModule": true, // delete if .ts breaks 48 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 49 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 50 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 51 | // "typeRoots": [], /* List of folders to include type definitions from. */ 52 | // "types": [], /* Type declaration files to be included in compilation. */ 53 | "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 54 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 55 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 56 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 57 | 58 | /* Source Map Options */ 59 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 61 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 62 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 63 | 64 | /* Experimental Options */ 65 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 66 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 67 | 68 | /* Advanced Options */ 69 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 70 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 71 | }, 72 | "exclude": ["./src/app/__tests__", "./src/backend/__tests__"], 73 | "typeDocOptions": { 74 | "mode": "file", 75 | "out": "docs" 76 | }, 77 | } 78 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ChromeExtensionReloader = require('webpack-chrome-extension-reloader'); 3 | 4 | // Webpack Config Object 5 | const config = { 6 | devtool: 'eval-cheap-module-source-map', 7 | entry: { 8 | // Entry for front-end files 9 | app: './src/app/index.tsx', 10 | // Entry for background.js service worker 11 | background: './src/extension/background.js', 12 | // Entry for chrome extension content script 13 | content: './src/extension/content.js', 14 | // Entry for injected backend file bundle 15 | backend: './src/backend/injected.ts', 16 | }, 17 | output: { 18 | path: path.resolve(__dirname, 'src/extension/bundles'), 19 | filename: '[name].bundle.js', 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.tsx?$/, 25 | use: 'ts-loader', 26 | exclude: /node_modules/, 27 | }, 28 | { 29 | test: /\.jsx?/, 30 | exclude: /(node_modules)/, 31 | resolve: { 32 | extensions: ['.js', '.jsx'], 33 | }, 34 | use: { 35 | loader: 'babel-loader', 36 | options: { 37 | presets: ['@babel/preset-env', '@babel/preset-react'], 38 | }, 39 | }, 40 | }, 41 | { 42 | test: /\.html$/i, 43 | loader: 'html-loader', 44 | }, 45 | { 46 | test: /\.scss$/, 47 | use: ['style-loader', 'css-loader', 'sass-loader'], 48 | }, 49 | { 50 | test: /\.css$/, 51 | use: ['style-loader', 'css-loader'], 52 | }, 53 | ], 54 | }, 55 | resolve: { 56 | extensions: ['.tsx', '.ts', '.js', '.jsx'], 57 | }, 58 | plugins: [], 59 | }; 60 | 61 | module.exports = (env, argv) => { 62 | if (argv.mode === 'development') { 63 | config.plugins.push( 64 | new ChromeExtensionReloader({ 65 | entries: { 66 | contentScript: ['content'], 67 | background: ['background'], 68 | }, 69 | }) 70 | ); 71 | } else { 72 | config.mode = 'production'; 73 | } 74 | return config; 75 | }; 76 | --------------------------------------------------------------------------------