├── .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 |
2 |
3 |
4 | Examin
5 | Automatic React Unit Test Generator
6 | examin.dev | Install Examin
7 |
8 |
9 |
10 |
11 | [](https://github.com/facebook/react/blob/master/LICENSE)
12 | []()
13 | []()
14 | []()
15 | []()
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 |
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 |
63 |
64 | - Editing import statements
65 |
66 |
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 |
50 | {value === index && (
51 |
52 | {children}
53 |
54 | )}
55 |
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 |
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 |
401 | Submit
402 |
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 |
--------------------------------------------------------------------------------